Compare commits

..

83 Commits

Author SHA1 Message Date
Zlatin Balevsky
761bf0a177 Release 0.6.2 2019-11-10 18:31:30 +00:00
Zlatin Balevsky
bd873211c0 wip on file preview 2019-11-10 14:50:19 +00:00
Zlatin Balevsky
036971cfe5 wip on file preview 2019-11-10 13:59:01 +00:00
Zlatin Balevsky
a2637570b1 Release 0.6.1 2019-11-10 06:23:28 +00:00
Zlatin Balevsky
6012adbeab fix unsharing of files with comments 2019-11-10 06:04:57 +00:00
Zlatin Balevsky
8f6b6b0caa update test for new json format 2019-11-10 05:20:09 +00:00
Zlatin Balevsky
8f3b5aea8d store lowercases in search index 2019-11-10 05:14:31 +00:00
Zlatin Balevsky
ee098ace8e update readme 2019-11-09 20:11:03 +00:00
Zlatin Balevsky
5d8401e4bf avoid NPE, pending further investigation 2019-11-09 20:10:21 +00:00
Zlatin Balevsky
fbf9add82a Release 0.6.0 2019-11-09 19:27:36 +00:00
Zlatin Balevsky
7379263fef extended signature in cli 2019-11-09 18:34:34 +00:00
Zlatin Balevsky
7d50843754 make signed queries mandatory 2019-11-09 17:03:38 +00:00
Zlatin Balevsky
f4a2864942 add extended signature in queries to prevent replay attacks 2019-11-09 16:39:16 +00:00
Zlatin Balevsky
afaadf65a4 only set selected row if the table contains that many rows. That fixes an AIOOBE 2019-11-09 15:14:14 +00:00
Zlatin Balevsky
7bd422d6b4 another instance of unexplained npe 2019-11-09 12:36:59 +00:00
Zlatin Balevsky
3f47274f61 add option to open containing folder 2019-11-09 11:28:12 +00:00
Zlatin Balevsky
419e9a0ce6 prevent npe when..? unclear when this happens 2019-11-09 11:01:55 +00:00
Zlatin Balevsky
ac1068a681 fix show comment/certificate buttons in group-by-file mode 2019-11-09 10:53:38 +00:00
Zlatin Balevsky
549457e36f close output stream silently 2019-11-08 21:46:44 +00:00
Zlatin Balevsky
14d6d10546 Release 0.5.10 2019-11-08 21:11:20 +00:00
Zlatin Balevsky
878e397aa0 preserve selections on update 2019-11-08 21:04:58 +00:00
Zlatin Balevsky
27831b488b add getter and use it; account for the case where a file has no certificates 2019-11-08 19:20:06 +00:00
Zlatin Balevsky
449f46c62b take list updating out of loop 2019-11-08 18:40:59 +00:00
Zlatin Balevsky
5703b85386 workaround? 2019-11-08 18:36:23 +00:00
Zlatin Balevsky
76d8d847bd wip on grouping by file 2019-11-08 18:15:54 +00:00
Zlatin Balevsky
db84d8e5bf wip on grouping by file 2019-11-08 17:33:41 +00:00
Zlatin Balevsky
cc9b384907 wip on grouping by file 2019-11-08 16:09:05 +00:00
Zlatin Balevsky
72960c24a8 implement trust reason in cli 2019-11-08 14:41:10 +00:00
Zlatin Balevsky
71298e5e73 proper rendering of date on subscriptions table 2019-11-08 08:31:00 +00:00
Zlatin Balevsky
11bc672544 say never if timestamp is 0 2019-11-08 08:30:44 +00:00
Zlatin Balevsky
2f6cd311a0 say never if timestamp is 0 2019-11-08 08:30:29 +00:00
Zlatin Balevsky
0448750491 lowercase for consistency 2019-11-08 08:18:33 +00:00
Zlatin Balevsky
800dd1cbba proper date sorting 2019-11-08 08:17:34 +00:00
Zlatin Balevsky
f95e9450f3 OutputStream.write 2019-11-08 07:47:11 +00:00
Zlatin Balevsky
d842e3f2f2 update for new object 2019-11-08 07:42:33 +00:00
Zlatin Balevsky
2017b53a43 pass comments on trust list subscriptions 2019-11-08 07:37:51 +00:00
Zlatin Balevsky
6e2b3f4f33 prompt for reason from review trust list view 2019-11-08 07:12:17 +00:00
Zlatin Balevsky
dbb305139b update for new type 2019-11-08 06:53:22 +00:00
Zlatin Balevsky
0801bfec08 add optinal reason for trusting/distrusting 2019-11-08 06:46:03 +00:00
Zlatin Balevsky
00a8d100fe show certificate comment form file details view 2019-11-08 04:51:37 +00:00
Zlatin Balevsky
e94b7cb0d4 prevent NPE when browsed from an older host 2019-11-08 04:02:11 +00:00
Zlatin Balevsky
b0357f2ecd update readme 2019-11-08 02:50:42 +00:00
Zlatin Balevsky
62e72a7ce0 Release 0.5.9 2019-11-07 20:01:15 +00:00
Zlatin Balevsky
26fa757b13 shared file details panel 2019-11-07 19:15:35 +00:00
Zlatin Balevsky
3b2e1cf98c make sure the persona reported by the browser matches 2019-11-07 18:35:34 +00:00
Zlatin Balevsky
5de8a51e47 account for unknown searchers 2019-11-07 18:34:11 +00:00
Zlatin Balevsky
f5c07f13c0 core side of searchers tracking 2019-11-07 18:31:20 +00:00
Zlatin Balevsky
c7b0ae34af associate persona with a search event, add skeleton for shared file panel 2019-11-07 17:43:37 +00:00
Zlatin Balevsky
cad5301827 rewrite Persona and Name in java 2019-11-07 17:41:32 +00:00
Zlatin Balevsky
c998011873 add right-click and show-in-library option for uploads 2019-11-07 05:02:53 +00:00
Zlatin Balevsky
5802ba7734 show trust status of certificate issuers in cli as well 2019-11-06 18:19:45 +00:00
Zlatin Balevsky
b3f775f59a show trust status in certificates view 2019-11-06 18:13:07 +00:00
Zlatin Balevsky
739dbc7a24 fix serialization of older certificates 2019-11-06 18:09:50 +00:00
Zlatin Balevsky
af99dee4a3 wip on view certificate comments in cli 2019-11-06 17:08:48 +00:00
Zlatin Balevsky
07a6c63357 wip on view certificate comments in cli 2019-11-06 16:58:22 +00:00
Zlatin Balevsky
c4096568f5 initialize group properly 2019-11-06 16:01:43 +00:00
Zlatin Balevsky
30dda180eb Add support for comments in certificates, bump certificate version 2019-11-06 15:32:39 +00:00
Zlatin Balevsky
83ea1bed3e add timestamp to the filename of the certificate 2019-11-06 14:05:17 +00:00
Zlatin Balevsky
9181829e4a split by newlines 2019-11-06 13:59:14 +00:00
Zlatin Balevsky
94678bad3c Release 0.5.8 2019-11-06 05:46:52 +00:00
Zlatin Balevsky
e7072803e9 Merge branch 'master' of https://github.com/zlatinb/muwire 2019-11-06 05:42:14 +00:00
Zlatin Balevsky
e9f7a51e16 Always share update files; disable forced update check on startup 2019-11-06 05:41:58 +00:00
Zlatin Balevsky
916fad7d9b more fake padding 2019-11-05 15:54:16 +00:00
Zlatin Balevsky
9feb891c51 support phrases in search 2019-11-05 15:52:23 +00:00
Zlatin Balevsky
b865376d24 more tests 2019-11-05 14:41:27 +00:00
Zlatin Balevsky
8dcba7535c modify indexing and search logic to account for phrases 2019-11-05 13:24:22 +00:00
Zlatin Balevsky
7e881f1fe6 close() output streams on rejection, update test 2019-11-05 12:57:52 +00:00
Zlatin Balevsky
a9aad7d9db test with deleted files 2019-11-05 12:57:16 +00:00
Zlatin Balevsky
e736b42751 view certificates in cli 2019-11-05 05:51:43 +00:00
Zlatin Balevsky
acda64aea7 Add certify button to cli. Make watched directory handling match that of gui 2019-11-05 04:41:25 +00:00
Zlatin Balevsky
d82dc4ce90 Certificates viewer 2019-11-04 21:34:21 +00:00
Zlatin Balevsky
f2ff90795d show a warning when user tries to certify 2019-11-04 20:49:46 +00:00
Zlatin Balevsky
49f51a9f5f view certificates from browse host 2019-11-04 19:39:04 +00:00
Zlatin Balevsky
6fbd1267fa make sure the View Certificates button appears at default size 2019-11-04 19:27:44 +00:00
Zlatin Balevsky
149568520f register necessary event, initialize mvc group, correct name representation 2019-11-04 19:05:53 +00:00
Zlatin Balevsky
c672880db0 statement was in wrong place 2019-11-04 18:45:57 +00:00
Zlatin Balevsky
6cb1674d14 set row height for tables pt2 2019-11-04 18:36:18 +00:00
Zlatin Balevsky
dba863a864 hook up CertClient, check that infohash in cert matches 2019-11-04 18:33:57 +00:00
Zlatin Balevsky
642044b7e2 ui elements for certificate fetching 2019-11-04 18:33:25 +00:00
Zlatin Balevsky
47c14f109a rename column, show certificate count in results 2019-11-04 17:21:37 +00:00
Zlatin Balevsky
36c1a1a288 core side of certificate exchange 2019-11-04 17:17:57 +00:00
Zlatin Balevsky
5d51b1c580 ability to certify shared files 2019-11-04 15:22:24 +00:00
Zlatin Balevsky
bf3502220f sign update queries as well 2019-11-03 22:44:42 +00:00
100 changed files with 3290 additions and 534 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. It is inspired by the LimeWire Gnutella client and developped by a former LimeWire developer.
The current stable release - 0.5.5 is avaiable for download at https://muwire.com. You can find technical documentation in the "doc" folder. The current stable release - 0.6.0 is avaiable for download at https://muwire.com. You can find technical documentation in the "doc" folder.
### Building ### Building

View File

@@ -17,7 +17,7 @@ class BrowseModel {
private final Persona persona private final Persona persona
private final Core core private final Core core
private final TextGUIThread guiThread 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 Map<String, UIResultEvent> rootToResult = new HashMap<>()
private int totalResults private int totalResults
@@ -53,7 +53,7 @@ class BrowseModel {
String size = DataHelper.formatSize2Decimal(e.size, false) + "B" String size = DataHelper.formatSize2Decimal(e.size, false) + "B"
String infoHash = Base64.encode(e.infohash.getRoot()) String infoHash = Base64.encode(e.infohash.getRoot())
String comment = String.valueOf(e.comment != null) 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) rootToResult.put(infoHash, e)
String percentageString = "" String percentageString = ""

View File

@@ -49,7 +49,7 @@ class BrowseView extends BasicWindow {
topPanel.addComponent(percentageLabel, layoutData) topPanel.addComponent(percentageLabel, layoutData)
contentPanel.addComponent(topPanel, layoutData) contentPanel.addComponent(topPanel, layoutData)
table = new Table("Name","Size","Hash","Comment") table = new Table("Name","Size","Hash","Comment","Certificates")
table.with { table.with {
setCellSelection(false) setCellSelection(false)
setTableModel(model.model) setTableModel(model.model)
@@ -71,19 +71,24 @@ class BrowseView extends BasicWindow {
int selectedRow = table.getSelectedRow() int selectedRow = table.getSelectedRow()
def row = model.model.getRow(selectedRow) def row = model.model.getRow(selectedRow)
String infoHash = row[2] String infoHash = row[2]
boolean comment = Boolean.parseBoolean(row[3]) boolean comment = Boolean.parseBoolean(row[3])
if (comment) { boolean certificates = row[4] > 0
if (comment || certificates) {
Window prompt = new BasicWindow("Download Or View Comment") Window prompt = new BasicWindow("Download Or View Comment")
prompt.setHints([Window.Hint.CENTERED]) prompt.setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel() Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(3)) contentPanel.setLayoutManager(new GridLayout(4))
Button downloadButton = new Button("Download", {download(infoHash)}) Button downloadButton = new Button("Download", {download(infoHash)})
Button viewButton = new Button("View Comment", {viewComment(infoHash)}) Button viewButton = new Button("View Comment", {viewComment(infoHash)})
Button viewCertificate = new Button("View Certificates",{viewCertificates(infoHash)})
Button closeButton = new Button("Cancel", {prompt.close()}) Button closeButton = new Button("Cancel", {prompt.close()})
contentPanel.with { contentPanel.with {
addComponent(downloadButton, layoutData) addComponent(downloadButton, layoutData)
addComponent(viewButton, layoutData) if (comment)
addComponent(viewButton, layoutData)
if (certificates)
addComponent(viewCertificate, layoutData)
addComponent(closeButton, layoutData) addComponent(closeButton, layoutData)
} }
@@ -105,7 +110,14 @@ class BrowseView extends BasicWindow {
private void viewComment(String infoHash) { private void viewComment(String infoHash) {
UIResultEvent result = model.rootToResult[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) 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 import com.muwire.core.files.AllFilesLoadedEvent
class CliLanterna { class CliLanterna {
private static final String MW_VERSION = "0.5.7" private static final String MW_VERSION = "0.6.2"
private static volatile Core core private static volatile Core core

View File

@@ -18,7 +18,7 @@ class FilesModel {
private final TextGUIThread guiThread private final TextGUIThread guiThread
private final Core core private final Core core
private final List<SharedFile> sharedFiles = new ArrayList<>() 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) { FilesModel(TextGUIThread guiThread, Core core) {
this.guiThread = guiThread this.guiThread = guiThread
@@ -41,7 +41,7 @@ class FilesModel {
def eventBus = core.eventBus def eventBus = core.eventBus
guiThread.invokeLater { guiThread.invokeLater {
core.muOptions.watchedDirectories.each { 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 { sharedFiles.each {
long size = it.getCachedLength() long size = it.getCachedLength()
boolean comment = it.comment != null boolean comment = it.comment != null
boolean certified = core.certificateManager.hasLocalCertificate(it.getInfoHash())
String hits = String.valueOf(it.getHits()) String hits = String.valueOf(it.getHits())
String downloaders = String.valueOf(it.getDownloaders().size()) 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.googlecode.lanterna.gui2.table.Table
import com.muwire.core.Core import com.muwire.core.Core
import com.muwire.core.SharedFile import com.muwire.core.SharedFile
import com.muwire.core.filecert.UICreateCertificateEvent
import com.muwire.core.files.DirectoryUnsharedEvent import com.muwire.core.files.DirectoryUnsharedEvent
import com.muwire.core.files.FileSharedEvent import com.muwire.core.files.FileSharedEvent
import com.muwire.core.files.FileUnsharedEvent import com.muwire.core.files.FileUnsharedEvent
@@ -42,7 +43,7 @@ class FilesView extends BasicWindow {
contentPanel.setLayoutManager(new GridLayout(1)) contentPanel.setLayoutManager(new GridLayout(1))
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false) 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.setCellSelection(false)
table.setTableModel(model.model) table.setTableModel(model.model)
table.setSelectAction({rowSelected()}) table.setSelectAction({rowSelected()})
@@ -77,7 +78,7 @@ class FilesView extends BasicWindow {
Window prompt = new BasicWindow("Unshare or add comment to "+sf.getFile().getName()+" ?") Window prompt = new BasicWindow("Unshare or add comment to "+sf.getFile().getName()+" ?")
prompt.setHints([Window.Hint.CENTERED]) prompt.setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel() Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(3)) contentPanel.setLayoutManager(new GridLayout(4))
Button unshareButton = new Button("Unshare", { Button unshareButton = new Button("Unshare", {
core.eventBus.publish(new FileUnsharedEvent(unsharedFile : sf)) core.eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
@@ -88,11 +89,16 @@ class FilesView extends BasicWindow {
AddCommentView view = new AddCommentView(textGUI, core, sf, terminalSize) AddCommentView view = new AddCommentView(textGUI, core, sf, terminalSize)
textGUI.addWindowAndWait(view) 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()}) Button closeButton = new Button("Close", {prompt.close()})
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER) LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
contentPanel.addComponent(unshareButton, layoutData) contentPanel.addComponent(unshareButton, layoutData)
contentPanel.addComponent(addCommentButton, layoutData) contentPanel.addComponent(addCommentButton, layoutData)
contentPanel.addComponent(certifyButton, layoutData)
contentPanel.addComponent(closeButton, layoutData) contentPanel.addComponent(closeButton, layoutData)
prompt.setComponent(contentPanel) prompt.setComponent(contentPanel)

View File

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

View File

@@ -37,7 +37,7 @@ class ResultsView extends BasicWindow {
Panel contentPanel = new Panel() Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(1)) 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.setCellSelection(false)
table.setSelectAction({rowSelected()}) table.setSelectAction({rowSelected()})
table.setTableModel(model.model) table.setTableModel(model.model)
@@ -55,18 +55,29 @@ class ResultsView extends BasicWindow {
int selectedRow = table.getSelectedRow() int selectedRow = table.getSelectedRow()
def rows = model.model.getRow(selectedRow) def rows = model.model.getRow(selectedRow)
boolean comment = Boolean.parseBoolean(rows[4]) boolean comment = Boolean.parseBoolean(rows[4])
if (comment) { boolean certificates = rows[5] > 0
Window prompt = new BasicWindow("Download Or View Comment") 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]) prompt.setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel() Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(3)) contentPanel.setLayoutManager(new GridLayout(4))
Button downloadButton = new Button("Download", {download(rows[2])}) 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()}) 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) contentPanel.addComponent(closeButton, layoutData)
prompt.setComponent(contentPanel) prompt.setComponent(contentPanel)
downloadButton.takeFocus() downloadButton.takeFocus()
@@ -88,7 +99,14 @@ class ResultsView extends BasicWindow {
private void viewComment(String infohash) { private void viewComment(String infohash) {
UIResultEvent result = model.rootToResult[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) textGUI.addWindowAndWait(view)
} }
} }

View File

@@ -7,6 +7,7 @@ import com.muwire.core.search.QueryEvent
import com.muwire.core.search.SearchEvent import com.muwire.core.search.SearchEvent
import com.muwire.core.search.UIResultBatchEvent import com.muwire.core.search.UIResultBatchEvent
import com.muwire.core.search.UIResultEvent import com.muwire.core.search.UIResultEvent
import com.muwire.core.util.DataUtil
import net.i2p.crypto.DSAEngine import net.i2p.crypto.DSAEngine
import net.i2p.data.Base64 import net.i2p.data.Base64
@@ -45,16 +46,16 @@ class SearchModel {
def searchEvent def searchEvent
byte [] payload byte [] payload
UUID uuid = UUID.randomUUID()
long timestamp = System.currentTimeMillis()
byte [] sig2 = DataUtil.signUUID(uuid, timestamp, core.spk)
if (hashSearch) { 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 payload = root
} else { } else {
def replaced = query.toLowerCase().trim().replaceAll(SplitPattern.SPLIT_PATTERN, " ") def nonEmpty = SplitPattern.termify(query)
def terms = replaced.split(" ")
def nonEmpty = []
terms.each { if (it.length() > 0) nonEmpty << it }
payload = String.join(" ", nonEmpty).getBytes(StandardCharsets.UTF_8) payload = String.join(" ", nonEmpty).getBytes(StandardCharsets.UTF_8)
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : UUID.randomUUID(), oobInfohash: true, searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : uuid, oobInfohash: true,
searchComments : core.muOptions.searchComments, compressedResults : true) searchComments : core.muOptions.searchComments, compressedResults : true)
} }
@@ -64,7 +65,7 @@ class SearchModel {
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : firstHop, core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : firstHop,
replyTo: core.me.destination, receivedOn: core.me.destination, replyTo: core.me.destination, receivedOn: core.me.destination,
originator : core.me, sig: sig.data)) originator : core.me, sig: sig.data, queryTime : timestamp, sig2 : sig2))
} }
void unregister() { 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.trustList = trustList
this.core = core this.core = core
trustedTableModel = new TableModel("Trusted User","Your Trust") trustedTableModel = new TableModel("Trusted User","Reason","Your Trust")
distrustedTableModel = new TableModel("Distrusted User", "Your Trust") distrustedTableModel = new TableModel("Distrusted User", "Reason", "Your Trust")
refreshModels() refreshModels()
core.eventBus.register(TrustEvent.class, this) core.eventBus.register(TrustEvent.class, this)
@@ -36,10 +36,10 @@ class TrustListModel {
distrustRows.times { distrustedTableModel.removeRow(0) } distrustRows.times { distrustedTableModel.removeRow(0) }
trustList.good.each { 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 { 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.Window
import com.googlecode.lanterna.gui2.dialogs.MessageDialog import com.googlecode.lanterna.gui2.dialogs.MessageDialog
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
import com.googlecode.lanterna.gui2.dialogs.TextInputDialog
import com.googlecode.lanterna.gui2.table.Table import com.googlecode.lanterna.gui2.table.Table
import com.muwire.core.Core import com.muwire.core.Core
import com.muwire.core.Persona import com.muwire.core.Persona
@@ -48,7 +49,7 @@ class TrustListView extends BasicWindow {
Panel topPanel = new Panel() Panel topPanel = new Panel()
topPanel.setLayoutManager(new GridLayout(2)) topPanel.setLayoutManager(new GridLayout(2))
trusted = new Table("Trusted User","Your Trust") trusted = new Table("Trusted User","Reason","Your Trust")
trusted.with { trusted.with {
setCellSelection(false) setCellSelection(false)
setTableModel(model.trustedTableModel) setTableModel(model.trustedTableModel)
@@ -57,7 +58,7 @@ class TrustListView extends BasicWindow {
trusted.setSelectAction({ actionsForUser(true) }) trusted.setSelectAction({ actionsForUser(true) })
topPanel.addComponent(trusted, layoutData) topPanel.addComponent(trusted, layoutData)
distrusted = new Table("Distrusted User", "Your Trust") distrusted = new Table("Distrusted User","Reason", "Your Trust")
distrusted.with { distrusted.with {
setCellSelection(false) setCellSelection(false)
setTableModel(model.distrustedTableModel) setTableModel(model.distrustedTableModel)
@@ -90,7 +91,8 @@ class TrustListView extends BasicWindow {
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER) LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
Button trustButton = new Button("Trust",{ 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", MessageDialog.showMessageDialog(textGUI, "Marked Trusted", persona.getHumanReadableName() + "has been marked trusted",
MessageDialogButton.OK) MessageDialogButton.OK)
}) })
@@ -100,7 +102,8 @@ class TrustListView extends BasicWindow {
MessageDialogButton.OK) MessageDialogButton.OK)
}) })
Button distrustButton = new Button("Distrust",{ 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", MessageDialog.showMessageDialog(textGUI, "Marked Distrusted", persona.getHumanReadableName() + "has been marked distrusted",
MessageDialogButton.OK) MessageDialogButton.OK)
}) })

View File

@@ -17,8 +17,8 @@ class TrustModel {
this.guiThread = guiThread this.guiThread = guiThread
this.core = core this.core = core
modelTrusted = new TableModel("Trusted Users") modelTrusted = new TableModel("Trusted Users","Reason")
modelDistrusted = new TableModel("Distrusted Users") modelDistrusted = new TableModel("Distrusted Users","Reason")
modelSubscriptions = new TableModel("Name","Trusted","Distrusted","Status","Last Updated") modelSubscriptions = new TableModel("Name","Trusted","Distrusted","Status","Last Updated")
core.eventBus.register(TrustEvent.class, this) core.eventBus.register(TrustEvent.class, this)
@@ -57,11 +57,11 @@ class TrustModel {
subsRows.times { modelSubscriptions.removeRow(0) } subsRows.times { modelSubscriptions.removeRow(0) }
core.trustService.good.values().each { core.trustService.good.values().each {
modelTrusted.addRow(new PersonaWrapper(it)) modelTrusted.addRow(new PersonaWrapper(it.persona),it.reason)
} }
core.trustService.bad.values().each { core.trustService.bad.values().each {
modelDistrusted.addRow(new PersonaWrapper(it)) modelDistrusted.addRow(new PersonaWrapper(it.persona),it.reason)
} }
core.trustSubscriber.remoteTrustLists.values().each { 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.MessageDialog
import com.googlecode.lanterna.gui2.dialogs.MessageDialogBuilder import com.googlecode.lanterna.gui2.dialogs.MessageDialogBuilder
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton 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.GridLayout.Alignment
import com.googlecode.lanterna.gui2.Label import com.googlecode.lanterna.gui2.Label
import com.googlecode.lanterna.gui2.table.Table import com.googlecode.lanterna.gui2.table.Table
@@ -44,14 +45,14 @@ class TrustView extends BasicWindow {
Panel topPanel = new Panel() Panel topPanel = new Panel()
topPanel.setLayoutManager(new GridLayout(2)) topPanel.setLayoutManager(new GridLayout(2))
trusted = new Table("Trusted Users") trusted = new Table("Trusted Users","Reason")
trusted.setCellSelection(false) trusted.setCellSelection(false)
trusted.setSelectAction({trustedActions()}) trusted.setSelectAction({trustedActions()})
trusted.setTableModel(model.modelTrusted) trusted.setTableModel(model.modelTrusted)
trusted.setVisibleRows(tableSize) trusted.setVisibleRows(tableSize)
topPanel.addComponent(trusted, layoutData) topPanel.addComponent(trusted, layoutData)
distrusted = new Table("Distrusted users") distrusted = new Table("Distrusted users","Reason")
distrusted.setCellSelection(false) distrusted.setCellSelection(false)
distrusted.setSelectAction({distrustedActions()}) distrusted.setSelectAction({distrustedActions()})
distrusted.setTableModel(model.modelDistrusted) distrusted.setTableModel(model.modelDistrusted)
@@ -106,7 +107,8 @@ class TrustView extends BasicWindow {
MessageDialogButton.OK) MessageDialogButton.OK)
}) })
Button markDistrusted = new Button("Mark Distrusted", { 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", MessageDialog.showMessageDialog(textGUI, "Marked Distrusted", persona.getHumanReadableName() + "has been marked distrusted",
MessageDialogButton.OK) MessageDialogButton.OK)
}) })
@@ -122,7 +124,7 @@ class TrustView extends BasicWindow {
} }
private void distrustedActions() { private void distrustedActions() {
int selectedRow = trusted.getSelectedRow() int selectedRow = distrusted.getSelectedRow()
def row = model.modelDistrusted.getRow(selectedRow) def row = model.modelDistrusted.getRow(selectedRow)
Persona persona = row[0].persona Persona persona = row[0].persona
@@ -138,7 +140,8 @@ class TrustView extends BasicWindow {
MessageDialogButton.OK) MessageDialogButton.OK)
}) })
Button markDistrusted = new Button("Mark Trusted", { 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", MessageDialog.showMessageDialog(textGUI, "Marked Trusted", persona.getHumanReadableName() + "has been marked trusted",
MessageDialogButton.OK) MessageDialogButton.OK)
}) })

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

View File

@@ -18,6 +18,11 @@ import com.muwire.core.download.UIDownloadCancelledEvent
import com.muwire.core.download.UIDownloadEvent import com.muwire.core.download.UIDownloadEvent
import com.muwire.core.download.UIDownloadPausedEvent import com.muwire.core.download.UIDownloadPausedEvent
import com.muwire.core.download.UIDownloadResumedEvent 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.FileDownloadedEvent
import com.muwire.core.files.FileHashedEvent import com.muwire.core.files.FileHashedEvent
import com.muwire.core.files.FileHashingEvent import com.muwire.core.files.FileHashingEvent
@@ -99,6 +104,7 @@ public class Core {
final FileManager fileManager final FileManager fileManager
final UploadManager uploadManager final UploadManager uploadManager
final ContentManager contentManager final ContentManager contentManager
final CertificateManager certificateManager
private final Router router private final Router router
@@ -209,6 +215,12 @@ public class Core {
eventBus = new EventBus() 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") log.info("initializing trust service")
File goodTrust = new File(home, "trusted") File goodTrust = new File(home, "trusted")
File badTrust = new File(home, "distrusted") File badTrust = new File(home, "distrusted")
@@ -255,15 +267,19 @@ public class Core {
cacheClient = new CacheClient(eventBus,hostCache, connectionManager, i2pSession, props, 10000) cacheClient = new CacheClient(eventBus,hostCache, connectionManager, i2pSession, props, 10000)
log.info("initializing update client") 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(FileDownloadedEvent.class, updateClient)
eventBus.register(UIResultBatchEvent.class, updateClient) eventBus.register(UIResultBatchEvent.class, updateClient)
log.info("initializing connector") log.info("initializing connector")
I2PConnector i2pConnector = new I2PConnector(socketManager) I2PConnector i2pConnector = new I2PConnector(socketManager)
log.info("initializing certificate client")
CertificateClient certificateClient = new CertificateClient(eventBus, i2pConnector)
eventBus.register(UIFetchCertificatesEvent.class, certificateClient)
log.info "initializing results sender" log.info "initializing results sender"
ResultsSender resultsSender = new ResultsSender(eventBus, i2pConnector, me, props) ResultsSender resultsSender = new ResultsSender(eventBus, i2pConnector, me, props, certificateManager)
log.info "initializing search manager" log.info "initializing search manager"
SearchManager searchManager = new SearchManager(eventBus, me, resultsSender) SearchManager searchManager = new SearchManager(eventBus, me, resultsSender)
@@ -289,7 +305,8 @@ public class Core {
log.info("initializing acceptor") log.info("initializing acceptor")
I2PAcceptor i2pAcceptor = new I2PAcceptor(socketManager) I2PAcceptor i2pAcceptor = new I2PAcceptor(socketManager)
connectionAcceptor = new ConnectionAcceptor(eventBus, connectionManager, props, connectionAcceptor = new ConnectionAcceptor(eventBus, connectionManager, props,
i2pAcceptor, hostCache, trustService, searchManager, uploadManager, fileManager, connectionEstablisher) i2pAcceptor, hostCache, trustService, searchManager, uploadManager, fileManager, connectionEstablisher,
certificateManager)
log.info("initializing directory watcher") log.info("initializing directory watcher")
directoryWatcher = new DirectoryWatcher(eventBus, fileManager, home, props) directoryWatcher = new DirectoryWatcher(eventBus, fileManager, home, props)
@@ -314,7 +331,7 @@ public class Core {
eventBus.register(QueryEvent.class, contentManager) eventBus.register(QueryEvent.class, contentManager)
log.info("initializing browse manager") log.info("initializing browse manager")
BrowseManager browseManager = new BrowseManager(i2pConnector, eventBus) BrowseManager browseManager = new BrowseManager(i2pConnector, eventBus, me)
eventBus.register(UIBrowseEvent.class, browseManager) eventBus.register(UIBrowseEvent.class, browseManager)
} }
@@ -389,7 +406,7 @@ public class Core {
} }
} }
Core core = new Core(props, home, "0.5.7") Core core = new Core(props, home, "0.6.2")
core.startServices() core.startServices()
// ... at the end, sleep or execute script // ... at the end, sleep or execute script

View File

@@ -20,6 +20,7 @@ class MuWireSettings {
int totalUploadSlots int totalUploadSlots
int uploadSlotsPerUser int uploadSlotsPerUser
int updateCheckInterval int updateCheckInterval
long lastUpdateCheck
boolean autoDownloadUpdate boolean autoDownloadUpdate
String updateType String updateType
String nickname String nickname
@@ -60,6 +61,7 @@ class MuWireSettings {
incompleteLocation = new File(incompleteLocationProp) incompleteLocation = new File(incompleteLocationProp)
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","60")) downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","60"))
updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","24")) updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","24"))
lastUpdateCheck = Long.parseLong(props.getProperty("lastUpdateChec","0"))
autoDownloadUpdate = Boolean.parseBoolean(props.getProperty("autoDownloadUpdate","true")) autoDownloadUpdate = Boolean.parseBoolean(props.getProperty("autoDownloadUpdate","true"))
updateType = props.getProperty("updateType","jar") updateType = props.getProperty("updateType","jar")
shareDownloadedFiles = Boolean.parseBoolean(props.getProperty("shareDownloadedFiles","true")) shareDownloadedFiles = Boolean.parseBoolean(props.getProperty("shareDownloadedFiles","true"))
@@ -107,6 +109,7 @@ class MuWireSettings {
props.setProperty("incompleteLocation", incompleteLocation.getAbsolutePath()) props.setProperty("incompleteLocation", incompleteLocation.getAbsolutePath())
props.setProperty("downloadRetryInterval", String.valueOf(downloadRetryInterval)) props.setProperty("downloadRetryInterval", String.valueOf(downloadRetryInterval))
props.setProperty("updateCheckInterval", String.valueOf(updateCheckInterval)) props.setProperty("updateCheckInterval", String.valueOf(updateCheckInterval))
props.setProperty("lastUpdateCheck", String.valueOf(lastUpdateCheck))
props.setProperty("autoDownloadUpdate", String.valueOf(autoDownloadUpdate)) props.setProperty("autoDownloadUpdate", String.valueOf(autoDownloadUpdate))
props.setProperty("updateType",String.valueOf(updateType)) props.setProperty("updateType",String.valueOf(updateType))
props.setProperty("shareDownloadedFiles", String.valueOf(shareDownloadedFiles)) props.setProperty("shareDownloadedFiles", String.valueOf(shareDownloadedFiles))

View File

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

View File

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

View File

@@ -2,6 +2,90 @@ package com.muwire.core
class SplitPattern { 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

@@ -153,6 +153,10 @@ abstract class Connection implements Closeable {
query.originator = e.originator.toBase64() query.originator = e.originator.toBase64()
if (e.sig != null) if (e.sig != null)
query.sig = Base64.encode(e.sig) 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) messages.put(query)
} }
@@ -232,7 +236,6 @@ abstract class Connection implements Closeable {
if (search.compressedResults != null) if (search.compressedResults != null)
compressedResults = search.compressedResults compressedResults = search.compressedResults
byte[] sig = null byte[] sig = null
// TODO: make this mandatory at some point
if (search.sig != null) { if (search.sig != null) {
sig = Base64.decode(search.sig) sig = Base64.decode(search.sig)
byte [] payload byte [] payload
@@ -247,21 +250,52 @@ abstract class Connection implements Closeable {
return return
} else } else
log.info("query signature verified") log.info("query signature verified")
} else } else {
log.info("no signature in query") 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, SearchEvent searchEvent = new SearchEvent(searchTerms : search.keywords,
searchHash : infohash, searchHash : infohash,
uuid : uuid, uuid : uuid,
oobInfohash : oob, oobInfohash : oob,
searchComments : searchComments, searchComments : searchComments,
compressedResults : compressedResults) compressedResults : compressedResults,
persona : originator)
QueryEvent event = new QueryEvent ( searchEvent : searchEvent, QueryEvent event = new QueryEvent ( searchEvent : searchEvent,
replyTo : replyTo, replyTo : replyTo,
originator : originator, originator : originator,
receivedOn : endpoint.destination, receivedOn : endpoint.destination,
firstHop : search.firstHop, firstHop : search.firstHop,
sig : sig ) sig : sig,
queryTime : queryTime,
sig2 : sig2 )
eventBus.publish(event) eventBus.publish(event)
} }

View File

@@ -1,6 +1,7 @@
package com.muwire.core.connection package com.muwire.core.connection
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.nio.file.attribute.DosFileAttributes
import java.util.concurrent.ExecutorService import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.logging.Level import java.util.logging.Level
@@ -11,8 +12,11 @@ import java.util.zip.InflaterInputStream
import com.muwire.core.Constants import com.muwire.core.Constants
import com.muwire.core.EventBus import com.muwire.core.EventBus
import com.muwire.core.InfoHash
import com.muwire.core.MuWireSettings import com.muwire.core.MuWireSettings
import com.muwire.core.Persona import com.muwire.core.Persona
import com.muwire.core.filecert.Certificate
import com.muwire.core.filecert.CertificateManager
import com.muwire.core.files.FileManager import com.muwire.core.files.FileManager
import com.muwire.core.hostcache.HostCache import com.muwire.core.hostcache.HostCache
import com.muwire.core.trust.TrustLevel import com.muwire.core.trust.TrustLevel
@@ -45,6 +49,7 @@ class ConnectionAcceptor {
final UploadManager uploadManager final UploadManager uploadManager
final FileManager fileManager final FileManager fileManager
final ConnectionEstablisher establisher final ConnectionEstablisher establisher
final CertificateManager certificateManager
final ExecutorService acceptorThread final ExecutorService acceptorThread
final ExecutorService handshakerThreads final ExecutorService handshakerThreads
@@ -56,7 +61,7 @@ class ConnectionAcceptor {
ConnectionAcceptor(EventBus eventBus, UltrapeerConnectionManager manager, ConnectionAcceptor(EventBus eventBus, UltrapeerConnectionManager manager,
MuWireSettings settings, I2PAcceptor acceptor, HostCache hostCache, MuWireSettings settings, I2PAcceptor acceptor, HostCache hostCache,
TrustService trustService, SearchManager searchManager, UploadManager uploadManager, TrustService trustService, SearchManager searchManager, UploadManager uploadManager,
FileManager fileManager, ConnectionEstablisher establisher) { FileManager fileManager, ConnectionEstablisher establisher, CertificateManager certificateManager) {
this.eventBus = eventBus this.eventBus = eventBus
this.manager = manager this.manager = manager
this.settings = settings this.settings = settings
@@ -67,6 +72,7 @@ class ConnectionAcceptor {
this.fileManager = fileManager this.fileManager = fileManager
this.uploadManager = uploadManager this.uploadManager = uploadManager
this.establisher = establisher this.establisher = establisher
this.certificateManager = certificateManager
acceptorThread = Executors.newSingleThreadExecutor { r -> acceptorThread = Executors.newSingleThreadExecutor { r ->
def rv = new Thread(r) def rv = new Thread(r)
@@ -145,11 +151,17 @@ class ConnectionAcceptor {
case (byte)'B': case (byte)'B':
processBROWSE(e) processBROWSE(e)
break break
case (byte)'C':
processCERTIFICATES(e)
break
default: default:
throw new Exception("Invalid read $read") throw new Exception("Invalid read $read")
} }
} catch (Exception ex) { } catch (Exception ex) {
log.log(Level.WARNING, "incoming connection failed",ex) log.log(Level.WARNING, "incoming connection failed",ex)
try {
e.getOutputStream().close()
} catch (Exception ignore) {}
e.close() e.close()
eventBus.publish new ConnectionEvent(endpoint: e, incoming: true, leaf: null, status: ConnectionAttemptStatus.FAILED) eventBus.publish new ConnectionEvent(endpoint: e, incoming: true, leaf: null, status: ConnectionAttemptStatus.FAILED)
} }
@@ -198,7 +210,9 @@ class ConnectionAcceptor {
os.writeShort(json.bytes.length) os.writeShort(json.bytes.length)
os.write(json.bytes) os.write(json.bytes)
} }
e.outputStream.flush() try {
e.outputStream.close()
} catch (Exception ignored) {}
e.close() e.close()
eventBus.publish(new ConnectionEvent(endpoint: e, incoming: true, leaf: leaf, status: ConnectionAttemptStatus.REJECTED)) eventBus.publish(new ConnectionEvent(endpoint: e, incoming: true, leaf: leaf, status: ConnectionAttemptStatus.REJECTED))
} }
@@ -278,18 +292,8 @@ class ConnectionAcceptor {
if (!searchManager.hasLocalSearch(resultsUUID)) if (!searchManager.hasLocalSearch(resultsUUID))
throw new UnexpectedResultsException(resultsUUID.toString()) throw new UnexpectedResultsException(resultsUUID.toString())
// parse all headers // parse all headers
Map<String,String> headers = new HashMap<>() Map<String,String> headers = DataUtil.readAllHeaders(is);
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("Sender")) if (!headers.containsKey("Sender"))
throw new IOException("No Sender header") throw new IOException("No Sender header")
@@ -330,8 +334,14 @@ class ConnectionAcceptor {
dis.readFully(rowse) dis.readFully(rowse)
if (rowse != "ROWSE\r\n".getBytes(StandardCharsets.US_ASCII)) if (rowse != "ROWSE\r\n".getBytes(StandardCharsets.US_ASCII))
throw new IOException("Invalid BROWSE connection") 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() OutputStream os = e.getOutputStream()
if (!settings.browseFiles) { if (!settings.browseFiles) {
@@ -352,8 +362,9 @@ class ConnectionAcceptor {
DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os)) DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))
JsonOutput jsonOutput = new JsonOutput() JsonOutput jsonOutput = new JsonOutput()
sharedFiles.each { sharedFiles.each {
it.hit() it.hit(browser, System.currentTimeMillis(), "Browse Host");
def obj = ResultsSender.sharedFileToObj(it, false) int certificates = certificateManager.getByInfoHash(it.getInfoHash()).size()
def obj = ResultsSender.sharedFileToObj(it, false, certificates)
def json = jsonOutput.toJson(obj) def json = jsonOutput.toJson(obj)
dos.writeShort((short)json.length()) dos.writeShort((short)json.length())
dos.write(json.getBytes(StandardCharsets.US_ASCII)) dos.write(json.getBytes(StandardCharsets.US_ASCII))
@@ -372,10 +383,10 @@ class ConnectionAcceptor {
dis.readFully(RUST) dis.readFully(RUST)
if (RUST != "RUST\r\n".getBytes(StandardCharsets.US_ASCII)) if (RUST != "RUST\r\n".getBytes(StandardCharsets.US_ASCII))
throw new IOException("Invalid TRUST connection") throw new IOException("Invalid TRUST connection")
String header
while ((header = DataUtil.readTillRN(dis)) != ""); // ignore headers for now Map<String,String> headers = DataUtil.readAllHeaders(dis)
OutputStream os = e.getOutputStream() OutputStream os = e.getOutputStream()
if (!settings.allowTrustLists) { if (!settings.allowTrustLists) {
os.write("403 Not Allowed\r\n\r\n".getBytes(StandardCharsets.US_ASCII)) os.write("403 Not Allowed\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
os.flush() os.flush()
@@ -383,22 +394,53 @@ class ConnectionAcceptor {
return return
} }
os.write("200 OK\r\n\r\n".getBytes(StandardCharsets.US_ASCII)) os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
List<Persona> good = new ArrayList<>(trustService.good.values())
int size = Math.min(Short.MAX_VALUE * 2, good.size()) boolean json = headers.containsKey('Json') && Boolean.parseBoolean(headers['Json'])
good = good.subList(0, size)
List<TrustService.TrustEntry> good = new ArrayList<>(trustService.good.values())
List<TrustService.TrustEntry> bad = new ArrayList<>(trustService.bad.values())
DataOutputStream dos = new DataOutputStream(os) DataOutputStream dos = new DataOutputStream(os)
dos.writeShort(size)
good.each {
it.write(dos)
}
List<Persona> bad = new ArrayList<>(trustService.bad.values()) if (!json) {
size = Math.min(Short.MAX_VALUE * 2, bad.size()) os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
bad = bad.subList(0, size) int size = Math.min(Short.MAX_VALUE * 2, good.size())
dos.writeShort(size) good = good.subList(0, size)
bad.each { dos.writeShort(size)
it.write(dos) 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() dos.flush()
@@ -406,5 +448,55 @@ class ConnectionAcceptor {
e.close() 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()
}
}
} }

View File

@@ -276,6 +276,57 @@ public class Downloader {
activeWorkers.put(d, newWorker) activeWorkers.put(d, newWorker)
executorService.submit(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 { class DownloadWorker implements Runnable {
private final Destination destination private final Destination destination

View File

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

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

View File

@@ -12,6 +12,7 @@ import java.util.stream.Collectors
import com.muwire.core.DownloadedFile import com.muwire.core.DownloadedFile
import com.muwire.core.EventBus import com.muwire.core.EventBus
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
import com.muwire.core.Persona
import com.muwire.core.Service import com.muwire.core.Service
import com.muwire.core.SharedFile import com.muwire.core.SharedFile
import com.muwire.core.UILoadedEvent import com.muwire.core.UILoadedEvent
@@ -129,15 +130,21 @@ class PersisterService extends Service {
return new FileLoadedEvent(loadedFile : df) return new FileLoadedEvent(loadedFile : df)
} }
int hits = 0
if (json.hits != null)
hits = json.hits
SharedFile sf = new SharedFile(file, ih, pieceSize) SharedFile sf = new SharedFile(file, ih, pieceSize)
sf.setComment(json.comment) sf.setComment(json.comment)
sf.hits = hits
if (json.downloaders != null) if (json.downloaders != null)
sf.getDownloaders().addAll(json.downloaders) 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) return new FileLoadedEvent(loadedFile: sf)
} }
@@ -172,6 +179,19 @@ class PersisterService extends Service {
json.hits = sf.getHits() json.hits = sf.getHits()
json.downloaders = sf.getDownloaders() 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) { if (sf instanceof DownloadedFile) {
json.sources = sf.sources.stream().map( {d -> d.toBase64()}).collect(Collectors.toList()) json.sources = sf.sources.stream().map( {d -> d.toBase64()}).collect(Collectors.toList())
} }

View File

@@ -2,6 +2,7 @@ package com.muwire.core.search
import com.muwire.core.Constants import com.muwire.core.Constants
import com.muwire.core.EventBus import com.muwire.core.EventBus
import com.muwire.core.Persona
import com.muwire.core.connection.Endpoint import com.muwire.core.connection.Endpoint
import com.muwire.core.connection.I2PConnector import com.muwire.core.connection.I2PConnector
import com.muwire.core.util.DataUtil import com.muwire.core.util.DataUtil
@@ -20,12 +21,14 @@ class BrowseManager {
private final I2PConnector connector private final I2PConnector connector
private final EventBus eventBus private final EventBus eventBus
private final Persona me
private final Executor browserThread = Executors.newSingleThreadExecutor() private final Executor browserThread = Executors.newSingleThreadExecutor()
BrowseManager(I2PConnector connector, EventBus eventBus) { BrowseManager(I2PConnector connector, EventBus eventBus, Persona me) {
this.connector = connector this.connector = connector
this.eventBus = eventBus this.eventBus = eventBus
this.me = me
} }
void onUIBrowseEvent(UIBrowseEvent e) { void onUIBrowseEvent(UIBrowseEvent e) {
@@ -35,7 +38,9 @@ class BrowseManager {
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.CONNECTING)) eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.CONNECTING))
endpoint = connector.connect(e.host.destination) endpoint = connector.connect(e.host.destination)
OutputStream os = endpoint.getOutputStream() 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() InputStream is = endpoint.getInputStream()
String code = DataUtil.readTillRN(is) String code = DataUtil.readTillRN(is)
@@ -43,16 +48,7 @@ class BrowseManager {
throw new IOException("Invalid code $code") throw new IOException("Invalid code $code")
// parse all headers // parse all headers
Map<String,String> headers = new HashMap<>() Map<String,String> headers = DataUtil.readAllHeaders(is)
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")) if (!headers.containsKey("Count"))
throw new IOException("No count header") throw new IOException("No count header")

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ package com.muwire.core.search
import com.muwire.core.SharedFile import com.muwire.core.SharedFile
import com.muwire.core.connection.Endpoint import com.muwire.core.connection.Endpoint
import com.muwire.core.connection.I2PConnector import com.muwire.core.connection.I2PConnector
import com.muwire.core.filecert.CertificateManager
import com.muwire.core.files.FileHasher import com.muwire.core.files.FileHasher
import com.muwire.core.util.DataUtil import com.muwire.core.util.DataUtil
import com.muwire.core.Persona import com.muwire.core.Persona
@@ -46,12 +47,14 @@ class ResultsSender {
private final Persona me private final Persona me
private final EventBus eventBus private final EventBus eventBus
private final MuWireSettings settings private final MuWireSettings settings
private final CertificateManager certificateManager
ResultsSender(EventBus eventBus, I2PConnector connector, Persona me, MuWireSettings settings) { ResultsSender(EventBus eventBus, I2PConnector connector, Persona me, MuWireSettings settings, CertificateManager certificateManager) {
this.connector = connector; this.connector = connector;
this.eventBus = eventBus this.eventBus = eventBus
this.me = me this.me = me
this.settings = settings this.settings = settings
this.certificateManager = certificateManager
} }
void sendResults(UUID uuid, SharedFile[] results, Destination target, boolean oobInfohash, boolean compressedResults) { void sendResults(UUID uuid, SharedFile[] results, Destination target, boolean oobInfohash, boolean compressedResults) {
@@ -70,6 +73,7 @@ class ResultsSender {
if (it.getComment() != null) { if (it.getComment() != null) {
comment = DataUtil.readi18nString(Base64.decode(it.getComment())) comment = DataUtil.readi18nString(Base64.decode(it.getComment()))
} }
int certificates = certificateManager.getByInfoHash(it.getInfoHash()).size()
def uiResultEvent = new UIResultEvent( sender : me, def uiResultEvent = new UIResultEvent( sender : me,
name : it.getFile().getName(), name : it.getFile().getName(),
size : length, size : length,
@@ -77,7 +81,8 @@ class ResultsSender {
pieceSize : pieceSize, pieceSize : pieceSize,
uuid : uuid, uuid : uuid,
sources : suggested, sources : suggested,
comment : comment comment : comment,
certificates : certificates
) )
uiResultEvents << uiResultEvent uiResultEvents << uiResultEvent
} }
@@ -108,7 +113,8 @@ class ResultsSender {
me.write(os) me.write(os)
os.writeShort((short)results.length) os.writeShort((short)results.length)
results.each { 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) def json = jsonOutput.toJson(obj)
os.writeShort((short)json.length()) os.writeShort((short)json.length())
os.write(json.getBytes(StandardCharsets.US_ASCII)) os.write(json.getBytes(StandardCharsets.US_ASCII))
@@ -127,7 +133,8 @@ class ResultsSender {
os.write("\r\n".getBytes(StandardCharsets.US_ASCII)) os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os)) DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))
results.each { 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) def json = jsonOutput.toJson(obj)
dos.writeShort((short)json.length()) dos.writeShort((short)json.length())
dos.write(json.getBytes(StandardCharsets.US_ASCII)) dos.write(json.getBytes(StandardCharsets.US_ASCII))
@@ -143,7 +150,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) byte [] name = sf.getFile().getName().getBytes(StandardCharsets.UTF_8)
def baos = new ByteArrayOutputStream() def baos = new ByteArrayOutputStream()
def daos = new DataOutputStream(baos) def daos = new DataOutputStream(baos)
@@ -166,6 +173,7 @@ class ResultsSender {
obj.comment = sf.getComment() obj.comment = sf.getComment()
obj.browse = browseFiles obj.browse = browseFiles
obj.certificates = certificates
obj obj
} }
} }

View File

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

View File

@@ -31,25 +31,49 @@ class SearchIndex {
} }
} }
private static String[] split(String source) { private static String[] split(final String source) {
source = source.replaceAll(SplitPattern.SPLIT_PATTERN, " ").toLowerCase() // first split by split pattern
String [] split = source.split(" ") String sourceSplit = source.replaceAll(SplitPattern.SPLIT_PATTERN, " ").toLowerCase()
String [] split = sourceSplit.split(" ")
def rv = [] def rv = []
split.each { if (it.length() > 0) rv << it } 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]) rv.toArray(new String[0])
} }
String[] search(List<String> terms) { String[] search(List<String> terms) {
Set<String> rv = null; Set<String> rv = null;
Set<String> powerSet = new HashSet<>()
terms.each { terms.each {
powerSet.addAll(it.toLowerCase().split(' '))
}
powerSet.each {
Set<String> forWord = keywords.getOrDefault(it,[]) Set<String> forWord = keywords.getOrDefault(it,[])
if (rv == null) { if (rv == null) {
rv = new HashSet<>(forWord) rv = new HashSet<>(forWord)
} else { } else {
rv.retainAll(forWord) 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) if (rv != null)

View File

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

View File

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

View File

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

View File

@@ -1,21 +1,26 @@
package com.muwire.core.trust package com.muwire.core.trust
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.logging.Level
import com.muwire.core.Persona import com.muwire.core.Persona
import com.muwire.core.Service 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.Base64
import net.i2p.data.Destination import net.i2p.data.Destination
import net.i2p.util.ConcurrentHashSet import net.i2p.util.ConcurrentHashSet
@Log
class TrustService extends Service { class TrustService extends Service {
final File persistGood, persistBad final File persistGood, persistBad
final long persistInterval final long persistInterval
final Map<Destination, Persona> good = new ConcurrentHashMap<>() final Map<Destination, TrustEntry> good = new ConcurrentHashMap<>()
final Map<Destination, Persona> bad = new ConcurrentHashMap<>() final Map<Destination, TrustEntry> bad = new ConcurrentHashMap<>()
final Timer timer final Timer timer
@@ -37,18 +42,41 @@ class TrustService extends Service {
} }
void load() { void load() {
JsonSlurper slurper = new JsonSlurper()
if (persistGood.exists()) { if (persistGood.exists()) {
persistGood.eachLine { persistGood.eachLine {
byte [] decoded = Base64.decode(it) try {
Persona persona = new Persona(new ByteArrayInputStream(decoded)) byte [] decoded = Base64.decode(it)
good.put(persona.destination, persona) 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()) { if (persistBad.exists()) {
persistBad.eachLine { persistBad.eachLine {
byte [] decoded = Base64.decode(it) try {
Persona persona = new Persona(new ByteArrayInputStream(decoded)) byte [] decoded = Base64.decode(it)
bad.put(persona.destination, persona) 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) timer.schedule({persist()} as TimerTask, persistInterval, persistInterval)
@@ -59,13 +87,19 @@ class TrustService extends Service {
persistGood.delete() persistGood.delete()
persistGood.withPrintWriter { writer -> persistGood.withPrintWriter { writer ->
good.each {k,v -> 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.delete()
persistBad.withPrintWriter { writer -> persistBad.withPrintWriter { writer ->
bad.each { k,v -> 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) { switch(e.level) {
case TrustLevel.TRUSTED: case TrustLevel.TRUSTED:
bad.remove(e.persona.destination) bad.remove(e.persona.destination)
good.put(e.persona.destination, e.persona) good.put(e.persona.destination, new TrustEntry(e.persona, e.reason))
break break
case TrustLevel.DISTRUSTED: case TrustLevel.DISTRUSTED:
good.remove(e.persona.destination) good.remove(e.persona.destination)
bad.put(e.persona.destination, e.persona) bad.put(e.persona.destination, new TrustEntry(e.persona, e.reason))
break break
case TrustLevel.NEUTRAL: case TrustLevel.NEUTRAL:
good.remove(e.persona.destination) good.remove(e.persona.destination)
@@ -94,4 +128,24 @@ class TrustService extends Service {
break 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.UILoadedEvent
import com.muwire.core.connection.Endpoint import com.muwire.core.connection.Endpoint
import com.muwire.core.connection.I2PConnector import com.muwire.core.connection.I2PConnector
import com.muwire.core.trust.TrustService.TrustEntry
import com.muwire.core.util.DataUtil import com.muwire.core.util.DataUtil
import groovy.json.JsonSlurper
import groovy.util.logging.Log import groovy.util.logging.Log
import net.i2p.data.Base64
import net.i2p.data.Destination import net.i2p.data.Destination
@Log @Log
@@ -109,7 +112,9 @@ class TrustSubscriber {
endpoint = i2pConnector.connect(trustList.persona.destination) endpoint = i2pConnector.connect(trustList.persona.destination)
OutputStream os = endpoint.getOutputStream() OutputStream os = endpoint.getOutputStream()
InputStream is = endpoint.getInputStream() 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() os.flush()
String codeString = DataUtil.readTillRN(is) String codeString = DataUtil.readTillRN(is)
@@ -123,24 +128,47 @@ class TrustSubscriber {
return false return false
} }
// swallow any headers Map<String,String> headers = DataUtil.readAllHeaders(is)
String header
while (( header = DataUtil.readTillRN(is)) != "");
DataInputStream dis = new DataInputStream(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 nBad = dis.readUnsignedShort()
int nGood = dis.readUnsignedShort() for (int i = 0; i < nBad; i++) {
for (int i = 0; i < nGood; i++) { Persona p = new Persona(dis)
Persona p = new Persona(dis) bad.add(new TrustEntry(p, null))
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)
} }
trustList.timestamp = now trustList.timestamp = now

View File

@@ -9,9 +9,11 @@ import com.muwire.core.Persona
import com.muwire.core.download.UIDownloadEvent import com.muwire.core.download.UIDownloadEvent
import com.muwire.core.files.FileDownloadedEvent import com.muwire.core.files.FileDownloadedEvent
import com.muwire.core.files.FileManager import com.muwire.core.files.FileManager
import com.muwire.core.files.FileSharedEvent
import com.muwire.core.search.QueryEvent import com.muwire.core.search.QueryEvent
import com.muwire.core.search.SearchEvent import com.muwire.core.search.SearchEvent
import com.muwire.core.search.UIResultBatchEvent import com.muwire.core.search.UIResultBatchEvent
import com.muwire.core.util.DataUtil
import groovy.json.JsonOutput import groovy.json.JsonOutput
import groovy.json.JsonSlurper import groovy.json.JsonSlurper
@@ -21,7 +23,10 @@ import net.i2p.client.I2PSessionMuxedListener
import net.i2p.client.SendMessageOptions import net.i2p.client.SendMessageOptions
import net.i2p.client.datagram.I2PDatagramDissector import net.i2p.client.datagram.I2PDatagramDissector
import net.i2p.client.datagram.I2PDatagramMaker import net.i2p.client.datagram.I2PDatagramMaker
import net.i2p.crypto.DSAEngine
import net.i2p.data.Base64 import net.i2p.data.Base64
import net.i2p.data.Signature
import net.i2p.data.SigningPrivateKey
import net.i2p.util.VersionComparator import net.i2p.util.VersionComparator
@Log @Log
@@ -32,6 +37,7 @@ class UpdateClient {
final MuWireSettings settings final MuWireSettings settings
final FileManager fileManager final FileManager fileManager
final Persona me final Persona me
final SigningPrivateKey spk
private final Timer timer private final Timer timer
@@ -43,13 +49,16 @@ class UpdateClient {
private volatile String text 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.eventBus = eventBus
this.session = session this.session = session
this.myVersion = myVersion this.myVersion = myVersion
this.settings = settings this.settings = settings
this.fileManager = fileManager this.fileManager = fileManager
this.me = me this.me = me
this.spk = spk
this.lastUpdateCheckTime = settings.lastUpdateCheck
timer = new Timer("update-client",true) timer = new Timer("update-client",true)
} }
@@ -78,6 +87,8 @@ class UpdateClient {
return return
updateDownloading = false updateDownloading = false
eventBus.publish(new UpdateDownloadedEvent(version : version, signer : signer, text : text)) eventBus.publish(new UpdateDownloadedEvent(version : version, signer : signer, text : text))
if (!settings.shareDownloadedFiles)
eventBus.publish(new FileSharedEvent(file : e.downloadedFile))
} }
private void checkUpdate() { private void checkUpdate() {
@@ -87,6 +98,7 @@ class UpdateClient {
return return
} }
lastUpdateCheckTime = now lastUpdateCheckTime = now
settings.lastUpdateCheck = now
log.info("checking for update") log.info("checking for update")
@@ -164,9 +176,13 @@ class UpdateClient {
version = payload.version version = payload.version
signer = payload.signer signer = payload.signer
log.info("starting search for new version hash $payload.infoHash") 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, 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) eventBus.publish(queryEvent)
} }
} }

View File

@@ -4,6 +4,7 @@ import net.i2p.crypto.SigType;
public class Constants { public class Constants {
public static final byte PERSONA_VERSION = (byte)1; public static final byte PERSONA_VERSION = (byte)1;
public static final byte FILE_CERT_VERSION = (byte)2;
public static final SigType SIG_TYPE = SigType.EdDSA_SHA512_Ed25519; public static final SigType SIG_TYPE = SigType.EdDSA_SHA512_Ed25519;
public static final int MAX_HEADER_SIZE = 0x1 << 14; public static final int MAX_HEADER_SIZE = 0x1 << 14;
@@ -12,4 +13,6 @@ public class Constants {
public static final int MAX_RESULTS = 0x1 << 16; public static final int MAX_RESULTS = 0x1 << 16;
public static final int MAX_COMMENT_LENGTH = 0x1 << 15; 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 { 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.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import com.muwire.core.util.DataUtil; import com.muwire.core.util.DataUtil;
@@ -26,8 +27,8 @@ public class SharedFile {
private final List<String> b64EncodedHashList; private final List<String> b64EncodedHashList;
private volatile String comment; private volatile String comment;
private volatile int hits;
private final Set<String> downloaders = Collections.synchronizedSet(new HashSet<>()); 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 { public SharedFile(File file, InfoHash infoHash, int pieceSize) throws IOException {
this.file = file; this.file = file;
@@ -97,17 +98,21 @@ public class SharedFile {
} }
public int getHits() { public int getHits() {
return hits; return searches.size();
} }
public void hit() { public void hit(Persona searcher, long timestamp, String query) {
hits++; searches.add(new SearchEntry(searcher, timestamp, query));
} }
public Set<String> getDownloaders() { public Set<String> getDownloaders() {
return downloaders; return downloaders;
} }
public Set<SearchEntry> getSearches() {
return searches;
}
public void addDownloader(String name) { public void addDownloader(String name) {
downloaders.add(name); downloaders.add(name);
} }
@@ -124,4 +129,29 @@ public class SharedFile {
SharedFile other = (SharedFile)o; SharedFile other = (SharedFile)o;
return file.equals(other.file) && infoHash.equals(other.infoHash); return file.equals(other.file) && infoHash.equals(other.infoHash);
} }
public static class SearchEntry {
private final Persona searcher;
private final long timestamp;
private final String query;
public SearchEntry(Persona searcher, long timestamp, String query) {
this.searcher = searcher;
this.timestamp = timestamp;
this.query = query;
}
public int hashCode() {
return Objects.hash(searcher) ^ Objects.hash(timestamp) ^ query.hashCode();
}
public boolean equals(Object o) {
if (!(o instanceof SearchEntry))
return false;
SearchEntry other = (SearchEntry)o;
return Objects.equals(searcher, other.searcher) &&
timestamp == other.timestamp &&
query.equals(other.query);
}
}
} }

View File

@@ -10,14 +10,20 @@ import java.lang.reflect.Method;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Properties; import java.util.Properties;
import java.util.Set; import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import com.muwire.core.Constants; import com.muwire.core.Constants;
import net.i2p.crypto.DSAEngine;
import net.i2p.data.Base64; import net.i2p.data.Base64;
import net.i2p.data.Signature;
import net.i2p.data.SigningPrivateKey;
import net.i2p.util.ConcurrentHashSet; import net.i2p.util.ConcurrentHashSet;
public class DataUtil { public class DataUtil {
@@ -99,6 +105,20 @@ public class DataUtil {
} }
return new String(baos.toByteArray(), StandardCharsets.US_ASCII); 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) { public static String encodeXHave(List<Integer> pieces, int totalPieces) {
int bytes = totalPieces / 8; int bytes = totalPieces / 8;
@@ -187,4 +207,10 @@ public class DataUtil {
.collect(Collectors.joining(",")); .collect(Collectors.joining(","));
props.setProperty(property, encoded); 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() connectionEstablisher = connectionEstablisherMock.proxyInstance()
acceptor = new ConnectionAcceptor(eventBus, connectionManager, settings, i2pAcceptor, acceptor = new ConnectionAcceptor(eventBus, connectionManager, settings, i2pAcceptor,
hostCache, trustService, searchManager, uploadManager, null, connectionEstablisher) hostCache, trustService, searchManager, uploadManager, null, connectionEstablisher, null)
acceptor.start() acceptor.start()
Thread.sleep(100) Thread.sleep(100)
} }

View File

@@ -1,5 +1,7 @@
package com.muwire.core.files package com.muwire.core.files
import static org.junit.jupiter.api.Assertions.assertAll
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
@@ -9,6 +11,9 @@ import com.muwire.core.MuWireSettings
import com.muwire.core.SharedFile import com.muwire.core.SharedFile
import com.muwire.core.search.ResultsEvent import com.muwire.core.search.ResultsEvent
import com.muwire.core.search.SearchEvent import com.muwire.core.search.SearchEvent
import com.muwire.core.util.DataUtil
import net.i2p.data.Base64
class FileManagerTest { class FileManagerTest {
@@ -149,7 +154,7 @@ class FileManagerTest {
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf1) manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf1)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf2) 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()) manager.onSearchEvent new SearchEvent(searchHash : ih.getRoot())
Thread.sleep(20) Thread.sleep(20)
@@ -170,7 +175,7 @@ class FileManagerTest {
SharedFile sf2 = new SharedFile(f2, ih2, 0) SharedFile sf2 = new SharedFile(f2, ih2, 0)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf2) manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf2)
manager.onFileUnsharedEvent new FileUnsharedEvent(unsharedFile: sf2) manager.onFileUnsharedEvent new FileUnsharedEvent(deleted : true, unsharedFile: sf2)
// 1 match left // 1 match left
manager.onSearchEvent new SearchEvent(searchTerms: ["c"]) manager.onSearchEvent new SearchEvent(searchTerms: ["c"])
@@ -185,4 +190,39 @@ class FileManagerTest {
assert results == null 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"]) def found = index.search(["muwire", "0", "3", "jar"])
assert found.size() == 1 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.Persona
import com.muwire.core.Personas import com.muwire.core.Personas
import groovy.json.JsonSlurper
import net.i2p.data.Base64 import net.i2p.data.Base64
import net.i2p.data.Destination import net.i2p.data.Destination
@@ -55,13 +56,16 @@ class TrustServiceTest {
service.onTrustEvent new TrustEvent(level: TrustLevel.DISTRUSTED, persona: personas.persona2) service.onTrustEvent new TrustEvent(level: TrustLevel.DISTRUSTED, persona: personas.persona2)
Thread.sleep(250) Thread.sleep(250)
JsonSlurper slurper = new JsonSlurper()
def trusted = new HashSet<>() def trusted = new HashSet<>()
persistGood.eachLine { 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<>() def distrusted = new HashSet<>()
persistBad.eachLine { 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 assert trusted.size() == 1

View File

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

View File

@@ -86,4 +86,29 @@ mvcGroups {
view = 'com.muwire.gui.AdvancedSharingView' view = 'com.muwire.gui.AdvancedSharingView'
controller = 'com.muwire.gui.AdvancedSharingController' 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"
}
} }

View File

@@ -8,6 +8,7 @@ import net.i2p.data.Base64
import javax.annotation.Nonnull import javax.annotation.Nonnull
import com.muwire.core.Core
import com.muwire.core.EventBus import com.muwire.core.EventBus
import com.muwire.core.download.UIDownloadEvent import com.muwire.core.download.UIDownloadEvent
import com.muwire.core.search.BrowseStatus import com.muwire.core.search.BrowseStatus
@@ -22,18 +23,18 @@ class BrowseController {
@MVCMember @Nonnull @MVCMember @Nonnull
BrowseView view BrowseView view
EventBus eventBus Core core
void register() { void register() {
eventBus.register(BrowseStatusEvent.class, this) core.eventBus.register(BrowseStatusEvent.class, this)
eventBus.register(UIResultEvent.class, this) core.eventBus.register(UIResultEvent.class, this)
eventBus.publish(new UIBrowseEvent(host : model.host)) core.eventBus.publish(new UIBrowseEvent(host : model.host))
} }
void mvcGroupDestroy() { void mvcGroupDestroy() {
eventBus.unregister(BrowseStatusEvent.class, this) core.eventBus.unregister(BrowseStatusEvent.class, this)
eventBus.unregister(UIResultEvent.class, this) core.eventBus.unregister(UIResultEvent.class, this)
} }
void onBrowseStatusEvent(BrowseStatusEvent e) { void onBrowseStatusEvent(BrowseStatusEvent e) {
@@ -69,7 +70,7 @@ class BrowseController {
selectedResults.each { result -> selectedResults.each { result ->
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name) def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
eventBus.publish(new UIDownloadEvent( core.eventBus.publish(new UIDownloadEvent(
result : [result], result : [result],
sources : [model.host.destination], sources : [model.host.destination],
target : file, target : file,
@@ -92,8 +93,24 @@ class BrowseController {
String groupId = Base64.encode(result.infohash.getRoot()) String groupId = Base64.encode(result.infohash.getRoot())
Map<String,Object> params = new HashMap<>() Map<String,Object> params = new HashMap<>()
params['result'] = result params['text'] = result.comment
params['name'] = result.name
mvcGroup.createMVCGroup("show-comment", groupId, params) 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

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

View File

@@ -0,0 +1,13 @@
package com.muwire.gui
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonController)
class DownloadPreviewController {
@MVCMember @Nonnull
DownloadPreviewModel model
}

View File

@@ -0,0 +1,85 @@
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 javax.swing.JOptionPane
import com.muwire.core.Core
import com.muwire.core.EventBus
import com.muwire.core.filecert.CertificateFetchEvent
import com.muwire.core.filecert.CertificateFetchStatus
import com.muwire.core.filecert.CertificateFetchedEvent
import com.muwire.core.filecert.UIFetchCertificatesEvent
import com.muwire.core.filecert.UIImportCertificateEvent
@ArtifactProviderFor(GriffonController)
class FetchCertificatesController {
@MVCMember @Nonnull
FetchCertificatesModel model
@MVCMember @Nonnull
FetchCertificatesView view
Core core
void register() {
core.eventBus.with {
register(CertificateFetchEvent.class, this)
register(CertificateFetchedEvent.class, this)
publish(new UIFetchCertificatesEvent(host : model.result.sender, infoHash : model.result.infohash))
}
}
void mvcGroupDestroy() {
core.eventBus.unregister(CertificateFetchEvent.class, this)
core.eventBus.unregister(CertificateFetchedEvent.class, this)
}
void onCertificateFetchEvent(CertificateFetchEvent e) {
runInsideUIAsync {
model.status = e.status
if (e.status == CertificateFetchStatus.FETCHING)
model.totalCertificates = e.count
}
}
void onCertificateFetchedEvent(CertificateFetchedEvent e) {
runInsideUIAsync {
model.certificates << e.certificate
model.certificateCount = model.certificates.size()
view.certsTable.model.fireTableDataChanged()
}
}
@ControllerAction
void importCertificates() {
def selectedCerts = view.selectedCertificates()
if (selectedCerts == null)
return
selectedCerts.each {
core.eventBus.publish(new UIImportCertificateEvent(certificate : it))
}
JOptionPane.showMessageDialog(null, "Certificates imported.")
}
@ControllerAction
void showComment() {
def selectedCerts = view.selectedCertificates()
if (selectedCerts == null || selectedCerts.size() != 1)
return
String comment = selectedCerts[0].comment.name
def params = [:]
params['text'] = comment
params['name'] = "Certificate Comment"
mvcGroup.createMVCGroup("show-comment", params)
}
@ControllerAction
void dismiss() {
view.dialog.setVisible(false)
mvcGroup.destroy()
}
}

View File

@@ -7,18 +7,23 @@ import griffon.core.mvc.MVCGroup
import griffon.core.mvc.MVCGroupConfiguration import griffon.core.mvc.MVCGroupConfiguration
import griffon.inject.MVCMember import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor import griffon.metadata.ArtifactProviderFor
import groovy.json.StringEscapeUtils
import net.i2p.crypto.DSAEngine import net.i2p.crypto.DSAEngine
import net.i2p.data.Base64 import net.i2p.data.Base64
import net.i2p.data.Signature import net.i2p.data.Signature
import net.i2p.data.SigningPrivateKey
import java.awt.Desktop
import java.awt.event.ActionEvent import java.awt.event.ActionEvent
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import javax.annotation.Nonnull import javax.annotation.Nonnull
import javax.inject.Inject import javax.inject.Inject
import javax.swing.JOptionPane
import javax.swing.JTable import javax.swing.JTable
import com.muwire.core.Core import com.muwire.core.Core
import com.muwire.core.InfoHash
import com.muwire.core.Persona import com.muwire.core.Persona
import com.muwire.core.SharedFile import com.muwire.core.SharedFile
import com.muwire.core.SplitPattern import com.muwire.core.SplitPattern
@@ -28,6 +33,7 @@ import com.muwire.core.download.UIDownloadCancelledEvent
import com.muwire.core.download.UIDownloadEvent import com.muwire.core.download.UIDownloadEvent
import com.muwire.core.download.UIDownloadPausedEvent import com.muwire.core.download.UIDownloadPausedEvent
import com.muwire.core.download.UIDownloadResumedEvent import com.muwire.core.download.UIDownloadResumedEvent
import com.muwire.core.filecert.UICreateCertificateEvent
import com.muwire.core.files.DirectoryUnsharedEvent import com.muwire.core.files.DirectoryUnsharedEvent
import com.muwire.core.files.FileUnsharedEvent import com.muwire.core.files.FileUnsharedEvent
import com.muwire.core.files.UIPersistFilesEvent import com.muwire.core.files.UIPersistFilesEvent
@@ -37,6 +43,9 @@ import com.muwire.core.trust.RemoteTrustList
import com.muwire.core.trust.TrustEvent import com.muwire.core.trust.TrustEvent
import com.muwire.core.trust.TrustLevel import com.muwire.core.trust.TrustLevel
import com.muwire.core.trust.TrustSubscriptionEvent import com.muwire.core.trust.TrustSubscriptionEvent
import com.muwire.core.upload.HashListUploader
import com.muwire.core.upload.Uploader
import com.muwire.core.util.DataUtil
@ArtifactProviderFor(GriffonController) @ArtifactProviderFor(GriffonController)
class MainFrameController { class MainFrameController {
@@ -85,6 +94,7 @@ class MainFrameController {
params["search-terms"] = search params["search-terms"] = search
params["uuid"] = uuid.toString() params["uuid"] = uuid.toString()
params["core"] = core params["core"] = core
params["settings"] = view.settings
def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params) def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params)
model.results[uuid.toString()] = group model.results[uuid.toString()] = group
@@ -102,25 +112,22 @@ class MainFrameController {
def searchEvent def searchEvent
byte [] payload byte [] payload
if (hashSearch) { if (hashSearch) {
searchEvent = new SearchEvent(searchHash : root, uuid : uuid, oobInfohash: true, compressedResults : true) searchEvent = new SearchEvent(searchHash : root, uuid : uuid, oobInfohash: true, compressedResults : true, persona : core.me)
payload = root payload = root
} else { } else {
// this can be improved a lot def nonEmpty = SplitPattern.termify(search)
def replaced = search.toLowerCase().trim().replaceAll(SplitPattern.SPLIT_PATTERN, " ")
def terms = replaced.split(" ")
def nonEmpty = []
terms.each { if (it.length() > 0) nonEmpty << it }
payload = String.join(" ",nonEmpty).getBytes(StandardCharsets.UTF_8) payload = String.join(" ",nonEmpty).getBytes(StandardCharsets.UTF_8)
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : uuid, oobInfohash: true, searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : uuid, oobInfohash: true,
searchComments : core.muOptions.searchComments, compressedResults : true) searchComments : core.muOptions.searchComments, compressedResults : true, persona : core.me)
} }
boolean firstHop = core.muOptions.allowUntrusted || core.muOptions.searchExtraHop boolean firstHop = core.muOptions.allowUntrusted || core.muOptions.searchExtraHop
Signature sig = DSAEngine.getInstance().sign(payload, core.spk) Signature sig = DSAEngine.getInstance().sign(payload, core.spk)
long timestamp = System.currentTimeMillis()
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : firstHop, core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : firstHop,
replyTo: core.me.destination, receivedOn: core.me.destination, replyTo: core.me.destination, receivedOn: core.me.destination,
originator : core.me, sig : sig.data)) originator : core.me, sig : sig.data, queryTime : timestamp, sig2 : DataUtil.signUUID(uuid, timestamp, core.spk)))
} }
@@ -135,13 +142,18 @@ class MainFrameController {
def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params) def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params)
model.results[uuid.toString()] = group model.results[uuid.toString()] = group
byte [] infoHashBytes = Base64.decode(infoHash)
Signature sig = DSAEngine.getInstance().sign(infoHashBytes, core.spk)
long timestamp = System.currentTimeMillis()
byte [] sig2 = DataUtil.signUUID(uuid, timestamp, core.spk)
def searchEvent = new SearchEvent(searchHash : Base64.decode(infoHash), uuid:uuid, def searchEvent = new SearchEvent(searchHash : Base64.decode(infoHash), uuid:uuid,
oobInfohash: true) oobInfohash: true, persona : core.me)
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true, core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true,
replyTo: core.me.destination, receivedOn: core.me.destination, replyTo: core.me.destination, receivedOn: core.me.destination,
originator : core.me)) originator : core.me, sig : sig.data, queryTime : timestamp, sig2 : sig2))
} }
private int selectedDownload() { private int selectedDownload() {
def downloadsTable = builder.getVariable("downloads-table") def downloadsTable = builder.getVariable("downloads-table")
def selected = downloadsTable.getSelectedRow() def selected = downloadsTable.getSelectedRow()
@@ -156,8 +168,9 @@ class MainFrameController {
int selected = builder.getVariable("searches-table").getSelectedRow() int selected = builder.getVariable("searches-table").getSelectedRow()
if (selected < 0) if (selected < 0)
return return
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
Persona p = model.searches[selected].originator Persona p = model.searches[selected].originator
core.eventBus.publish( new TrustEvent(persona : p, level : TrustLevel.TRUSTED) ) core.eventBus.publish( new TrustEvent(persona : p, level : TrustLevel.TRUSTED, reason : reason) )
} }
@ControllerAction @ControllerAction
@@ -165,8 +178,9 @@ class MainFrameController {
int selected = builder.getVariable("searches-table").getSelectedRow() int selected = builder.getVariable("searches-table").getSelectedRow()
if (selected < 0) if (selected < 0)
return return
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
Persona p = model.searches[selected].originator Persona p = model.searches[selected].originator
core.eventBus.publish( new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED) ) core.eventBus.publish( new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED, reason : reason) )
} }
@ControllerAction @ControllerAction
@@ -190,6 +204,14 @@ class MainFrameController {
downloader.pause() downloader.pause()
core.eventBus.publish(new UIDownloadPausedEvent()) core.eventBus.publish(new UIDownloadPausedEvent())
} }
@ControllerAction
void preview() {
def downloader = model.downloads[selectedDownload()].downloader
def params = [:]
params['downloader'] = downloader
mvcGroup.createMVCGroup("download-preview", params)
}
@ControllerAction @ControllerAction
void clear() { void clear() {
@@ -213,7 +235,7 @@ class MainFrameController {
if (row < 0) if (row < 0)
return return
builder.getVariable(tableName).model.fireTableDataChanged() builder.getVariable(tableName).model.fireTableDataChanged()
core.eventBus.publish(new TrustEvent(persona : list[row], level : level)) core.eventBus.publish(new TrustEvent(persona : list[row].persona, level : level))
} }
@ControllerAction @ControllerAction
@@ -251,7 +273,7 @@ class MainFrameController {
int row = view.getSelectedTrustTablesRow("trusted-table") int row = view.getSelectedTrustTablesRow("trusted-table")
if (row < 0) if (row < 0)
return return
Persona p = model.trusted[row] Persona p = model.trusted[row].persona
core.muOptions.trustSubscriptions.add(p) core.muOptions.trustSubscriptions.add(p)
saveMuWireSettings() saveMuWireSettings()
core.eventBus.publish(new TrustSubscriptionEvent(persona : p, subscribe : true)) core.eventBus.publish(new TrustSubscriptionEvent(persona : p, subscribe : true))
@@ -328,6 +350,28 @@ class MainFrameController {
model.uploads.removeAll { it.finished } model.uploads.removeAll { it.finished }
} }
@ControllerAction
void showInLibrary() {
Uploader uploader = view.selectedUploader()
if (uploader == null)
return
SharedFile sf = null
if (uploader instanceof HashListUploader) {
InfoHash infoHash = uploader.infoHash
Set<SharedFile> sfs = core.fileManager.rootToFiles.get(infoHash)
if (sfs != null && !sfs.isEmpty())
sf = sfs.first()
} else {
File f = uploader.file
sf = core.fileManager.fileToSharedFile.get(f)
}
if (sf == null)
return // can happen if user un-shared
view.focusOnSharedFile(sf)
}
@ControllerAction @ControllerAction
void restoreSession() { void restoreSession() {
model.sessionRestored = true model.sessionRestored = true
@@ -335,6 +379,47 @@ class MainFrameController {
performSearch(it) performSearch(it)
} }
} }
@ControllerAction
void issueCertificate() {
if (view.settings.certificateWarning) {
def params = [:]
params['settings'] = view.settings
params['home'] = core.home
mvcGroup.createMVCGroup("certificate-warning", params)
} else {
view.selectedSharedFiles().each {
core.eventBus.publish(new UICreateCertificateEvent(sharedFile : it))
}
JOptionPane.showMessageDialog(null, "Certificate(s) have been issued")
}
}
@ControllerAction
void showFileDetails() {
def selected = view.selectedSharedFiles()
if (selected == null || selected.size() != 1) {
JOptionPane.showMessageDialog(null, "Please select only one file to view it's details")
return
}
def params = [:]
params['sf'] = selected[0]
params['core'] = core
mvcGroup.createMVCGroup("shared-file", params)
}
@ControllerAction
void openContainingFolder() {
def selected = view.selectedSharedFiles()
if (selected == null || selected.size() != 1) {
JOptionPane.showMessageDialog(null, "Please select only one file to open it's containing folder")
return
}
try {
Desktop.getDesktop().open(selected[0].file.getParentFile())
} catch (Exception ignored) {}
}
void saveMuWireSettings() { void saveMuWireSettings() {
core.saveMuSettings() core.saveMuSettings()

View File

@@ -155,6 +155,8 @@ class OptionsController {
uiSettings.autoFontSize = model.automaticFontSize uiSettings.autoFontSize = model.automaticFontSize
uiSettings.fontSize = Integer.parseInt(view.fontSizeField.text) uiSettings.fontSize = Integer.parseInt(view.fontSizeField.text)
uiSettings.groupByFile = model.groupByFile
boolean clearCancelledDownloads = view.clearCancelledDownloadsCheckbox.model.isSelected() boolean clearCancelledDownloads = view.clearCancelledDownloadsCheckbox.model.isSelected()
model.clearCancelledDownloads = clearCancelledDownloads model.clearCancelledDownloads = clearCancelledDownloads
uiSettings.clearCancelledDownloads = clearCancelledDownloads uiSettings.clearCancelledDownloads = clearCancelledDownloads
@@ -242,6 +244,16 @@ class OptionsController {
model.closeDecisionMade = true model.closeDecisionMade = true
} }
@ControllerAction
void groupByFile() {
model.groupByFile = true
}
@ControllerAction
void groupBySender() {
model.groupByFile = false
}
@ControllerAction @ControllerAction
void clearHistory() { void clearHistory() {
uiSettings.searchHistory.clear() uiSettings.searchHistory.clear()

View File

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

View File

@@ -0,0 +1,28 @@
package com.muwire.gui
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
import com.muwire.core.filecert.Certificate
@ArtifactProviderFor(GriffonController)
class SharedFileController {
@MVCMember @Nonnull
SharedFileView view
@MVCMember @Nonnull
SharedFileModel model
@ControllerAction
void showComment() {
Certificate cert = view.getSelectedCertificate()
if (cert == null || cert.comment == null)
return
def params = [:]
params['text'] = cert.comment.name
mvcGroup.createMVCGroup('show-comment',params)
}
}

View File

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

View File

@@ -14,6 +14,7 @@ class BrowseModel {
@Observable BrowseStatus status @Observable BrowseStatus status
@Observable boolean downloadActionEnabled @Observable boolean downloadActionEnabled
@Observable boolean viewCommentActionEnabled @Observable boolean viewCommentActionEnabled
@Observable boolean viewCertificatesActionEnabled
@Observable int totalResults @Observable int totalResults
@Observable int resultCount @Observable int resultCount

View File

@@ -0,0 +1,21 @@
package com.muwire.gui
import com.muwire.core.Core
import griffon.core.artifact.GriffonModel
import griffon.transform.Observable
import griffon.metadata.ArtifactProviderFor
@ArtifactProviderFor(GriffonModel)
class CertificateControlModel {
def users = []
def certificates = []
Core core
@Observable boolean showCommentActionEnabled
void mvcGroupInit(Map<String,String> args) {
users.addAll(core.certificateManager.byIssuer.keySet())
}
}

View File

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

View File

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

View File

@@ -0,0 +1,22 @@
package com.muwire.gui
import com.muwire.core.filecert.CertificateFetchStatus
import com.muwire.core.search.UIResultEvent
import griffon.core.artifact.GriffonModel
import griffon.transform.Observable
import griffon.metadata.ArtifactProviderFor
@ArtifactProviderFor(GriffonModel)
class FetchCertificatesModel {
UIResultEvent result
@Observable CertificateFetchStatus status
@Observable int totalCertificates
@Observable int certificateCount
@Observable boolean importActionEnabled
@Observable boolean showCommentActionEnabled
def certificates = []
}

View File

@@ -27,6 +27,7 @@ import com.muwire.core.connection.DisconnectionEvent
import com.muwire.core.content.ContentControlEvent import com.muwire.core.content.ContentControlEvent
import com.muwire.core.download.DownloadStartedEvent import com.muwire.core.download.DownloadStartedEvent
import com.muwire.core.download.Downloader import com.muwire.core.download.Downloader
import com.muwire.core.filecert.CertificateCreatedEvent
import com.muwire.core.files.AllFilesLoadedEvent import com.muwire.core.files.AllFilesLoadedEvent
import com.muwire.core.files.DirectoryUnsharedEvent import com.muwire.core.files.DirectoryUnsharedEvent
import com.muwire.core.files.DirectoryWatchedEvent import com.muwire.core.files.DirectoryWatchedEvent
@@ -99,6 +100,7 @@ class MainFrameModel {
@Observable boolean retryButtonEnabled @Observable boolean retryButtonEnabled
@Observable boolean pauseButtonEnabled @Observable boolean pauseButtonEnabled
@Observable boolean clearButtonEnabled @Observable boolean clearButtonEnabled
@Observable boolean previewButtonEnabled
@Observable String resumeButtonText @Observable String resumeButtonText
@Observable boolean addCommentButtonEnabled @Observable boolean addCommentButtonEnabled
@Observable boolean subscribeButtonEnabled @Observable boolean subscribeButtonEnabled
@@ -175,8 +177,7 @@ class MainFrameModel {
} }
} }
builder.getVariable("uploads-table")?.model.fireTableDataChanged() updateTablePreservingSelection("uploads-table")
updateTablePreservingSelection("downloads-table") updateTablePreservingSelection("downloads-table")
updateTablePreservingSelection("trusted-table") updateTablePreservingSelection("trusted-table")
updateTablePreservingSelection("distrusted-table") updateTablePreservingSelection("distrusted-table")
@@ -208,6 +209,7 @@ class MainFrameModel {
core.eventBus.register(UpdateDownloadedEvent.class, this) core.eventBus.register(UpdateDownloadedEvent.class, this)
core.eventBus.register(TrustSubscriptionUpdatedEvent.class, this) core.eventBus.register(TrustSubscriptionUpdatedEvent.class, this)
core.eventBus.register(SearchEvent.class, this) core.eventBus.register(SearchEvent.class, this)
core.eventBus.register(CertificateCreatedEvent.class, this)
core.muOptions.watchedKeywords.each { core.muOptions.watchedKeywords.each {
core.eventBus.publish(new ContentControlEvent(term : it, regex: false, add: true)) core.eventBus.publish(new ContentControlEvent(term : it, regex: false, add: true))
@@ -407,8 +409,7 @@ class MainFrameModel {
wrapper.finished = false wrapper.finished = false
} else } else
uploads << new UploaderWrapper(uploader : e.uploader) uploads << new UploaderWrapper(uploader : e.uploader)
JTable table = builder.getVariable("uploads-table") updateTablePreservingSelection("uploads-table")
table.model.fireTableDataChanged()
view.refreshSharedFiles() view.refreshSharedFiles()
} }
} }
@@ -427,8 +428,7 @@ class MainFrameModel {
} else { } else {
wrapper.finished = true wrapper.finished = true
} }
JTable table = builder.getVariable("uploads-table") updateTablePreservingSelection("uploads-table")
table.model.fireTableDataChanged()
} }
} }
@@ -571,6 +571,12 @@ class MainFrameModel {
} }
} }
void onCertificateCreatedEvent(CertificateCreatedEvent e) {
runInsideUIAsync {
view.refreshSharedFiles()
}
}
private void insertIntoTree(SharedFile file) { private void insertIntoTree(SharedFile file) {
List<File> parents = new ArrayList<>() List<File> parents = new ArrayList<>()
File tmp = file.file.getParentFile() File tmp = file.file.getParentFile()

View File

@@ -42,6 +42,7 @@ class OptionsModel {
@Observable boolean excludeLocalResult @Observable boolean excludeLocalResult
@Observable boolean showSearchHashes @Observable boolean showSearchHashes
@Observable boolean clearUploads @Observable boolean clearUploads
@Observable boolean groupByFile
@Observable boolean exitOnClose @Observable boolean exitOnClose
@Observable boolean closeDecisionMade @Observable boolean closeDecisionMade
@@ -92,6 +93,7 @@ class OptionsModel {
clearUploads = uiSettings.clearUploads clearUploads = uiSettings.clearUploads
exitOnClose = uiSettings.exitOnClose exitOnClose = uiSettings.exitOnClose
storeSearchHistory = uiSettings.storeSearchHistory storeSearchHistory = uiSettings.storeSearchHistory
groupByFile = uiSettings.groupByFile
if (core.router != null) { if (core.router != null) {
inBw = String.valueOf(settings.inBw) inBw = String.valueOf(settings.inBw)

View File

@@ -23,6 +23,8 @@ class SearchTabModel {
@Observable boolean trustButtonsEnabled @Observable boolean trustButtonsEnabled
@Observable boolean browseActionEnabled @Observable boolean browseActionEnabled
@Observable boolean viewCommentActionEnabled @Observable boolean viewCommentActionEnabled
@Observable boolean viewCertificatesActionEnabled
@Observable boolean groupedByFile
Core core Core core
UISettings uiSettings UISettings uiSettings
@@ -33,6 +35,9 @@ class SearchTabModel {
def sourcesBucket = [:] def sourcesBucket = [:]
def sendersBucket = new LinkedHashMap<>() def sendersBucket = new LinkedHashMap<>()
def results2 = []
def senders2 = []
void mvcGroupInit(Map<String, String> args) { void mvcGroupInit(Map<String, String> args) {
core = mvcGroup.parentGroup.model.core core = mvcGroup.parentGroup.model.core
@@ -71,9 +76,14 @@ class SearchTabModel {
sourcesBucket.put(e.infohash, sourceBucket) sourcesBucket.put(e.infohash, sourceBucket)
} }
sourceBucket.addAll(e.sources) sourceBucket.addAll(e.sources)
results2.clear()
results2.addAll(hashBucket.keySet())
JTable table = builder.getVariable("senders-table") JTable table = builder.getVariable("senders-table")
table.model.fireTableDataChanged() table.model.fireTableDataChanged()
table = builder.getVariable("results-table2")
table.model.fireTableDataChanged()
} }
} }
@@ -106,9 +116,16 @@ class SearchTabModel {
bucket << it bucket << it
senderBucket << it senderBucket << it
} }
results2.clear()
results2.addAll(hashBucket.keySet())
JTable table = builder.getVariable("senders-table") JTable table = builder.getVariable("senders-table")
table.model.fireTableDataChanged() table.model.fireTableDataChanged()
table = builder.getVariable("results-table2")
int selectedRow = table.getSelectedRow()
table.model.fireTableDataChanged()
table.selectionModel.setSelectionInterval(selectedRow, selectedRow)
} }
} }
} }

View File

@@ -0,0 +1,26 @@
package com.muwire.gui
import com.muwire.core.Core
import com.muwire.core.SharedFile
import griffon.core.artifact.GriffonModel
import griffon.transform.Observable
import griffon.metadata.ArtifactProviderFor
@ArtifactProviderFor(GriffonModel)
class SharedFileModel {
SharedFile sf
Core core
def searchers = []
def downloaders = []
def certificates = []
@Observable boolean showCommentActionEnabled
public void mvcGroupInit(Map<String,String> args) {
searchers.addAll(sf.getSearches())
downloaders.addAll(sf.getDownloaders())
certificates.addAll(core.certificateManager.byInfoHash.getOrDefault(sf.infoHash,[]))
}
}

View File

@@ -8,5 +8,6 @@ import griffon.metadata.ArtifactProviderFor
@ArtifactProviderFor(GriffonModel) @ArtifactProviderFor(GriffonModel)
class ShowCommentModel { class ShowCommentModel {
UIResultEvent result String name
String text
} }

View File

@@ -40,6 +40,7 @@ class BrowseView {
def resultsTable def resultsTable
def lastSortEvent def lastSortEvent
void initUI() { void initUI() {
int rowHeight = application.context.get("row-height")
mainFrame = application.windowManager.findWindow("main-frame") mainFrame = application.windowManager.findWindow("main-frame")
dialog = new JDialog(mainFrame, model.host.getHumanReadableName(), true) dialog = new JDialog(mainFrame, model.host.getHumanReadableName(), true)
dialog.setResizable(true) dialog.setResizable(true)
@@ -52,17 +53,19 @@ class BrowseView {
label(text : bind {model.totalResults == 0 ? "" : Math.round(model.resultCount * 100 / model.totalResults)+ "%"}) label(text : bind {model.totalResults == 0 ? "" : Math.round(model.resultCount * 100 / model.totalResults)+ "%"})
} }
scrollPane (constraints : BorderLayout.CENTER){ scrollPane (constraints : BorderLayout.CENTER){
resultsTable = table(autoCreateRowSorter : true) { resultsTable = table(autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.results) { tableModel(list : model.results) {
closureColumn(header: "Name", preferredWidth: 350, type: String, read : {row -> row.name.replace('<','_')}) closureColumn(header: "Name", preferredWidth: 350, type: String, read : {row -> row.name.replace('<','_')})
closureColumn(header: "Size", preferredWidth: 20, type: Long, read : {row -> row.size}) closureColumn(header: "Size", preferredWidth: 20, type: Long, read : {row -> row.size})
closureColumn(header: "Comments", preferredWidth: 20, type: Boolean, read : {row -> row.comment != null}) closureColumn(header: "Comments", preferredWidth: 20, type: Boolean, read : {row -> row.comment != null})
closureColumn(header: "Certificates", preferredWidth: 20, type: Integer, read : {row -> row.certificates})
} }
} }
} }
panel (constraints : BorderLayout.SOUTH) { panel (constraints : BorderLayout.SOUTH) {
button(text : "Download", enabled : bind {model.downloadActionEnabled}, downloadAction) button(text : "Download", enabled : bind {model.downloadActionEnabled}, downloadAction)
button(text : "View Comment", enabled : bind{model.viewCommentActionEnabled}, viewCommentAction) button(text : "View Comment", enabled : bind{model.viewCommentActionEnabled}, viewCommentAction)
button(text : "View Certificates", enabled : bind{model.viewCertificatesActionEnabled}, viewCertificatesAction)
button(text : "Dismiss", dismissAction) button(text : "Dismiss", dismissAction)
} }
} }
@@ -84,6 +87,7 @@ class BrowseView {
if (rows.length == 0) { if (rows.length == 0) {
model.downloadActionEnabled = false model.downloadActionEnabled = false
model.viewCommentActionEnabled = false model.viewCommentActionEnabled = false
model.viewCertificatesActionEnabled = false
return return
} }
@@ -94,6 +98,9 @@ class BrowseView {
} }
boolean downloadActionEnabled = true boolean downloadActionEnabled = true
model.viewCertificatesActionEnabled = (rows.length == 1 && model.results[rows[0]].certificates > 0)
if (rows.length == 1 && model.results[rows[0]].comment != null) if (rows.length == 1 && model.results[rows[0]].comment != null)
model.viewCommentActionEnabled = true model.viewCommentActionEnabled = true
else else
@@ -104,16 +111,16 @@ class BrowseView {
} }
model.downloadActionEnabled = downloadActionEnabled model.downloadActionEnabled = downloadActionEnabled
resultsTable.addMouseListener(new MouseAdapter() { })
public void mouseReleased(MouseEvent e) { resultsTable.addMouseListener(new MouseAdapter() {
if (e.isPopupTrigger()) public void mouseReleased(MouseEvent e) {
showMenu(e) if (e.isPopupTrigger())
} showMenu(e)
public void mousePressed(MouseEvent e) { }
if (e.isPopupTrigger()) public void mousePressed(MouseEvent e) {
showMenu(e) if (e.isPopupTrigger())
} showMenu(e)
}) }
}) })
} }
@@ -129,6 +136,11 @@ class BrowseView {
viewComment.addActionListener({controller.viewComment()}) viewComment.addActionListener({controller.viewComment()})
menu.add(viewComment) menu.add(viewComment)
} }
if (model.viewCertificatesActionEnabled) {
JMenuItem viewCertificates = new JMenuItem("View Certificates")
viewCertificates.addActionListener({controller.viewCertificates()})
menu.add(viewCertificates)
}
JMenuItem copyHash = new JMenuItem("Copy Hash To Clipboard") JMenuItem copyHash = new JMenuItem("Copy Hash To Clipboard")
copyHash.addActionListener({ copyHash.addActionListener({

View File

@@ -0,0 +1,155 @@
package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import net.i2p.data.Base64
import javax.swing.JDialog
import javax.swing.JMenuItem
import javax.swing.JPopupMenu
import javax.swing.ListSelectionModel
import javax.swing.SwingConstants
import com.muwire.core.Persona
import com.muwire.core.filecert.Certificate
import java.awt.BorderLayout
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class CertificateControlView {
@MVCMember @Nonnull
FactoryBuilderSupport builder
@MVCMember @Nonnull
CertificateControlModel model
@MVCMember @Nonnull
CertificateControlController controller
def mainFrame
def dialog
def panel
def usersTable
def certsTable
def lastUsersSortEvent
def lastCertsSortEvent
void initUI() {
mainFrame = application.windowManager.findWindow("main-frame")
int rowHeight = application.context.get("row-height")
dialog = new JDialog(mainFrame,"Certificates",true)
dialog.setResizable(true)
panel = builder.panel {
borderLayout()
panel(constraints : BorderLayout.NORTH) {
label("Certificates in your repository")
}
panel (constraints : BorderLayout.CENTER) {
gridLayout(rows : 1, cols : 2)
scrollPane {
usersTable = table(autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.users) {
closureColumn(header : "Issuer", type : String, read : {it.getHumanReadableName()})
}
}
}
scrollPane {
certsTable = table(autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.certificates) {
closureColumn(header : "File Name", type : String, read : {it.name.name})
closureColumn(header : "Hash", type : String, read : {Base64.encode(it.infoHash.getRoot())})
closureColumn(header : "Comment", preferredWidth : 20, type : Boolean, read : {it.comment != null})
closureColumn(header : "Timestamp", type : Long, read : { it.timestamp })
}
}
}
}
panel (constraints : BorderLayout.SOUTH) {
button(text : "Show Comment", enabled : bind {model.showCommentActionEnabled}, showCommentAction)
}
}
}
void mvcGroupInit(Map<String,String> args) {
usersTable.rowSorter.addRowSorterListener({evt -> lastUsersSortEvent = evt})
def selectionModel = usersTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
selectionModel.addListSelectionListener({
Persona issuer = getSelectedIssuer()
if (issuer == null)
return
Set<Certificate> certs = model.core.certificateManager.byIssuer.get(issuer)
if (certs == null)
return
model.certificates.clear()
model.certificates.addAll(certs)
certsTable.model.fireTableDataChanged()
})
certsTable.rowSorter.addRowSorterListener({evt -> lastCertsSortEvent = evt})
selectionModel = certsTable.getSelectionModel()
selectionModel.addListSelectionListener({
Certificate c = getSelectedSertificate()
model.showCommentActionEnabled = c != null && c.comment != null
})
certsTable.addMouseListener(new MouseAdapter() {
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger())
showMenu(e)
}
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger())
showMenu(e)
}
})
certsTable.setDefaultRenderer(Long.class, new DateRenderer())
dialog.getContentPane().add(panel)
dialog.pack()
dialog.setLocationRelativeTo(mainFrame)
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
dialog.addWindowListener(new WindowAdapter() {
public void windowClosed(WindowEvent e) {
mvcGroup.destroy()
}
})
dialog.show()
}
private Persona getSelectedIssuer() {
int selectedRow = usersTable.getSelectedRow()
if (selectedRow < 0)
return null
if (lastUsersSortEvent != null)
selectedRow = usersTable.rowSorter.convertRowIndexToModel(selectedRow)
model.users[selectedRow]
}
Certificate getSelectedSertificate() {
int [] selectedRows = certsTable.getSelectedRows()
if (selectedRows.length != 1)
return null
if (lastCertsSortEvent != null)
selectedRows[0] = certsTable.rowSorter.convertRowIndexToModel(selectedRows[0])
model.certificates[selectedRows[0]]
}
private void showMenu(MouseEvent e) {
if (!model.showCommentActionEnabled)
return
JPopupMenu menu = new JPopupMenu()
JMenuItem showComment = new JMenuItem("Show Comment")
showComment.addActionListener({controller.showComment()})
menu.add(showComment)
menu.show(e.getComponent(), e.getX(), e.getY())
}
}

View File

@@ -0,0 +1,59 @@
package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.swing.JDialog
import javax.swing.SwingConstants
import java.awt.GridBagConstraints
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class CertificateWarningView {
@MVCMember @Nonnull
FactoryBuilderSupport builder
def mainFrame
def dialog
def panel
def checkbox
void initUI() {
mainFrame = application.windowManager.findWindow("main-frame")
dialog = new JDialog(mainFrame, "Certificate Warning", true)
panel = builder.panel {
gridBagLayout()
label(text : "When you certify a file, you create a proof that you shared this file.", constraints :gbc(gridx: 0, gridy : 0, gridwidth : 2))
label(text : "Even if you delete the certificate from your disk, others may already have it.", constraints : gbc(gridx:0, gridy : 1, gridwidth: 2))
label(text : "If you are sure you want to do this, check the checkbox below, then click \"Certify\" again.", constraints : gbc(gridx:0, gridy: 2, gridwidth:2))
label(text : "\n", constraints : gbc(gridx:0, gridy:3)) // TODO: real padding
label(text : " I understand, do not show this warning again", constraints : gbc(gridx:0, gridy:4, anchor : GridBagConstraints.LINE_END))
checkbox = checkBox(constraints : gbc(gridx:1, gridy:4, anchor : GridBagConstraints.LINE_START))
panel (constraints : gbc(gridx :0, gridy : 5, gridwidth : 2)) {
button(text : "Ok", dismissAction)
}
}
dialog.getContentPane().add(panel)
dialog.pack()
dialog.setResizable(false)
dialog.setLocationRelativeTo(mainFrame)
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
dialog.addWindowListener(new WindowAdapter() {
public void windowClosed(WindowEvent e) {
dialog.setVisible(false)
mvcGroup.destroy()
}
})
}
void mvcGroupInit(Map<String,String> args) {
dialog.show()
}
}

View File

@@ -0,0 +1,61 @@
package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.swing.Box
import javax.swing.JDialog
import javax.swing.JOptionPane
import javax.swing.SwingConstants
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class DownloadPreviewView {
@MVCMember @Nonnull
FactoryBuilderSupport builder
@MVCMember @Nonnull
DownloadPreviewModel model
def mainFrame
def dialog
def panel
void initUI() {
mainFrame = application.windowManager.findWindow("main-frame")
dialog = new JDialog(mainFrame, "Generating Preview", true)
panel = builder.panel {
vbox {
label(text : "Generating preview for "+model.downloader.file.getName())
Box.createVerticalGlue()
progressBar(indeterminate : true)
}
}
dialog.getContentPane().add(panel)
dialog.pack()
dialog.setResizable(false)
dialog.setLocationRelativeTo(mainFrame)
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
dialog.addWindowListener(new WindowAdapter() {
public void windowClosed(WindowEvent e) {
mainFrame.setVisible(false)
mvcGroup.destroy()
}
})
}
void mvcGroupInit(Map<String, String> args) {
if (!model.downloader.isSequential())
JOptionPane.showMessageDialog(mainFrame, "This download is not sequential, there may not be much to preview")
DownloadPreviewer previewer = new DownloadPreviewer(model.downloader, this)
previewer.execute()
dialog.show()
}
}

View File

@@ -0,0 +1,147 @@
package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.swing.JDialog
import javax.swing.JMenuItem
import javax.swing.JPopupMenu
import javax.swing.ListSelectionModel
import javax.swing.SwingConstants
import com.muwire.core.filecert.Certificate
import java.awt.BorderLayout
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class FetchCertificatesView {
@MVCMember @Nonnull
FactoryBuilderSupport builder
@MVCMember @Nonnull
FetchCertificatesModel model
@MVCMember @Nonnull
FetchCertificatesController controller
def mainFrame
def dialog
def p
def certsTable
def lastSortEvent
void initUI() {
int rowHeight = application.context.get("row-height")
mainFrame = application.windowManager.findWindow("main-frame")
dialog = new JDialog(mainFrame, model.result.name, true)
dialog.setResizable(true)
p = builder.panel {
borderLayout()
panel(constraints : BorderLayout.NORTH) {
label(text : "Status:")
label(text : bind {model.status.toString()})
label(text : bind {model.certificateCount == 0 ? "" : Math.round(model.certificateCount * 100 / model.totalCertificates)+"%"})
}
scrollPane(constraints : BorderLayout.CENTER) {
certsTable = table(autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.certificates) {
closureColumn(header : "Issuer", preferredWidth : 200, type : String, read : {it.issuer.getHumanReadableName()})
closureColumn(header : "Trust Status", preferredWidth: 50, type : String, read : {controller.core.trustService.getLevel(it.issuer.destination)})
closureColumn(header : "Name", preferredWidth : 200, type: String, read : {it.name.name.toString()})
closureColumn(header : "Issued", preferredWidth : 100, type : String, read : {
def date = new Date(it.timestamp)
date.toString()
})
closureColumn(header : "Comments", preferredWidth: 20, type : Boolean, read :{it.comment != null})
}
}
}
panel(constraints : BorderLayout.SOUTH) {
button(text : "Import", enabled : bind {model.importActionEnabled}, importCertificatesAction)
button(text : "Show Comment", enabled : bind {model.showCommentActionEnabled}, showCommentAction)
button(text : "Dismiss", dismissAction)
}
}
certsTable.rowSorter.addRowSorterListener({evt -> lastSortEvent = evt})
certsTable.rowSorter.setSortsOnUpdates(true)
def selectionModel = certsTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION)
selectionModel.addListSelectionListener({
int[] rows = certsTable.getSelectedRows()
model.importActionEnabled = rows.length > 0
if (rows.length == 1) {
if (lastSortEvent != null)
rows[0] = certsTable.rowSorter.convertRowIndexToModel(rows[0])
model.showCommentActionEnabled = model.certificates[rows[0]].comment != null
} else
model.showCommentActionEnabled = false
})
certsTable.addMouseListener(new MouseAdapter() {
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger())
showMenu(e)
}
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger())
showMenu(e)
}
})
}
private void showMenu(MouseEvent e) {
JPopupMenu menu = new JPopupMenu()
JMenuItem importItem = new JMenuItem("Import")
importItem.addActionListener({controller.importCertificates()})
menu.add(importItem)
if (model.showCommentActionEnabled) {
JMenuItem showComment = new JMenuItem("Show Comment")
showComment.addActionListener({controller.showComment()})
menu.add(showComment)
}
menu.show(e.getComponent(), e.getX(), e.getY())
}
def selectedCertificates() {
int [] rows = certsTable.getSelectedRows()
if (rows.length == 0)
return null
if (lastSortEvent != null) {
for(int i = 0; i< rows.length; i++) {
rows[i] = certsTable.rowSorter.convertRowIndexToModel(rows[i])
}
}
List<Certificate> rv = new ArrayList<>()
for (Integer i : rows)
rv << model.certificates[i]
rv
}
void mvcGroupInit(Map<String,String> args) {
controller.register()
dialog.getContentPane().add(p)
dialog.setSize(700, 400)
dialog.setLocationRelativeTo(mainFrame)
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
dialog.addWindowListener( new WindowAdapter() {
public void windowClosed(WindowEvent e) {
mvcGroup.destroy()
}
})
dialog.show()
}
}

View File

@@ -46,6 +46,7 @@ import java.awt.FlowLayout
import java.awt.GridBagConstraints import java.awt.GridBagConstraints
import java.awt.GridBagLayout import java.awt.GridBagLayout
import java.awt.Insets import java.awt.Insets
import java.awt.Rectangle
import java.awt.Toolkit import java.awt.Toolkit
import java.awt.datatransfer.DataFlavor import java.awt.datatransfer.DataFlavor
import java.awt.datatransfer.StringSelection import java.awt.datatransfer.StringSelection
@@ -70,6 +71,7 @@ class MainFrameView {
def downloadsTable def downloadsTable
def lastDownloadSortEvent def lastDownloadSortEvent
def lastUploadsSortEvent
def lastSharedSortEvent def lastSharedSortEvent
def trustTablesSortEvents = [:] def trustTablesSortEvents = [:]
def expansionListener = new TreeExpansions() def expansionListener = new TreeExpansions()
@@ -122,6 +124,11 @@ class MainFrameView {
env["core"] = model.core env["core"] = model.core
mvcGroup.createMVCGroup("advanced-sharing",env) mvcGroup.createMVCGroup("advanced-sharing",env)
}) })
menuItem("Certificates", actionPerformed : {
def env = [:]
env['core'] = model.core
mvcGroup.createMVCGroup("certificate-control",env)
})
} }
} }
borderLayout() borderLayout()
@@ -209,6 +216,7 @@ class MainFrameView {
button(text: "Pause", enabled : bind {model.pauseButtonEnabled}, pauseAction) button(text: "Pause", enabled : bind {model.pauseButtonEnabled}, pauseAction)
button(text: bind { model.resumeButtonText }, enabled : bind {model.retryButtonEnabled}, resumeAction) button(text: bind { model.resumeButtonText }, enabled : bind {model.retryButtonEnabled}, resumeAction)
button(text: "Cancel", enabled : bind {model.cancelButtonEnabled }, cancelAction) button(text: "Cancel", enabled : bind {model.cancelButtonEnabled }, cancelAction)
button(text: "Preview", enabled : bind {model.previewButtonEnabled}, previewAction)
button(text: "Clear Done", enabled : bind {model.clearButtonEnabled}, clearAction) button(text: "Clear Done", enabled : bind {model.clearButtonEnabled}, clearAction)
} }
} }
@@ -269,6 +277,10 @@ class MainFrameView {
closureColumn(header : "Name", preferredWidth : 500, type : String, read : {row -> row.getCachedPath()}) closureColumn(header : "Name", preferredWidth : 500, type : String, read : {row -> row.getCachedPath()})
closureColumn(header : "Size", preferredWidth : 50, type : Long, read : {row -> row.getCachedLength() }) closureColumn(header : "Size", preferredWidth : 50, type : Long, read : {row -> row.getCachedLength() })
closureColumn(header : "Comments", preferredWidth : 50, type : Boolean, read : {it.getComment() != null}) closureColumn(header : "Comments", preferredWidth : 50, type : Boolean, read : {it.getComment() != null})
closureColumn(header : "Certified", preferredWidth : 50, type : Boolean, read : {
Core core = application.context.get("core")
core.certificateManager.hasLocalCertificate(it.getInfoHash())
})
closureColumn(header : "Search Hits", preferredWidth: 50, type : Integer, read : {it.getHits()}) closureColumn(header : "Search Hits", preferredWidth: 50, type : Integer, read : {it.getHits()})
closureColumn(header : "Downloaders", preferredWidth: 50, type : Integer, read : {it.getDownloaders().size()}) closureColumn(header : "Downloaders", preferredWidth: 50, type : Integer, read : {it.getDownloaders().size()})
} }
@@ -295,6 +307,7 @@ class MainFrameView {
panel { panel {
button(text : "Share files", actionPerformed : shareFiles) button(text : "Share files", actionPerformed : shareFiles)
button(text : "Add Comment", enabled : bind {model.addCommentButtonEnabled}, addCommentAction) button(text : "Add Comment", enabled : bind {model.addCommentButtonEnabled}, addCommentAction)
button(text : "Certify", enabled : bind {model.addCommentButtonEnabled}, issueCertificateAction)
} }
panel { panel {
panel { panel {
@@ -310,7 +323,7 @@ class MainFrameView {
label("Uploads") label("Uploads")
} }
scrollPane (constraints : BorderLayout.CENTER) { scrollPane (constraints : BorderLayout.CENTER) {
table(id : "uploads-table", rowHeight : rowHeight) { table(id : "uploads-table", autoCreateRowSorter: true, rowHeight : rowHeight) {
tableModel(list : model.uploads) { tableModel(list : model.uploads) {
closureColumn(header : "Name", type : String, read : {row -> row.uploader.getName() }) closureColumn(header : "Name", type : String, read : {row -> row.uploader.getName() })
closureColumn(header : "Progress", type : String, read : { row -> closureColumn(header : "Progress", type : String, read : { row ->
@@ -410,7 +423,8 @@ class MainFrameView {
scrollPane(constraints : BorderLayout.CENTER) { scrollPane(constraints : BorderLayout.CENTER) {
table(id : "trusted-table", autoCreateRowSorter : true, rowHeight : rowHeight) { table(id : "trusted-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.trusted) { tableModel(list : model.trusted) {
closureColumn(header : "Trusted Users", type : String, read : { it.getHumanReadableName() } ) closureColumn(header : "Trusted Users", type : String, read : { it.persona.getHumanReadableName() } )
closureColumn(header : "Reason", type : String, read : {it.reason})
} }
} }
} }
@@ -426,7 +440,8 @@ class MainFrameView {
scrollPane(constraints : BorderLayout.CENTER) { scrollPane(constraints : BorderLayout.CENTER) {
table(id : "distrusted-table", autoCreateRowSorter : true, rowHeight : rowHeight) { table(id : "distrusted-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.distrusted) { tableModel(list : model.distrusted) {
closureColumn(header: "Distrusted Users", type : String, read : { it.getHumanReadableName() } ) closureColumn(header: "Distrusted Users", type : String, read : { it.persona.getHumanReadableName() } )
closureColumn(header: "Reason", type : String, read : {it.reason})
} }
} }
} }
@@ -449,12 +464,7 @@ class MainFrameView {
closureColumn(header : "Trusted", preferredWidth : 20, type: Integer, read : {it.good.size()}) closureColumn(header : "Trusted", preferredWidth : 20, type: Integer, read : {it.good.size()})
closureColumn(header : "Distrusted", preferredWidth: 20, type: Integer, read : {it.bad.size()}) closureColumn(header : "Distrusted", preferredWidth: 20, type: Integer, read : {it.bad.size()})
closureColumn(header : "Status", preferredWidth: 30, type: String, read : {it.status.toString()}) closureColumn(header : "Status", preferredWidth: 30, type: String, read : {it.status.toString()})
closureColumn(header : "Last Updated", preferredWidth: 200, type : String, read : { closureColumn(header : "Last Updated", preferredWidth: 200, type : Long, read : { it.timestamp })
if (it.timestamp == 0)
return "Never"
else
return String.valueOf(new Date(it.timestamp))
})
} }
} }
} }
@@ -527,6 +537,7 @@ class MainFrameView {
model.cancelButtonEnabled = false model.cancelButtonEnabled = false
model.retryButtonEnabled = false model.retryButtonEnabled = false
model.pauseButtonEnabled = false model.pauseButtonEnabled = false
model.previewButtonEnabled = false
model.downloader = null model.downloader = null
downloadDetailsPanel.getLayout().show(downloadDetailsPanel,"select-download") downloadDetailsPanel.getLayout().show(downloadDetailsPanel,"select-download")
return return
@@ -535,6 +546,7 @@ class MainFrameView {
if (downloader == null) if (downloader == null)
return return
model.downloader = downloader model.downloader = downloader
model.previewButtonEnabled = true
downloadDetailsPanel.getLayout().show(downloadDetailsPanel,"download-selected") downloadDetailsPanel.getLayout().show(downloadDetailsPanel,"download-selected")
switch(downloader.getCurrentState()) { switch(downloader.getCurrentState()) {
case Downloader.DownloadState.CONNECTING : case Downloader.DownloadState.CONNECTING :
@@ -596,6 +608,15 @@ class MainFrameView {
JMenuItem commentSelectedFiles = new JMenuItem("Comment selected files") JMenuItem commentSelectedFiles = new JMenuItem("Comment selected files")
commentSelectedFiles.addActionListener({mvcGroup.controller.addComment()}) commentSelectedFiles.addActionListener({mvcGroup.controller.addComment()})
sharedFilesMenu.add(commentSelectedFiles) sharedFilesMenu.add(commentSelectedFiles)
JMenuItem certifySelectedFiles = new JMenuItem("Certify selected files")
certifySelectedFiles.addActionListener({mvcGroup.controller.issueCertificate()})
sharedFilesMenu.add(certifySelectedFiles)
JMenuItem openContainingFolder = new JMenuItem("Open containing folder")
openContainingFolder.addActionListener({mvcGroup.controller.openContainingFolder()})
sharedFilesMenu.add(openContainingFolder)
JMenuItem showFileDetails = new JMenuItem("Show file details")
showFileDetails.addActionListener({mvcGroup.controller.showFileDetails()})
sharedFilesMenu.add(showFileDetails)
def sharedFilesMouseListener = new MouseAdapter() { def sharedFilesMouseListener = new MouseAdapter() {
@Override @Override
@@ -637,6 +658,29 @@ class MainFrameView {
sharedFilesTree.addTreeExpansionListener(expansionListener) sharedFilesTree.addTreeExpansionListener(expansionListener)
// uploadsTable
def uploadsTable = builder.getVariable("uploads-table")
uploadsTable.rowSorter.addRowSorterListener({evt -> lastUploadsSortEvent = evt})
uploadsTable.rowSorter.setSortsOnUpdates(true)
selectionModel = uploadsTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
JPopupMenu uploadsTableMenu = new JPopupMenu()
JMenuItem showInLibrary = new JMenuItem("Show in library")
showInLibrary.addActionListener({mvcGroup.controller.showInLibrary()})
uploadsTableMenu.add(showInLibrary)
uploadsTable.addMouseListener(new MouseAdapter() {
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger())
showPopupMenu(uploadsTableMenu, e)
}
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger())
showPopupMenu(uploadsTableMenu, e)
}
})
// searches table // searches table
def searchesTable = builder.getVariable("searches-table") def searchesTable = builder.getVariable("searches-table")
JPopupMenu searchTableMenu = new JPopupMenu() JPopupMenu searchTableMenu = new JPopupMenu()
@@ -702,6 +746,8 @@ class MainFrameView {
break break
} }
}) })
subscriptionTable.setDefaultRenderer(Long.class, new DateRenderer())
// trusted table // trusted table
def trustedTable = builder.getVariable("trusted-table") def trustedTable = builder.getVariable("trusted-table")
@@ -889,6 +935,36 @@ class MainFrameView {
showPopupMenu(menu, e) showPopupMenu(menu, e)
} }
def selectedUploader() {
def uploadsTable = builder.getVariable("uploads-table")
int selectedRow = uploadsTable.getSelectedRow()
if (selectedRow < 0)
return null
if (lastUploadsSortEvent != null)
selectedRow = uploadsTable.rowSorter.convertRowIndexToModel(selectedRow)
model.uploads[selectedRow].uploader
}
void focusOnSharedFile(SharedFile sf) {
if(model.treeVisible) {
def tree = builder.getVariable("shared-files-tree")
def node = model.fileToNode.get(sf)
if (node == null)
return
def path = new TreePath(node.getPath())
tree.setSelectionPath(path)
tree.scrollPathToVisible(path)
} else {
def table = builder.getVariable("shared-files-table")
int row = model.shared.indexOf(sf)
if (row < 0)
return
table.setRowSelectionInterval(row, row)
table.scrollRectToVisible(new Rectangle(table.getCellRect(row, 0, true)))
}
}
void showRestoreOrEmpty() { void showRestoreOrEmpty() {
def searchWindow = builder.getVariable("search window") def searchWindow = builder.getVariable("search window")
String id String id

View File

@@ -200,10 +200,16 @@ class OptionsView {
panel (border : titledBorder(title : "Search Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP), panel (border : titledBorder(title : "Search Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP),
constraints : gbc(gridx : 0, gridy : 1, fill : GridBagConstraints.HORIZONTAL, weightx : 100)) { constraints : gbc(gridx : 0, gridy : 1, fill : GridBagConstraints.HORIZONTAL, weightx : 100)) {
gridBagLayout() gridBagLayout()
label(text : "Remember search history", constraints: gbc(gridx: 0, gridy:0, anchor : GridBagConstraints.LINE_START, weightx: 100)) label(text : "By default, group search results by", constraints : gbc(gridx :0, gridy: 0, anchor : GridBagConstraints.LINE_START, weightx : 100))
panel(constraints : gbc(gridx : 1, gridy : 0, anchor : GridBagConstraints.LINE_END)) {
buttonGroup(id : "groupBy")
radioButton(text : "Sender", selected : bind {!model.groupByFile}, buttonGroup : groupBy, groupBySenderAction)
radioButton(text : "File", selected : bind {model.groupByFile}, buttonGroup : groupBy, groupByFileAction)
}
label(text : "Remember search history", constraints: gbc(gridx: 0, gridy:1, anchor : GridBagConstraints.LINE_START, weightx: 100))
storeSearchHistoryCheckbox = checkBox(selected : bind {model.storeSearchHistory}, storeSearchHistoryCheckbox = checkBox(selected : bind {model.storeSearchHistory},
constraints : gbc(gridx : 1, gridy:0, anchor : GridBagConstraints.LINE_END)) constraints : gbc(gridx : 1, gridy:1, anchor : GridBagConstraints.LINE_END))
button(text : "Clear history", constraints : gbc(gridx : 1, gridy : 1, anchor : GridBagConstraints.LINE_END), clearHistoryAction) button(text : "Clear history", constraints : gbc(gridx : 1, gridy : 2, anchor : GridBagConstraints.LINE_END), clearHistoryAction)
} }
panel (border : titledBorder(title : "Other Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP), panel (border : titledBorder(title : "Other Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP),
@@ -221,7 +227,7 @@ class OptionsView {
label(text : "Exclude local files from results", constraints: gbc(gridx:0, gridy:3, anchor : GridBagConstraints.LINE_START, weightx: 100)) label(text : "Exclude local files from results", constraints: gbc(gridx:0, gridy:3, anchor : GridBagConstraints.LINE_START, weightx: 100))
excludeLocalResultCheckbox = checkBox(selected : bind {model.excludeLocalResult}, excludeLocalResultCheckbox = checkBox(selected : bind {model.excludeLocalResult},
constraints : gbc(gridx: 1, gridy : 3, anchor : GridBagConstraints.LINE_END)) constraints : gbc(gridx: 1, gridy : 3, anchor : GridBagConstraints.LINE_END))
label(text : "Automatically Clear finished uploads", constraints:gbc(gridx:0, gridy:4, anchor: GridBagConstraints.LINE_START, weightx : 100)) label(text : "Automatically clear finished uploads", constraints:gbc(gridx:0, gridy:4, anchor: GridBagConstraints.LINE_START, weightx : 100))
clearUploadsCheckbox = checkBox(selected : bind {model.clearUploads}, clearUploadsCheckbox = checkBox(selected : bind {model.clearUploads},
constraints : gbc(gridx:1, gridy: 4, anchor:GridBagConstraints.LINE_END)) constraints : gbc(gridx:1, gridy: 4, anchor:GridBagConstraints.LINE_END))
label(text : "When closing MuWire", constraints : gbc(gridx: 0, gridy : 5, anchor : GridBagConstraints.LINE_START, weightx: 100)) label(text : "When closing MuWire", constraints : gbc(gridx: 0, gridy : 5, anchor : GridBagConstraints.LINE_START, weightx: 100))

View File

@@ -17,6 +17,7 @@ import javax.swing.ListSelectionModel
import javax.swing.SwingConstants import javax.swing.SwingConstants
import javax.swing.table.DefaultTableCellRenderer import javax.swing.table.DefaultTableCellRenderer
import com.muwire.core.InfoHash
import com.muwire.core.Persona import com.muwire.core.Persona
import com.muwire.core.search.UIResultEvent import com.muwire.core.search.UIResultEvent
import com.muwire.core.util.DataUtil import com.muwire.core.util.DataUtil
@@ -38,80 +39,175 @@ class SearchTabView {
FactoryBuilderSupport builder FactoryBuilderSupport builder
@MVCMember @Nonnull @MVCMember @Nonnull
SearchTabModel model SearchTabModel model
UISettings settings
def pane def pane
def parent def parent
def searchTerms def searchTerms
def sendersTable def sendersTable, sendersTable2
def lastSendersSortEvent def lastSendersSortEvent
def resultsTable def resultsTable, resultsTable2
def lastSortEvent def lastSortEvent
def lastResults2SortEvent, lastSenders2SortEvent
def sequentialDownloadCheckbox def sequentialDownloadCheckbox
def sequentialDownloadCheckbox2
void initUI() { void initUI() {
int rowHeight = application.context.get("row-height") int rowHeight = application.context.get("row-height")
builder.with { builder.with {
def resultsTable def resultsTable, resultsTable2
def sendersTable def sendersTable, sendersTable2
def sequentialDownloadCheckbox def sequentialDownloadCheckbox, sequentialDownloadCheckbox2
def pane = panel { def pane = panel {
gridLayout(rows :1, cols : 1) borderLayout()
splitPane(orientation: JSplitPane.VERTICAL_SPLIT, continuousLayout : true, dividerLocation: 300 ) { panel (id : "results-panel", constraints : BorderLayout.CENTER) {
panel { cardLayout()
borderLayout() panel (constraints : "grouped-by-sender"){
scrollPane (constraints : BorderLayout.CENTER) { gridLayout(rows :1, cols : 1)
sendersTable = table(id : "senders-table", autoCreateRowSorter : true, rowHeight : rowHeight) { splitPane(orientation: JSplitPane.VERTICAL_SPLIT, continuousLayout : true, dividerLocation: 300 ) {
tableModel(list : model.senders) { panel {
closureColumn(header : "Sender", preferredWidth : 500, type: String, read : {row -> row.getHumanReadableName()}) borderLayout()
closureColumn(header : "Results", preferredWidth : 20, type: Integer, read : {row -> model.sendersBucket[row].size()}) scrollPane (constraints : BorderLayout.CENTER) {
closureColumn(header : "Browse", preferredWidth : 20, type: Boolean, read : {row -> model.sendersBucket[row].first().browse}) sendersTable = table(id : "senders-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
closureColumn(header : "Trust", preferredWidth : 50, type: String, read : { row -> tableModel(list : model.senders) {
model.core.trustService.getLevel(row.destination).toString() closureColumn(header : "Sender", preferredWidth : 500, type: String, read : {row -> row.getHumanReadableName()})
}) closureColumn(header : "Results", preferredWidth : 20, type: Integer, read : {row -> model.sendersBucket[row].size()})
closureColumn(header : "Browse", preferredWidth : 20, type: Boolean, read : {row -> model.sendersBucket[row].first().browse})
closureColumn(header : "Trust", preferredWidth : 50, type: String, read : { row ->
model.core.trustService.getLevel(row.destination).toString()
})
}
}
}
panel(constraints : BorderLayout.SOUTH) {
gridLayout(rows: 1, cols : 2)
panel (border : etchedBorder()){
button(text : "Browse Host", enabled : bind {model.browseActionEnabled}, browseAction)
}
panel (border : etchedBorder()){
button(text : "Trust", enabled: bind {model.trustButtonsEnabled }, trustAction)
button(text : "Neutral", enabled: bind {model.trustButtonsEnabled}, neutralAction)
button(text : "Distrust", enabled : bind {model.trustButtonsEnabled}, distrustAction)
}
}
}
panel {
borderLayout()
scrollPane (constraints : BorderLayout.CENTER) {
resultsTable = table(id : "results-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list: model.results) {
closureColumn(header: "Name", preferredWidth: 350, type: String, read : {row -> row.name.replace('<','_')})
closureColumn(header: "Size", preferredWidth: 20, type: Long, read : {row -> row.size})
closureColumn(header: "Direct Sources", preferredWidth: 50, type : Integer, read : { row -> model.hashBucket[row.infohash].size()})
closureColumn(header: "Possible Sources", preferredWidth : 50, type : Integer, read : {row -> model.sourcesBucket[row.infohash].size()})
closureColumn(header: "Comments", preferredWidth: 20, type: Boolean, read : {row -> row.comment != null})
closureColumn(header: "Certificates", preferredWidth: 20, type: Integer, read : {row -> row.certificates})
}
}
}
panel(constraints : BorderLayout.SOUTH) {
gridBagLayout()
label(text : "", constraints : gbc(gridx : 0, gridy: 0, weightx : 100))
button(text : "Download", enabled : bind {model.downloadActionEnabled}, constraints : gbc(gridx : 1, gridy:0), downloadAction)
button(text : "View Comment", enabled : bind {model.viewCommentActionEnabled}, constraints : gbc(gridx:2, gridy:0), showCommentAction)
button(text : "View Certificates", enabled : bind {model.viewCertificatesActionEnabled}, constraints : gbc(gridx:3, gridy:0), viewCertificatesAction)
label(text : "Download sequentially", constraints : gbc(gridx: 4, gridy: 0, weightx : 80, anchor : GridBagConstraints.LINE_END))
sequentialDownloadCheckbox = checkBox(constraints : gbc(gridx : 5, gridy: 0, anchor : GridBagConstraints.LINE_END),
selected : false, enabled : bind {model.downloadActionEnabled})
} }
} }
} }
panel(constraints : BorderLayout.SOUTH) {
gridLayout(rows: 1, cols : 2)
panel (border : etchedBorder()){
button(text : "Browse Host", enabled : bind {model.browseActionEnabled}, browseAction)
}
panel (border : etchedBorder()){
button(text : "Trust", enabled: bind {model.trustButtonsEnabled }, trustAction)
button(text : "Neutral", enabled: bind {model.trustButtonsEnabled}, neutralAction)
button(text : "Distrust", enabled : bind {model.trustButtonsEnabled}, distrustAction)
}
}
} }
panel { panel (constraints : "grouped-by-file") {
borderLayout() gridLayout(rows : 1, cols : 1)
scrollPane (constraints : BorderLayout.CENTER) { splitPane(orientation: JSplitPane.VERTICAL_SPLIT, continuousLayout : true, dividerLocation: 300 ) {
resultsTable = table(id : "results-table", autoCreateRowSorter : true, rowHeight : rowHeight) { panel {
tableModel(list: model.results) { borderLayout()
closureColumn(header: "Name", preferredWidth: 350, type: String, read : {row -> row.name.replace('<','_')}) scrollPane(constraints : BorderLayout.CENTER) {
closureColumn(header: "Size", preferredWidth: 20, type: Long, read : {row -> row.size}) resultsTable2 = table(id : "results-table2", autoCreateRowSorter : true, rowHeight : rowHeight) {
closureColumn(header: "Direct Sources", preferredWidth: 50, type : Integer, read : { row -> model.hashBucket[row.infohash].size()}) tableModel(list : model.results2) {
closureColumn(header: "Possible Sources", preferredWidth : 50, type : Integer, read : {row -> model.sourcesBucket[row.infohash].size()}) closureColumn(header : "Name", preferredWidth : 350, type : String, read : {
closureColumn(header: "Comments", preferredWidth: 20, type: Boolean, read : {row -> row.comment != null}) model.hashBucket[it].first().name.replace('<', '_')
})
closureColumn(header : "Size", preferredWidth : 20, type : Long, read : {
model.hashBucket[it].first().size
})
closureColumn(header : "Direct Sources", preferredWidth : 20, type : Integer, read : {
model.hashBucket[it].size()
})
closureColumn(header : "Possible Sources", preferredWidth : 20, type : Integer , read : {
model.sourcesBucket[it].size()
})
closureColumn(header : "Comments", preferredWidth : 20, type : Integer, read : {
int count = 0
model.hashBucket[it].each {
if (it.comment != null)
count++
}
count
})
closureColumn(header : "Certificates", preferredWidth : 20, type : Integer, read : {
int count = 0
model.hashBucket[it].each {
count += it.certificates
}
count
})
}
}
}
panel (constraints : BorderLayout.SOUTH) {
gridLayout(rows :1, cols : 3)
panel {}
panel {
button(text : "Download", enabled : bind {model.downloadActionEnabled}, downloadAction)
}
panel {
gridBagLayout()
label(text : "Download sequentially", constraints : gbc(gridx : 0, gridy : 0, weightx : 100, anchor : GridBagConstraints.LINE_END))
sequentialDownloadCheckbox2 = checkBox( constraints : gbc(gridx: 1, gridy:0, weightx: 0, anchor : GridBagConstraints.LINE_END))
}
}
}
panel {
borderLayout()
scrollPane(constraints : BorderLayout.CENTER) {
sendersTable2 = table(id : "senders-table2", autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.senders2) {
closureColumn(header : "Sender", preferredWidth : 350, type : String, read : {it.sender.getHumanReadableName()})
closureColumn(header : "Browse", preferredWidth : 20, type : Boolean, read : {it.browse})
closureColumn(header : "Comment", preferredWidth : 20, type : Boolean, read : {it.comment != null})
closureColumn(header : "Certificates", preferredWidth : 20, type: Integer, read : {it.certificates})
closureColumn(header : "Trust", preferredWidth : 50, type : String, read : {
model.core.trustService.getLevel(it.sender.destination).toString()
})
}
}
}
panel (constraints : BorderLayout.SOUTH) {
gridLayout(rows : 1, cols : 2)
panel (border : etchedBorder()) {
button(text : "Browse Host", enabled : bind {model.browseActionEnabled}, browseAction)
button(text : "View Comment", enabled : bind {model.viewCommentActionEnabled}, showCommentAction)
button(text : "View Certificates", enabled : bind {model.viewCertificatesActionEnabled}, viewCertificatesAction)
}
panel (border : etchedBorder()) {
button(text : "Trust", enabled: bind {model.trustButtonsEnabled }, trustAction)
button(text : "Neutral", enabled: bind {model.trustButtonsEnabled}, neutralAction)
button(text : "Distrust", enabled : bind {model.trustButtonsEnabled}, distrustAction)
}
} }
} }
} }
panel(constraints : BorderLayout.SOUTH) {
gridLayout(rows: 1, cols: 3)
panel()
panel {
button(text : "Download", enabled : bind {model.downloadActionEnabled}, downloadAction)
button(text : "View Comment", enabled : bind {model.viewCommentActionEnabled}, showCommentAction)
}
panel {
gridBagLayout()
panel (constraints : gbc(gridx : 0, gridy : 0, weightx : 100))
sequentialDownloadCheckbox = checkBox(constraints : gbc(gridx : 1, gridy: 0, weightx : 0),selected : false, enabled : bind {model.downloadActionEnabled})
label(constraints: gbc(gridx: 2, gridy: 0, weightx : 0),text : "Download sequentially")
}
}
} }
} }
panel (constraints : BorderLayout.SOUTH) {
label(text : "Group by")
buttonGroup(id : "groupBy")
radioButton(text : "Sender", selected : bind {!model.groupedByFile}, buttonGroup : groupBy, actionPerformed: showSenderGrouping)
radioButton(text : "File", selected : bind {model.groupedByFile}, buttonGroup : groupBy, actionPerformed: showFileGrouping)
}
} }
this.pane = pane this.pane = pane
@@ -120,7 +216,10 @@ class SearchTabView {
this.resultsTable = resultsTable this.resultsTable = resultsTable
this.sendersTable = sendersTable this.sendersTable = sendersTable
this.resultsTable2 = resultsTable2
this.sendersTable2 = sendersTable2
this.sequentialDownloadCheckbox = sequentialDownloadCheckbox this.sequentialDownloadCheckbox = sequentialDownloadCheckbox
this.sequentialDownloadCheckbox2 = sequentialDownloadCheckbox2
def selectionModel = resultsTable.getSelectionModel() def selectionModel = resultsTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION) selectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION)
@@ -196,9 +295,11 @@ class SearchTabView {
def result = getSelectedResult() def result = getSelectedResult()
if (result == null) { if (result == null) {
model.viewCommentActionEnabled = false model.viewCommentActionEnabled = false
model.viewCertificatesActionEnabled = false
return return
} else { } else {
model.viewCommentActionEnabled = result.comment != null model.viewCommentActionEnabled = result.comment != null
model.viewCertificatesActionEnabled = result.certificates > 0
} }
}) })
@@ -223,7 +324,67 @@ class SearchTabView {
resultsTable.model.fireTableDataChanged() resultsTable.model.fireTableDataChanged()
} }
}) })
// results table 2
resultsTable2.setDefaultRenderer(Integer.class,centerRenderer)
resultsTable2.columnModel.getColumn(1).setCellRenderer(new SizeRenderer())
resultsTable2.rowSorter.addRowSorterListener({evt -> lastResults2SortEvent = evt})
resultsTable2.rowSorter.setSortsOnUpdates(true)
selectionModel = resultsTable2.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
selectionModel.addListSelectionListener({
UIResultEvent e = getSelectedResult()
if (e == null) {
model.trustButtonsEnabled = false
model.browseActionEnabled = false
model.viewCertificatesActionEnabled = false
return
}
model.downloadActionEnabled = true
def results = model.hashBucket[e.infohash]
model.senders2.clear()
model.senders2.addAll(results)
int selectedRow = sendersTable2.getSelectedRow()
sendersTable2.model.fireTableDataChanged()
if (selectedRow < results.size())
sendersTable2.selectionModel.setSelectionInterval(selectedRow,selectedRow)
})
resultsTable2.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() > 1 && e.button == MouseEvent.BUTTON1)
mvcGroup.controller.download()
}
})
// TODO: add download right-click action
// senders table 2
sendersTable2.setDefaultRenderer(Integer.class, centerRenderer)
sendersTable2.rowSorter.addRowSorterListener({ evt -> lastSenders2SortEvent = evt})
sendersTable2.rowSorter.setSortsOnUpdates(true)
selectionModel = sendersTable2.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
selectionModel.addListSelectionListener({
int row = selectedSenderRow()
if (row < 0 || model.senders2[row] == null) {
model.browseActionEnabled = false
model.viewCertificatesActionEnabled = false
model.trustButtonsEnabled = false
model.viewCommentActionEnabled = false
return
}
model.browseActionEnabled = model.senders2[row].browse
model.trustButtonsEnabled = true
model.viewCommentActionEnabled = model.senders2[row].comment != null
model.viewCertificatesActionEnabled = model.senders2[row].certificates > 0
})
if (settings.groupByFile)
showFileGrouping.call()
else
showSenderGrouping.call()
} }
def closeTab = { def closeTab = {
@@ -258,19 +419,47 @@ class SearchTabView {
showComment.addActionListener({mvcGroup.controller.showComment()}) showComment.addActionListener({mvcGroup.controller.showComment()})
menu.add(showComment) menu.add(showComment)
} }
// view certificates if any
if (model.viewCertificatesActionEnabled) {
JMenuItem viewCerts = new JMenuItem("View Certificates")
viewCerts.addActionListener({mvcGroup.controller.viewCertificates()})
menu.add(viewCerts)
}
} }
if (showMenu) if (showMenu)
menu.show(e.getComponent(), e.getX(), e.getY()) menu.show(e.getComponent(), e.getX(), e.getY())
} }
private UIResultEvent getSelectedResult() { UIResultEvent getSelectedResult() {
int[] selectedRows = resultsTable.getSelectedRows() if (model.groupedByFile) {
if (selectedRows.length != 1) int selectedRow = resultsTable2.getSelectedRow()
return null if (selectedRow < 0)
int selected = selectedRows[0] return null
if (lastSortEvent != null) if (lastResults2SortEvent != null)
selected = resultsTable.rowSorter.convertRowIndexToModel(selected) selectedRow = resultsTable2.rowSorter.convertRowIndexToModel(selectedRow)
model.results[selected] InfoHash infohash = model.results2[selectedRow]
Persona sender = selectedSender()
if (sender == null) // really shouldn't happen
return model.hashBucket[infohash].first()
for (UIResultEvent candidate : model.hashBucket[infohash]) {
if (candidate.sender == sender)
return candidate
}
// also shouldn't happen
return model.hashBucket[infohash].first()
} else {
int[] selectedRows = resultsTable.getSelectedRows()
if (selectedRows.length != 1)
return null
int selected = selectedRows[0]
if (lastSortEvent != null)
selected = resultsTable.rowSorter.convertRowIndexToModel(selected)
return model.results[selected]
}
} }
def copyHashToClipboard() { def copyHashToClipboard() {
@@ -293,11 +482,49 @@ class SearchTabView {
} }
int selectedSenderRow() { int selectedSenderRow() {
int row = sendersTable.getSelectedRow() if (model.groupedByFile) {
int row = sendersTable2.getSelectedRow()
if (row < 0)
return row
if (lastSenders2SortEvent != null)
row = sendersTable2.rowSorter.convertRowIndexToModel(row)
return row
} else {
int row = sendersTable.getSelectedRow()
if (row < 0)
return -1
if (lastSendersSortEvent != null)
row = sendersTable.rowSorter.convertRowIndexToModel(row)
return row
}
}
Persona selectedSender() {
int row = selectedSenderRow()
if (row < 0) if (row < 0)
return -1 return null
if (lastSendersSortEvent != null) if (model.groupedByFile)
row = sendersTable.rowSorter.convertRowIndexToModel(row) return model.senders2[row]?.sender
row else
return model.senders[row]
}
def showSenderGrouping = {
model.groupedByFile = false
def cardsPanel = builder.getVariable("results-panel")
cardsPanel.getLayout().show(cardsPanel, "grouped-by-sender")
}
def showFileGrouping = {
model.groupedByFile = true
def cardsPanel = builder.getVariable("results-panel")
cardsPanel.getLayout().show(cardsPanel, "grouped-by-file")
}
boolean sequentialDownload() {
if (model.groupedByFile)
return sequentialDownloadCheckbox2.model.isSelected()
else
return sequentialDownloadCheckbox.model.isSelected()
} }
} }

View File

@@ -0,0 +1,154 @@
package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.swing.JDialog
import javax.swing.JMenuItem
import javax.swing.JPopupMenu
import javax.swing.JTabbedPane
import javax.swing.ListSelectionModel
import javax.swing.SwingConstants
import com.muwire.core.filecert.Certificate
import java.awt.BorderLayout
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class SharedFileView {
@MVCMember @Nonnull
FactoryBuilderSupport builder
@MVCMember @Nonnull
SharedFileModel model
@MVCMember @Nonnull
SharedFileController controller
def mainFrame
def dialog
def panel
def searchersPanel
def searchersTable
def downloadersPanel
def certificatesTable
def certificatesPanel
def lastCertificateSortEvent
void initUI() {
mainFrame = application.windowManager.findWindow("main-frame")
int rowHeight = application.context.get("row-height")
dialog = new JDialog(mainFrame,"Details for "+model.sf.getFile().getName(),true)
dialog.setResizable(true)
searchersPanel = builder.panel {
borderLayout()
scrollPane(constraints : BorderLayout.CENTER) {
searchersTable = table(autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.searchers) {
closureColumn(header : "Searcher", type : String, read : {it.searcher?.getHumanReadableName()})
closureColumn(header : "Query", type : String, read : {it.query})
closureColumn(header : "Timestamp", type : Long, read : {it.timestamp})
}
}
}
}
downloadersPanel = builder.panel {
borderLayout()
scrollPane(constraints : BorderLayout.CENTER) {
table(autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.downloaders) {
closureColumn(header : "Downloader", type : String, read : {it})
}
}
}
}
certificatesPanel = builder.panel {
borderLayout()
scrollPane(constraints : BorderLayout.CENTER) {
certificatesTable = table(autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.certificates) {
closureColumn(header : "Issuer", type:String, read : {it.issuer.getHumanReadableName()})
closureColumn(header : "File Name", type : String, read : {it.name.name})
closureColumn(header : "Comment", type : Boolean, read : {it.comment != null})
closureColumn(header : "Timestamp", type : Long, read : {it.timestamp})
}
}
}
panel(constraints : BorderLayout.SOUTH) {
button(text : "Show Comment", enabled : bind {model.showCommentActionEnabled}, showCommentAction)
}
}
}
void mvcGroupInit(Map<String,String> args) {
certificatesTable.rowSorter.addRowSorterListener({evt -> lastCertificateSortEvent = evt})
def selectionModel = certificatesTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
selectionModel.addListSelectionListener({
Certificate c = getSelectedCertificate()
model.showCommentActionEnabled = c != null && c.comment != null
})
certificatesTable.addMouseListener(new MouseAdapter() {
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger())
showMenu(e)
}
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger())
showMenu(e)
}
})
certificatesTable.setDefaultRenderer(Long.class, new DateRenderer())
searchersTable.setDefaultRenderer(Long.class, new DateRenderer())
def tabbedPane = new JTabbedPane()
tabbedPane.addTab("Search Hits", searchersPanel)
tabbedPane.addTab("Downloaders", downloadersPanel)
tabbedPane.addTab("Certificates", certificatesPanel)
dialog.with {
getContentPane().add(tabbedPane)
pack()
setLocationRelativeTo(mainFrame)
setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
addWindowListener(new WindowAdapter() {
public void windowClosed(WindowEvent e) {
mvcGroup.destroy()
}
})
show()
}
}
Certificate getSelectedCertificate() {
int selectedRow = certificatesTable.getSelectedRow()
if (selectedRow < 0)
return null
if (lastCertificateSortEvent != null)
selectedRow = certificatesTable.rowSorter.convertRowIndexToModel(selectedRow)
model.certificates[selectedRow]
}
private void showMenu(MouseEvent e) {
if (!model.showCommentActionEnabled)
return
JPopupMenu menu = new JPopupMenu()
JMenuItem showComment = new JMenuItem("Show Comment")
showComment.addActionListener({controller.showComment()})
menu.add(showComment)
menu.show(e.getComponent(), e.getX(), e.getY())
}
}

View File

@@ -29,7 +29,7 @@ class ShowCommentView {
void initUI() { void initUI() {
mainFrame = application.windowManager.findWindow("main-frame") mainFrame = application.windowManager.findWindow("main-frame")
dialog = new JDialog(mainFrame, model.result.name, true) dialog = new JDialog(mainFrame, model.name, true)
dialog.setResizable(true) dialog.setResizable(true)
@@ -37,7 +37,7 @@ class ShowCommentView {
borderLayout() borderLayout()
panel (constraints : BorderLayout.CENTER) { panel (constraints : BorderLayout.CENTER) {
scrollPane { scrollPane {
textArea(text : model.result.comment, rows : 20, columns : 100, editable : false, lineWrap : true, wrapStyleWord : true) textArea(text : model.text, rows : 20, columns : 100, editable : false, lineWrap : true, wrapStyleWord : true)
} }
} }
panel (constraints : BorderLayout.SOUTH) { panel (constraints : BorderLayout.SOUTH) {

View File

@@ -49,8 +49,9 @@ class TrustListView {
scrollPane (constraints : BorderLayout.CENTER){ scrollPane (constraints : BorderLayout.CENTER){
table(id : "trusted-table", autoCreateRowSorter : true, rowHeight : rowHeight) { table(id : "trusted-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.trusted) { tableModel(list : model.trusted) {
closureColumn(header: "Trusted Users", type : String, read : {it.getHumanReadableName()}) closureColumn(header: "Trusted Users", type : String, read : {it.persona.getHumanReadableName()})
closureColumn(header: "Your Trust", type : String, read : {model.trustService.getLevel(it.destination).toString()}) closureColumn(header: "Reason", type : String, read : {it.reason})
closureColumn(header: "Your Trust", type : String, read : {model.trustService.getLevel(it.persona.destination).toString()})
} }
} }
} }
@@ -65,8 +66,9 @@ class TrustListView {
scrollPane (constraints : BorderLayout.CENTER ){ scrollPane (constraints : BorderLayout.CENTER ){
table(id : "distrusted-table", autoCreateRowSorter : true, rowHeight : rowHeight) { table(id : "distrusted-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.distrusted) { tableModel(list : model.distrusted) {
closureColumn(header: "Distrusted Users", type : String, read : {it.getHumanReadableName()}) closureColumn(header: "Distrusted Users", type : String, read : {it.persona.getHumanReadableName()})
closureColumn(header: "Your Trust", type : String, read : {model.trustService.getLevel(it.destination).toString()}) closureColumn(header: "Reason", type:String, read : {it.reason})
closureColumn(header: "Your Trust", type : String, read : {model.trustService.getLevel(it.persona.destination).toString()})
} }
} }
} }

View File

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

View File

@@ -0,0 +1,36 @@
package com.muwire.gui
import java.awt.Desktop
import javax.swing.JDialog
import javax.swing.JOptionPane
import javax.swing.SwingWorker
import com.muwire.core.download.Downloader
class DownloadPreviewer extends SwingWorker {
private final Downloader downloader
private final DownloadPreviewView view
DownloadPreviewer(Downloader downloader, DownloadPreviewView view) {
this.downloader = downloader
this.view = view
}
@Override
protected Object doInBackground() throws Exception {
downloader.generatePreview()
}
@Override
public void done() {
File previewFile = get()
view.dialog.setVisible(false)
view.mvcGroup.destroy()
if (previewFile == null)
JOptionPane.showMessageDialog(null, "Generating preview file failed", "Preview Failed", JOptionPane.ERROR_MESSAGE)
else
Desktop.getDesktop().open(previewFile)
}
}

View File

@@ -14,6 +14,10 @@ class SizeRenderer extends DefaultTableCellRenderer {
@Override @Override
JComponent getTableCellRendererComponent(JTable table, Object value, JComponent getTableCellRendererComponent(JTable table, Object value,
boolean isSelected, boolean hasFocus, int row, int column) { boolean isSelected, boolean hasFocus, int row, int column) {
if (value == null) {
// this is very strange, but it happens. Probably a swing bug?
return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column)
}
Long l = (Long) value Long l = (Long) value
String formatted = DataHelper.formatSize2Decimal(l, false)+"B" String formatted = DataHelper.formatSize2Decimal(l, false)+"B"
setText(formatted) setText(formatted)

View File

@@ -14,9 +14,11 @@ class UISettings {
boolean excludeLocalResult boolean excludeLocalResult
boolean showSearchHashes boolean showSearchHashes
boolean closeWarning boolean closeWarning
boolean certificateWarning
boolean exitOnClose boolean exitOnClose
boolean clearUploads boolean clearUploads
boolean storeSearchHistory boolean storeSearchHistory
boolean groupByFile
Set<String> searchHistory Set<String> searchHistory
Set<String> openTabs Set<String> openTabs
@@ -31,9 +33,11 @@ class UISettings {
autoFontSize = Boolean.parseBoolean(props.getProperty("autoFontSize","false")) autoFontSize = Boolean.parseBoolean(props.getProperty("autoFontSize","false"))
fontSize = Integer.parseInt(props.getProperty("fontSize","12")) fontSize = Integer.parseInt(props.getProperty("fontSize","12"))
closeWarning = Boolean.parseBoolean(props.getProperty("closeWarning","true")) closeWarning = Boolean.parseBoolean(props.getProperty("closeWarning","true"))
certificateWarning = Boolean.parseBoolean(props.getProperty("certificateWarning","true"))
exitOnClose = Boolean.parseBoolean(props.getProperty("exitOnClose","false")) exitOnClose = Boolean.parseBoolean(props.getProperty("exitOnClose","false"))
clearUploads = Boolean.parseBoolean(props.getProperty("clearUploads","false")) clearUploads = Boolean.parseBoolean(props.getProperty("clearUploads","false"))
storeSearchHistory = Boolean.parseBoolean(props.getProperty("storeSearchHistory","true")) storeSearchHistory = Boolean.parseBoolean(props.getProperty("storeSearchHistory","true"))
groupByFile = Boolean.parseBoolean(props.getProperty("groupByFile","false"))
searchHistory = DataUtil.readEncodedSet(props, "searchHistory") searchHistory = DataUtil.readEncodedSet(props, "searchHistory")
openTabs = DataUtil.readEncodedSet(props, "openTabs") openTabs = DataUtil.readEncodedSet(props, "openTabs")
@@ -50,9 +54,11 @@ class UISettings {
props.setProperty("autoFontSize", String.valueOf(autoFontSize)) props.setProperty("autoFontSize", String.valueOf(autoFontSize))
props.setProperty("fontSize", String.valueOf(fontSize)) props.setProperty("fontSize", String.valueOf(fontSize))
props.setProperty("closeWarning", String.valueOf(closeWarning)) props.setProperty("closeWarning", String.valueOf(closeWarning))
props.setProperty("certificateWarning", String.valueOf(certificateWarning))
props.setProperty("exitOnClose", String.valueOf(exitOnClose)) props.setProperty("exitOnClose", String.valueOf(exitOnClose))
props.setProperty("clearUploads", String.valueOf(clearUploads)) props.setProperty("clearUploads", String.valueOf(clearUploads))
props.setProperty("storeSearchHistory", String.valueOf(storeSearchHistory)) props.setProperty("storeSearchHistory", String.valueOf(storeSearchHistory))
props.setProperty("groupByFile", String.valueOf(groupByFile))
if (font != null) if (font != null)
props.setProperty("font", font) props.setProperty("font", font)