Compare commits
83 Commits
muwire-0.5
...
muwire-0.6
Author | SHA1 | Date | |
---|---|---|---|
![]() |
761bf0a177 | ||
![]() |
bd873211c0 | ||
![]() |
036971cfe5 | ||
![]() |
a2637570b1 | ||
![]() |
6012adbeab | ||
![]() |
8f6b6b0caa | ||
![]() |
8f3b5aea8d | ||
![]() |
ee098ace8e | ||
![]() |
5d8401e4bf | ||
![]() |
fbf9add82a | ||
![]() |
7379263fef | ||
![]() |
7d50843754 | ||
![]() |
f4a2864942 | ||
![]() |
afaadf65a4 | ||
![]() |
7bd422d6b4 | ||
![]() |
3f47274f61 | ||
![]() |
419e9a0ce6 | ||
![]() |
ac1068a681 | ||
![]() |
549457e36f | ||
![]() |
14d6d10546 | ||
![]() |
878e397aa0 | ||
![]() |
27831b488b | ||
![]() |
449f46c62b | ||
![]() |
5703b85386 | ||
![]() |
76d8d847bd | ||
![]() |
db84d8e5bf | ||
![]() |
cc9b384907 | ||
![]() |
72960c24a8 | ||
![]() |
71298e5e73 | ||
![]() |
11bc672544 | ||
![]() |
2f6cd311a0 | ||
![]() |
0448750491 | ||
![]() |
800dd1cbba | ||
![]() |
f95e9450f3 | ||
![]() |
d842e3f2f2 | ||
![]() |
2017b53a43 | ||
![]() |
6e2b3f4f33 | ||
![]() |
dbb305139b | ||
![]() |
0801bfec08 | ||
![]() |
00a8d100fe | ||
![]() |
e94b7cb0d4 | ||
![]() |
b0357f2ecd | ||
![]() |
62e72a7ce0 | ||
![]() |
26fa757b13 | ||
![]() |
3b2e1cf98c | ||
![]() |
5de8a51e47 | ||
![]() |
f5c07f13c0 | ||
![]() |
c7b0ae34af | ||
![]() |
cad5301827 | ||
![]() |
c998011873 | ||
![]() |
5802ba7734 | ||
![]() |
b3f775f59a | ||
![]() |
739dbc7a24 | ||
![]() |
af99dee4a3 | ||
![]() |
07a6c63357 | ||
![]() |
c4096568f5 | ||
![]() |
30dda180eb | ||
![]() |
83ea1bed3e | ||
![]() |
9181829e4a | ||
![]() |
94678bad3c | ||
![]() |
e7072803e9 | ||
![]() |
e9f7a51e16 | ||
![]() |
916fad7d9b | ||
![]() |
9feb891c51 | ||
![]() |
b865376d24 | ||
![]() |
8dcba7535c | ||
![]() |
7e881f1fe6 | ||
![]() |
a9aad7d9db | ||
![]() |
e736b42751 | ||
![]() |
acda64aea7 | ||
![]() |
d82dc4ce90 | ||
![]() |
f2ff90795d | ||
![]() |
49f51a9f5f | ||
![]() |
6fbd1267fa | ||
![]() |
149568520f | ||
![]() |
c672880db0 | ||
![]() |
6cb1674d14 | ||
![]() |
dba863a864 | ||
![]() |
642044b7e2 | ||
![]() |
47c14f109a | ||
![]() |
36c1a1a288 | ||
![]() |
5d51b1c580 | ||
![]() |
bf3502220f |
@@ -4,7 +4,7 @@ MuWire is an easy to use file-sharing program which offers anonymity using [I2P
|
||||
|
||||
It is inspired by the LimeWire Gnutella client and developped by a former LimeWire developer.
|
||||
|
||||
The current stable release - 0.5.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
|
||||
|
||||
|
@@ -17,7 +17,7 @@ class BrowseModel {
|
||||
private final Persona persona
|
||||
private final Core core
|
||||
private final TextGUIThread guiThread
|
||||
private final TableModel model = new TableModel("Name","Size","Hash","Comment")
|
||||
private final TableModel model = new TableModel("Name","Size","Hash","Comment","Certificates")
|
||||
private Map<String, UIResultEvent> rootToResult = new HashMap<>()
|
||||
|
||||
private int totalResults
|
||||
@@ -53,7 +53,7 @@ class BrowseModel {
|
||||
String size = DataHelper.formatSize2Decimal(e.size, false) + "B"
|
||||
String infoHash = Base64.encode(e.infohash.getRoot())
|
||||
String comment = String.valueOf(e.comment != null)
|
||||
model.addRow(e.name, size, infoHash, comment)
|
||||
model.addRow(e.name, size, infoHash, comment, e.certificates)
|
||||
rootToResult.put(infoHash, e)
|
||||
|
||||
String percentageString = ""
|
||||
|
@@ -49,7 +49,7 @@ class BrowseView extends BasicWindow {
|
||||
topPanel.addComponent(percentageLabel, layoutData)
|
||||
contentPanel.addComponent(topPanel, layoutData)
|
||||
|
||||
table = new Table("Name","Size","Hash","Comment")
|
||||
table = new Table("Name","Size","Hash","Comment","Certificates")
|
||||
table.with {
|
||||
setCellSelection(false)
|
||||
setTableModel(model.model)
|
||||
@@ -71,19 +71,24 @@ class BrowseView extends BasicWindow {
|
||||
int selectedRow = table.getSelectedRow()
|
||||
def row = model.model.getRow(selectedRow)
|
||||
String infoHash = row[2]
|
||||
boolean comment = Boolean.parseBoolean(row[3])
|
||||
if (comment) {
|
||||
boolean comment = Boolean.parseBoolean(row[3])
|
||||
boolean certificates = row[4] > 0
|
||||
if (comment || certificates) {
|
||||
Window prompt = new BasicWindow("Download Or View Comment")
|
||||
prompt.setHints([Window.Hint.CENTERED])
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(3))
|
||||
contentPanel.setLayoutManager(new GridLayout(4))
|
||||
Button downloadButton = new Button("Download", {download(infoHash)})
|
||||
Button viewButton = new Button("View Comment", {viewComment(infoHash)})
|
||||
Button viewCertificate = new Button("View Certificates",{viewCertificates(infoHash)})
|
||||
Button closeButton = new Button("Cancel", {prompt.close()})
|
||||
|
||||
contentPanel.with {
|
||||
addComponent(downloadButton, layoutData)
|
||||
addComponent(viewButton, layoutData)
|
||||
if (comment)
|
||||
addComponent(viewButton, layoutData)
|
||||
if (certificates)
|
||||
addComponent(viewCertificate, layoutData)
|
||||
addComponent(closeButton, layoutData)
|
||||
}
|
||||
|
||||
@@ -105,7 +110,14 @@ class BrowseView extends BasicWindow {
|
||||
|
||||
private void viewComment(String infoHash) {
|
||||
UIResultEvent result = model.rootToResult[infoHash]
|
||||
ViewCommentView view = new ViewCommentView(result, terminalSize)
|
||||
ViewCommentView view = new ViewCommentView(result.comment, result.name, terminalSize)
|
||||
textGUI.addWindowAndWait(view)
|
||||
}
|
||||
|
||||
private void viewCertificates(String infoHash) {
|
||||
UIResultEvent result = model.rootToResult[infoHash]
|
||||
ViewCertificatesModel model = new ViewCertificatesModel(result, core, textGUI.getGUIThread())
|
||||
ViewCertificatesView view = new ViewCertificatesView(model, textGUI, core, terminalSize)
|
||||
textGUI.addWindowAndWait(view)
|
||||
}
|
||||
}
|
||||
|
@@ -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()
|
||||
}
|
||||
}
|
@@ -32,7 +32,7 @@ import com.muwire.core.UILoadedEvent
|
||||
import com.muwire.core.files.AllFilesLoadedEvent
|
||||
|
||||
class CliLanterna {
|
||||
private static final String MW_VERSION = "0.5.7"
|
||||
private static final String MW_VERSION = "0.6.2"
|
||||
|
||||
private static volatile Core core
|
||||
|
||||
|
@@ -18,7 +18,7 @@ class FilesModel {
|
||||
private final TextGUIThread guiThread
|
||||
private final Core core
|
||||
private final List<SharedFile> sharedFiles = new ArrayList<>()
|
||||
private final TableModel model = new TableModel("Name","Size","Comment","Search Hits","Downloaders")
|
||||
private final TableModel model = new TableModel("Name","Size","Comment","Certified","Search Hits","Downloaders")
|
||||
|
||||
FilesModel(TextGUIThread guiThread, Core core) {
|
||||
this.guiThread = guiThread
|
||||
@@ -41,7 +41,7 @@ class FilesModel {
|
||||
def eventBus = core.eventBus
|
||||
guiThread.invokeLater {
|
||||
core.muOptions.watchedDirectories.each {
|
||||
eventBus.publish(new DirectoryWatchedEvent(directory : new File(it)))
|
||||
eventBus.publish(new FileSharedEvent(file: new File(it)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,9 +72,10 @@ class FilesModel {
|
||||
sharedFiles.each {
|
||||
long size = it.getCachedLength()
|
||||
boolean comment = it.comment != null
|
||||
boolean certified = core.certificateManager.hasLocalCertificate(it.getInfoHash())
|
||||
String hits = String.valueOf(it.getHits())
|
||||
String downloaders = String.valueOf(it.getDownloaders().size())
|
||||
model.addRow(new SharedFileWrapper(it), DataHelper.formatSize2(size, false)+"B", comment, hits, downloaders)
|
||||
model.addRow(new SharedFileWrapper(it), DataHelper.formatSize2(size, false)+"B", comment, certified, hits, downloaders)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -17,6 +17,7 @@ import com.googlecode.lanterna.gui2.dialogs.TextInputDialog
|
||||
import com.googlecode.lanterna.gui2.table.Table
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.filecert.UICreateCertificateEvent
|
||||
import com.muwire.core.files.DirectoryUnsharedEvent
|
||||
import com.muwire.core.files.FileSharedEvent
|
||||
import com.muwire.core.files.FileUnsharedEvent
|
||||
@@ -42,7 +43,7 @@ class FilesView extends BasicWindow {
|
||||
contentPanel.setLayoutManager(new GridLayout(1))
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false)
|
||||
|
||||
table = new Table("Name","Size","Comment","Search Hits","Downloaders")
|
||||
table = new Table("Name","Size","Comment","Certified","Search Hits","Downloaders")
|
||||
table.setCellSelection(false)
|
||||
table.setTableModel(model.model)
|
||||
table.setSelectAction({rowSelected()})
|
||||
@@ -77,7 +78,7 @@ class FilesView extends BasicWindow {
|
||||
Window prompt = new BasicWindow("Unshare or add comment to "+sf.getFile().getName()+" ?")
|
||||
prompt.setHints([Window.Hint.CENTERED])
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(3))
|
||||
contentPanel.setLayoutManager(new GridLayout(4))
|
||||
|
||||
Button unshareButton = new Button("Unshare", {
|
||||
core.eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
|
||||
@@ -88,11 +89,16 @@ class FilesView extends BasicWindow {
|
||||
AddCommentView view = new AddCommentView(textGUI, core, sf, terminalSize)
|
||||
textGUI.addWindowAndWait(view)
|
||||
})
|
||||
Button certifyButton = new Button("Certify", {
|
||||
core.eventBus.publish(new UICreateCertificateEvent(sharedFile : sf))
|
||||
MessageDialog.showMessageDialog(textGUI, "Certificate Created", "Certificate has been issued", MessageDialogButton.OK)
|
||||
})
|
||||
Button closeButton = new Button("Close", {prompt.close()})
|
||||
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
contentPanel.addComponent(unshareButton, layoutData)
|
||||
contentPanel.addComponent(addCommentButton, layoutData)
|
||||
contentPanel.addComponent(certifyButton, layoutData)
|
||||
contentPanel.addComponent(closeButton, layoutData)
|
||||
|
||||
prompt.setComponent(contentPanel)
|
||||
|
@@ -15,13 +15,13 @@ class ResultsModel {
|
||||
|
||||
ResultsModel(UIResultBatchEvent results) {
|
||||
this.results = results
|
||||
model = new TableModel("Name","Size","Hash","Sources","Comment")
|
||||
model = new TableModel("Name","Size","Hash","Sources","Comment","Certificates")
|
||||
results.results.each {
|
||||
String size = DataHelper.formatSize2Decimal(it.size, false) + "B"
|
||||
String infoHash = Base64.encode(it.infohash.getRoot())
|
||||
String sources = String.valueOf(it.sources.size())
|
||||
String comment = String.valueOf(it.comment != null)
|
||||
model.addRow(it.name, size, infoHash, sources, comment)
|
||||
model.addRow(it.name, size, infoHash, sources, comment, it.certificates)
|
||||
rootToResult.put(infoHash, it)
|
||||
}
|
||||
}
|
||||
|
@@ -37,7 +37,7 @@ class ResultsView extends BasicWindow {
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(1))
|
||||
|
||||
table = new Table("Name","Size","Hash","Sources","Comment")
|
||||
table = new Table("Name","Size","Hash","Sources","Comment","Certificates")
|
||||
table.setCellSelection(false)
|
||||
table.setSelectAction({rowSelected()})
|
||||
table.setTableModel(model.model)
|
||||
@@ -55,18 +55,29 @@ class ResultsView extends BasicWindow {
|
||||
int selectedRow = table.getSelectedRow()
|
||||
def rows = model.model.getRow(selectedRow)
|
||||
boolean comment = Boolean.parseBoolean(rows[4])
|
||||
if (comment) {
|
||||
Window prompt = new BasicWindow("Download Or View Comment")
|
||||
boolean certificates = rows[5] > 0
|
||||
if (comment || certificates) {
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
|
||||
Window prompt = new BasicWindow("Download Or View Comment/Certificates")
|
||||
prompt.setHints([Window.Hint.CENTERED])
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(3))
|
||||
contentPanel.setLayoutManager(new GridLayout(4))
|
||||
Button downloadButton = new Button("Download", {download(rows[2])})
|
||||
Button viewButton = new Button("View Comment", {viewComment(rows[2])})
|
||||
contentPanel.addComponent(downloadButton, layoutData)
|
||||
|
||||
|
||||
if (comment) {
|
||||
Button viewButton = new Button("View Comment", {viewComment(rows[2])})
|
||||
contentPanel.addComponent(viewButton, layoutData)
|
||||
}
|
||||
if (certificates) {
|
||||
Button certsButton = new Button("View Certificates", {viewCertificates(rows[2])})
|
||||
contentPanel.addComponent(certsButton, layoutData)
|
||||
}
|
||||
|
||||
Button closeButton = new Button("Cancel", {prompt.close()})
|
||||
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
contentPanel.addComponent(downloadButton, layoutData)
|
||||
contentPanel.addComponent(viewButton, layoutData)
|
||||
contentPanel.addComponent(closeButton, layoutData)
|
||||
prompt.setComponent(contentPanel)
|
||||
downloadButton.takeFocus()
|
||||
@@ -88,7 +99,14 @@ class ResultsView extends BasicWindow {
|
||||
|
||||
private void viewComment(String infohash) {
|
||||
UIResultEvent result = model.rootToResult[infohash]
|
||||
ViewCommentView view = new ViewCommentView(result, terminalSize)
|
||||
ViewCommentView view = new ViewCommentView(result.comment, result.name, terminalSize)
|
||||
textGUI.addWindowAndWait(view)
|
||||
}
|
||||
|
||||
private void viewCertificates(String infohash) {
|
||||
UIResultEvent result = model.rootToResult[infohash]
|
||||
ViewCertificatesModel model = new ViewCertificatesModel(result, core, textGUI.getGUIThread())
|
||||
ViewCertificatesView view = new ViewCertificatesView(model, textGUI, core, terminalSize)
|
||||
textGUI.addWindowAndWait(view)
|
||||
}
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ import com.muwire.core.search.QueryEvent
|
||||
import com.muwire.core.search.SearchEvent
|
||||
import com.muwire.core.search.UIResultBatchEvent
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import net.i2p.crypto.DSAEngine
|
||||
import net.i2p.data.Base64
|
||||
@@ -45,16 +46,16 @@ class SearchModel {
|
||||
|
||||
def searchEvent
|
||||
byte [] payload
|
||||
UUID uuid = UUID.randomUUID()
|
||||
long timestamp = System.currentTimeMillis()
|
||||
byte [] sig2 = DataUtil.signUUID(uuid, timestamp, core.spk)
|
||||
if (hashSearch) {
|
||||
searchEvent = new SearchEvent(searchHash : root, uuid : UUID.randomUUID(), oobInfohash : true, compressedResults : true)
|
||||
searchEvent = new SearchEvent(searchHash : root, uuid : uuid, oobInfohash : true, compressedResults : true)
|
||||
payload = root
|
||||
} else {
|
||||
def replaced = query.toLowerCase().trim().replaceAll(SplitPattern.SPLIT_PATTERN, " ")
|
||||
def terms = replaced.split(" ")
|
||||
def nonEmpty = []
|
||||
terms.each { if (it.length() > 0) nonEmpty << it }
|
||||
def nonEmpty = SplitPattern.termify(query)
|
||||
payload = String.join(" ", nonEmpty).getBytes(StandardCharsets.UTF_8)
|
||||
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : UUID.randomUUID(), oobInfohash: true,
|
||||
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : uuid, oobInfohash: true,
|
||||
searchComments : core.muOptions.searchComments, compressedResults : true)
|
||||
}
|
||||
|
||||
@@ -64,7 +65,7 @@ class SearchModel {
|
||||
|
||||
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : firstHop,
|
||||
replyTo: core.me.destination, receivedOn: core.me.destination,
|
||||
originator : core.me, sig: sig.data))
|
||||
originator : core.me, sig: sig.data, queryTime : timestamp, sig2 : sig2))
|
||||
}
|
||||
|
||||
void unregister() {
|
||||
|
@@ -0,0 +1,7 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.muwire.core.trust.TrustService
|
||||
|
||||
class TrustEntryWrapper {
|
||||
TrustService.TrustEntry entry
|
||||
}
|
@@ -16,8 +16,8 @@ class TrustListModel {
|
||||
this.trustList = trustList
|
||||
this.core = core
|
||||
|
||||
trustedTableModel = new TableModel("Trusted User","Your Trust")
|
||||
distrustedTableModel = new TableModel("Distrusted User", "Your Trust")
|
||||
trustedTableModel = new TableModel("Trusted User","Reason","Your Trust")
|
||||
distrustedTableModel = new TableModel("Distrusted User", "Reason", "Your Trust")
|
||||
refreshModels()
|
||||
|
||||
core.eventBus.register(TrustEvent.class, this)
|
||||
@@ -36,10 +36,10 @@ class TrustListModel {
|
||||
distrustRows.times { distrustedTableModel.removeRow(0) }
|
||||
|
||||
trustList.good.each {
|
||||
trustedTableModel.addRow(new PersonaWrapper(it), core.trustService.getLevel(it.destination))
|
||||
trustedTableModel.addRow(new PersonaWrapper(it.persona),it.reason, core.trustService.getLevel(it.persona.destination))
|
||||
}
|
||||
trustList.bad.each {
|
||||
distrustedTableModel.addRow(new PersonaWrapper(it), core.trustService.getLevel(it.destination))
|
||||
distrustedTableModel.addRow(new PersonaWrapper(it.persona),it.reason, core.trustService.getLevel(it.persona.destination))
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -12,6 +12,7 @@ import com.googlecode.lanterna.gui2.TextGUI
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
|
||||
import com.googlecode.lanterna.gui2.dialogs.TextInputDialog
|
||||
import com.googlecode.lanterna.gui2.table.Table
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.Persona
|
||||
@@ -48,7 +49,7 @@ class TrustListView extends BasicWindow {
|
||||
Panel topPanel = new Panel()
|
||||
topPanel.setLayoutManager(new GridLayout(2))
|
||||
|
||||
trusted = new Table("Trusted User","Your Trust")
|
||||
trusted = new Table("Trusted User","Reason","Your Trust")
|
||||
trusted.with {
|
||||
setCellSelection(false)
|
||||
setTableModel(model.trustedTableModel)
|
||||
@@ -57,7 +58,7 @@ class TrustListView extends BasicWindow {
|
||||
trusted.setSelectAction({ actionsForUser(true) })
|
||||
topPanel.addComponent(trusted, layoutData)
|
||||
|
||||
distrusted = new Table("Distrusted User", "Your Trust")
|
||||
distrusted = new Table("Distrusted User","Reason", "Your Trust")
|
||||
distrusted.with {
|
||||
setCellSelection(false)
|
||||
setTableModel(model.distrustedTableModel)
|
||||
@@ -90,7 +91,8 @@ class TrustListView extends BasicWindow {
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
|
||||
Button trustButton = new Button("Trust",{
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.TRUSTED))
|
||||
String reason = TextInputDialog.showDialog(textGUI, "Reason", "Enter reason (optional)", "")
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.TRUSTED, reason : reason))
|
||||
MessageDialog.showMessageDialog(textGUI, "Marked Trusted", persona.getHumanReadableName() + "has been marked trusted",
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
@@ -100,7 +102,8 @@ class TrustListView extends BasicWindow {
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
Button distrustButton = new Button("Distrust",{
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.DISTRUSTED))
|
||||
String reason = TextInputDialog.showDialog(textGUI, "Reason", "Enter reason (optional)", "")
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.DISTRUSTED, reason : reason))
|
||||
MessageDialog.showMessageDialog(textGUI, "Marked Distrusted", persona.getHumanReadableName() + "has been marked distrusted",
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
|
@@ -17,8 +17,8 @@ class TrustModel {
|
||||
this.guiThread = guiThread
|
||||
this.core = core
|
||||
|
||||
modelTrusted = new TableModel("Trusted Users")
|
||||
modelDistrusted = new TableModel("Distrusted Users")
|
||||
modelTrusted = new TableModel("Trusted Users","Reason")
|
||||
modelDistrusted = new TableModel("Distrusted Users","Reason")
|
||||
modelSubscriptions = new TableModel("Name","Trusted","Distrusted","Status","Last Updated")
|
||||
|
||||
core.eventBus.register(TrustEvent.class, this)
|
||||
@@ -57,11 +57,11 @@ class TrustModel {
|
||||
subsRows.times { modelSubscriptions.removeRow(0) }
|
||||
|
||||
core.trustService.good.values().each {
|
||||
modelTrusted.addRow(new PersonaWrapper(it))
|
||||
modelTrusted.addRow(new PersonaWrapper(it.persona),it.reason)
|
||||
}
|
||||
|
||||
core.trustService.bad.values().each {
|
||||
modelDistrusted.addRow(new PersonaWrapper(it))
|
||||
modelDistrusted.addRow(new PersonaWrapper(it.persona),it.reason)
|
||||
}
|
||||
|
||||
core.trustSubscriber.remoteTrustLists.values().each {
|
||||
|
@@ -11,6 +11,7 @@ import com.googlecode.lanterna.gui2.Window
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialogBuilder
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
|
||||
import com.googlecode.lanterna.gui2.dialogs.TextInputDialog
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.Label
|
||||
import com.googlecode.lanterna.gui2.table.Table
|
||||
@@ -44,14 +45,14 @@ class TrustView extends BasicWindow {
|
||||
Panel topPanel = new Panel()
|
||||
topPanel.setLayoutManager(new GridLayout(2))
|
||||
|
||||
trusted = new Table("Trusted Users")
|
||||
trusted = new Table("Trusted Users","Reason")
|
||||
trusted.setCellSelection(false)
|
||||
trusted.setSelectAction({trustedActions()})
|
||||
trusted.setTableModel(model.modelTrusted)
|
||||
trusted.setVisibleRows(tableSize)
|
||||
topPanel.addComponent(trusted, layoutData)
|
||||
|
||||
distrusted = new Table("Distrusted users")
|
||||
distrusted = new Table("Distrusted users","Reason")
|
||||
distrusted.setCellSelection(false)
|
||||
distrusted.setSelectAction({distrustedActions()})
|
||||
distrusted.setTableModel(model.modelDistrusted)
|
||||
@@ -106,7 +107,8 @@ class TrustView extends BasicWindow {
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
Button markDistrusted = new Button("Mark Distrusted", {
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.DISTRUSTED))
|
||||
String reason = TextInputDialog.showDialog(textGUI, "Reason", "Enter reason (optional)", "")
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.DISTRUSTED, reason : reason))
|
||||
MessageDialog.showMessageDialog(textGUI, "Marked Distrusted", persona.getHumanReadableName() + "has been marked distrusted",
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
@@ -122,7 +124,7 @@ class TrustView extends BasicWindow {
|
||||
}
|
||||
|
||||
private void distrustedActions() {
|
||||
int selectedRow = trusted.getSelectedRow()
|
||||
int selectedRow = distrusted.getSelectedRow()
|
||||
def row = model.modelDistrusted.getRow(selectedRow)
|
||||
Persona persona = row[0].persona
|
||||
|
||||
@@ -138,7 +140,8 @@ class TrustView extends BasicWindow {
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
Button markDistrusted = new Button("Mark Trusted", {
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.TRUSTED))
|
||||
String reason = TextInputDialog.showDialog(textGUI, "Reason", "Enter reason (optional)", "")
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.TRUSTED, reason : reason))
|
||||
MessageDialog.showMessageDialog(textGUI, "Marked Trusted", persona.getHumanReadableName() + "has been marked trusted",
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
@@ -19,8 +19,8 @@ class ViewCommentView extends BasicWindow {
|
||||
private final TextBox textBox
|
||||
private final LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
|
||||
ViewCommentView(UIResultEvent result, TerminalSize terminalSize) {
|
||||
super("View Comments For "+result.getName())
|
||||
ViewCommentView(String text, String title, TerminalSize terminalSize) {
|
||||
super("View Comments For "+title)
|
||||
|
||||
setHints([Window.Hint.CENTERED])
|
||||
|
||||
@@ -28,7 +28,7 @@ class ViewCommentView extends BasicWindow {
|
||||
contentPanel.setLayoutManager(new GridLayout(1))
|
||||
|
||||
TerminalSize boxSize = new TerminalSize((terminalSize.getColumns() / 2).toInteger(), (terminalSize.getRows() / 2).toInteger())
|
||||
textBox = new TextBox(boxSize, result.comment, TextBox.Style.MULTI_LINE)
|
||||
textBox = new TextBox(boxSize, text, TextBox.Style.MULTI_LINE)
|
||||
contentPanel.addComponent(textBox, layoutData)
|
||||
|
||||
Button closeButton = new Button("Close", {close()})
|
||||
|
@@ -18,6 +18,11 @@ import com.muwire.core.download.UIDownloadCancelledEvent
|
||||
import com.muwire.core.download.UIDownloadEvent
|
||||
import com.muwire.core.download.UIDownloadPausedEvent
|
||||
import com.muwire.core.download.UIDownloadResumedEvent
|
||||
import com.muwire.core.filecert.CertificateClient
|
||||
import com.muwire.core.filecert.CertificateManager
|
||||
import com.muwire.core.filecert.UICreateCertificateEvent
|
||||
import com.muwire.core.filecert.UIFetchCertificatesEvent
|
||||
import com.muwire.core.filecert.UIImportCertificateEvent
|
||||
import com.muwire.core.files.FileDownloadedEvent
|
||||
import com.muwire.core.files.FileHashedEvent
|
||||
import com.muwire.core.files.FileHashingEvent
|
||||
@@ -99,6 +104,7 @@ public class Core {
|
||||
final FileManager fileManager
|
||||
final UploadManager uploadManager
|
||||
final ContentManager contentManager
|
||||
final CertificateManager certificateManager
|
||||
|
||||
private final Router router
|
||||
|
||||
@@ -209,6 +215,12 @@ public class Core {
|
||||
|
||||
eventBus = new EventBus()
|
||||
|
||||
log.info("initializing certificate manager")
|
||||
certificateManager = new CertificateManager(eventBus, home, me, spk)
|
||||
eventBus.register(UICreateCertificateEvent.class, certificateManager)
|
||||
eventBus.register(UIImportCertificateEvent.class, certificateManager)
|
||||
|
||||
|
||||
log.info("initializing trust service")
|
||||
File goodTrust = new File(home, "trusted")
|
||||
File badTrust = new File(home, "distrusted")
|
||||
@@ -255,15 +267,19 @@ public class Core {
|
||||
cacheClient = new CacheClient(eventBus,hostCache, connectionManager, i2pSession, props, 10000)
|
||||
|
||||
log.info("initializing update client")
|
||||
updateClient = new UpdateClient(eventBus, i2pSession, myVersion, props, fileManager, me)
|
||||
updateClient = new UpdateClient(eventBus, i2pSession, myVersion, props, fileManager, me, spk)
|
||||
eventBus.register(FileDownloadedEvent.class, updateClient)
|
||||
eventBus.register(UIResultBatchEvent.class, updateClient)
|
||||
|
||||
log.info("initializing connector")
|
||||
I2PConnector i2pConnector = new I2PConnector(socketManager)
|
||||
|
||||
log.info("initializing certificate client")
|
||||
CertificateClient certificateClient = new CertificateClient(eventBus, i2pConnector)
|
||||
eventBus.register(UIFetchCertificatesEvent.class, certificateClient)
|
||||
|
||||
log.info "initializing results sender"
|
||||
ResultsSender resultsSender = new ResultsSender(eventBus, i2pConnector, me, props)
|
||||
ResultsSender resultsSender = new ResultsSender(eventBus, i2pConnector, me, props, certificateManager)
|
||||
|
||||
log.info "initializing search manager"
|
||||
SearchManager searchManager = new SearchManager(eventBus, me, resultsSender)
|
||||
@@ -289,7 +305,8 @@ public class Core {
|
||||
log.info("initializing acceptor")
|
||||
I2PAcceptor i2pAcceptor = new I2PAcceptor(socketManager)
|
||||
connectionAcceptor = new ConnectionAcceptor(eventBus, connectionManager, props,
|
||||
i2pAcceptor, hostCache, trustService, searchManager, uploadManager, fileManager, connectionEstablisher)
|
||||
i2pAcceptor, hostCache, trustService, searchManager, uploadManager, fileManager, connectionEstablisher,
|
||||
certificateManager)
|
||||
|
||||
log.info("initializing directory watcher")
|
||||
directoryWatcher = new DirectoryWatcher(eventBus, fileManager, home, props)
|
||||
@@ -314,7 +331,7 @@ public class Core {
|
||||
eventBus.register(QueryEvent.class, contentManager)
|
||||
|
||||
log.info("initializing browse manager")
|
||||
BrowseManager browseManager = new BrowseManager(i2pConnector, eventBus)
|
||||
BrowseManager browseManager = new BrowseManager(i2pConnector, eventBus, me)
|
||||
eventBus.register(UIBrowseEvent.class, browseManager)
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
// ... at the end, sleep or execute script
|
||||
|
@@ -20,6 +20,7 @@ class MuWireSettings {
|
||||
int totalUploadSlots
|
||||
int uploadSlotsPerUser
|
||||
int updateCheckInterval
|
||||
long lastUpdateCheck
|
||||
boolean autoDownloadUpdate
|
||||
String updateType
|
||||
String nickname
|
||||
@@ -60,6 +61,7 @@ class MuWireSettings {
|
||||
incompleteLocation = new File(incompleteLocationProp)
|
||||
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","60"))
|
||||
updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","24"))
|
||||
lastUpdateCheck = Long.parseLong(props.getProperty("lastUpdateChec","0"))
|
||||
autoDownloadUpdate = Boolean.parseBoolean(props.getProperty("autoDownloadUpdate","true"))
|
||||
updateType = props.getProperty("updateType","jar")
|
||||
shareDownloadedFiles = Boolean.parseBoolean(props.getProperty("shareDownloadedFiles","true"))
|
||||
@@ -107,6 +109,7 @@ class MuWireSettings {
|
||||
props.setProperty("incompleteLocation", incompleteLocation.getAbsolutePath())
|
||||
props.setProperty("downloadRetryInterval", String.valueOf(downloadRetryInterval))
|
||||
props.setProperty("updateCheckInterval", String.valueOf(updateCheckInterval))
|
||||
props.setProperty("lastUpdateCheck", String.valueOf(lastUpdateCheck))
|
||||
props.setProperty("autoDownloadUpdate", String.valueOf(autoDownloadUpdate))
|
||||
props.setProperty("updateType",String.valueOf(updateType))
|
||||
props.setProperty("shareDownloadedFiles", String.valueOf(shareDownloadedFiles))
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
@@ -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()
|
||||
}
|
||||
}
|
@@ -2,6 +2,90 @@ package com.muwire.core
|
||||
|
||||
class SplitPattern {
|
||||
|
||||
public static final String SPLIT_PATTERN = "[\\*\\+\\-,\\.:;\\(\\)=_/\\\\\\!\\\"\\\'\\\$%\\|\\[\\]\\{\\}\\?]";
|
||||
public static final String SPLIT_PATTERN = "[\\*\\+\\-,\\.:;\\(\\)=_/\\\\\\!\\\"\\\'\\\$%\\|\\[\\]\\{\\}\\?\r\n]";
|
||||
|
||||
private static final Set<Character> SPLIT_CHARS = new HashSet<>()
|
||||
static {
|
||||
SPLIT_CHARS.with {
|
||||
add(' '.toCharacter())
|
||||
add('*'.toCharacter())
|
||||
add('+'.toCharacter())
|
||||
add('-'.toCharacter())
|
||||
add(','.toCharacter())
|
||||
add('.'.toCharacter())
|
||||
add(':'.toCharacter())
|
||||
add(';'.toCharacter())
|
||||
add('('.toCharacter())
|
||||
add(')'.toCharacter())
|
||||
add('='.toCharacter())
|
||||
add('_'.toCharacter())
|
||||
add('/'.toCharacter())
|
||||
add('\\'.toCharacter())
|
||||
add('!'.toCharacter())
|
||||
add('\''.toCharacter())
|
||||
add('$'.toCharacter())
|
||||
add('%'.toCharacter())
|
||||
add('|'.toCharacter())
|
||||
add('['.toCharacter())
|
||||
add(']'.toCharacter())
|
||||
add('{'.toCharacter())
|
||||
add('}'.toCharacter())
|
||||
add('?'.toCharacter())
|
||||
}
|
||||
}
|
||||
|
||||
public static String[] termify(final String source) {
|
||||
String lowercase = source.toLowerCase().trim()
|
||||
|
||||
def rv = []
|
||||
int pos = 0
|
||||
int quote = -1
|
||||
|
||||
StringBuilder tmp = new StringBuilder()
|
||||
while(pos < lowercase.length()) {
|
||||
char c = lowercase.charAt(pos++)
|
||||
if (quote < 0 && c == '"') {
|
||||
quote = pos - 1
|
||||
continue
|
||||
}
|
||||
if (quote >= 0) {
|
||||
if (c == '"') {
|
||||
quote = -1
|
||||
if (tmp.length() != 0) {
|
||||
rv << tmp.toString()
|
||||
tmp = new StringBuilder()
|
||||
}
|
||||
} else
|
||||
tmp.append(c)
|
||||
} else if (SPLIT_CHARS.contains(c)) {
|
||||
if (tmp.length() != 0) {
|
||||
rv << tmp.toString()
|
||||
tmp = new StringBuilder()
|
||||
}
|
||||
} else
|
||||
tmp.append c
|
||||
}
|
||||
|
||||
// check if odd number of quotes and re-tokenize from last quote
|
||||
if (quote >= 0) {
|
||||
tmp = new StringBuilder()
|
||||
pos = quote + 1
|
||||
while(pos < lowercase.length()) {
|
||||
char c = lowercase.charAt(pos++)
|
||||
if (SPLIT_CHARS.contains(c)) {
|
||||
if (tmp.length() > 0) {
|
||||
rv << tmp.toString()
|
||||
tmp = new StringBuilder()
|
||||
}
|
||||
} else
|
||||
tmp.append(c)
|
||||
}
|
||||
}
|
||||
|
||||
if (tmp.length() > 0)
|
||||
rv << tmp.toString()
|
||||
|
||||
rv
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -153,6 +153,10 @@ abstract class Connection implements Closeable {
|
||||
query.originator = e.originator.toBase64()
|
||||
if (e.sig != null)
|
||||
query.sig = Base64.encode(e.sig)
|
||||
if (e.queryTime > 0)
|
||||
query.queryTime = e.queryTime
|
||||
if (e.sig2 != null)
|
||||
query.sig2 = Base64.encode(e.sig2)
|
||||
messages.put(query)
|
||||
}
|
||||
|
||||
@@ -232,7 +236,6 @@ abstract class Connection implements Closeable {
|
||||
if (search.compressedResults != null)
|
||||
compressedResults = search.compressedResults
|
||||
byte[] sig = null
|
||||
// TODO: make this mandatory at some point
|
||||
if (search.sig != null) {
|
||||
sig = Base64.decode(search.sig)
|
||||
byte [] payload
|
||||
@@ -247,21 +250,52 @@ abstract class Connection implements Closeable {
|
||||
return
|
||||
} else
|
||||
log.info("query signature verified")
|
||||
} else
|
||||
} else {
|
||||
log.info("no signature in query")
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: make this mandatory at some point
|
||||
byte[] sig2 = null
|
||||
long queryTime = 0
|
||||
if (search.sig2 != null) {
|
||||
if (search.queryTime == null) {
|
||||
log.info("extended signature but no timestamp")
|
||||
return
|
||||
}
|
||||
sig2 = Base64.decode(search.sig2)
|
||||
queryTime = search.queryTime
|
||||
byte [] payload = (search.uuid + String.valueOf(queryTime)).getBytes(StandardCharsets.US_ASCII)
|
||||
def spk = originator.destination.getSigningPublicKey()
|
||||
def signature = new Signature(Constants.SIG_TYPE, sig2)
|
||||
if (!DSAEngine.getInstance().verifySignature(signature, payload, spk)) {
|
||||
log.info("extended signature didn't match uuid and timestamp")
|
||||
return
|
||||
} else {
|
||||
log.info("extended query signature verified")
|
||||
if (queryTime < System.currentTimeMillis() - Constants.MAX_QUERY_AGE) {
|
||||
log.info("query too old")
|
||||
return
|
||||
}
|
||||
}
|
||||
} else
|
||||
log.info("no extended signature in query")
|
||||
|
||||
SearchEvent searchEvent = new SearchEvent(searchTerms : search.keywords,
|
||||
searchHash : infohash,
|
||||
uuid : uuid,
|
||||
oobInfohash : oob,
|
||||
searchComments : searchComments,
|
||||
compressedResults : compressedResults)
|
||||
compressedResults : compressedResults,
|
||||
persona : originator)
|
||||
QueryEvent event = new QueryEvent ( searchEvent : searchEvent,
|
||||
replyTo : replyTo,
|
||||
originator : originator,
|
||||
receivedOn : endpoint.destination,
|
||||
firstHop : search.firstHop,
|
||||
sig : sig )
|
||||
sig : sig,
|
||||
queryTime : queryTime,
|
||||
sig2 : sig2 )
|
||||
eventBus.publish(event)
|
||||
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package com.muwire.core.connection
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.attribute.DosFileAttributes
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.logging.Level
|
||||
@@ -11,8 +12,11 @@ import java.util.zip.InflaterInputStream
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.filecert.Certificate
|
||||
import com.muwire.core.filecert.CertificateManager
|
||||
import com.muwire.core.files.FileManager
|
||||
import com.muwire.core.hostcache.HostCache
|
||||
import com.muwire.core.trust.TrustLevel
|
||||
@@ -45,6 +49,7 @@ class ConnectionAcceptor {
|
||||
final UploadManager uploadManager
|
||||
final FileManager fileManager
|
||||
final ConnectionEstablisher establisher
|
||||
final CertificateManager certificateManager
|
||||
|
||||
final ExecutorService acceptorThread
|
||||
final ExecutorService handshakerThreads
|
||||
@@ -56,7 +61,7 @@ class ConnectionAcceptor {
|
||||
ConnectionAcceptor(EventBus eventBus, UltrapeerConnectionManager manager,
|
||||
MuWireSettings settings, I2PAcceptor acceptor, HostCache hostCache,
|
||||
TrustService trustService, SearchManager searchManager, UploadManager uploadManager,
|
||||
FileManager fileManager, ConnectionEstablisher establisher) {
|
||||
FileManager fileManager, ConnectionEstablisher establisher, CertificateManager certificateManager) {
|
||||
this.eventBus = eventBus
|
||||
this.manager = manager
|
||||
this.settings = settings
|
||||
@@ -67,6 +72,7 @@ class ConnectionAcceptor {
|
||||
this.fileManager = fileManager
|
||||
this.uploadManager = uploadManager
|
||||
this.establisher = establisher
|
||||
this.certificateManager = certificateManager
|
||||
|
||||
acceptorThread = Executors.newSingleThreadExecutor { r ->
|
||||
def rv = new Thread(r)
|
||||
@@ -145,11 +151,17 @@ class ConnectionAcceptor {
|
||||
case (byte)'B':
|
||||
processBROWSE(e)
|
||||
break
|
||||
case (byte)'C':
|
||||
processCERTIFICATES(e)
|
||||
break
|
||||
default:
|
||||
throw new Exception("Invalid read $read")
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.log(Level.WARNING, "incoming connection failed",ex)
|
||||
try {
|
||||
e.getOutputStream().close()
|
||||
} catch (Exception ignore) {}
|
||||
e.close()
|
||||
eventBus.publish new ConnectionEvent(endpoint: e, incoming: true, leaf: null, status: ConnectionAttemptStatus.FAILED)
|
||||
}
|
||||
@@ -198,7 +210,9 @@ class ConnectionAcceptor {
|
||||
os.writeShort(json.bytes.length)
|
||||
os.write(json.bytes)
|
||||
}
|
||||
e.outputStream.flush()
|
||||
try {
|
||||
e.outputStream.close()
|
||||
} catch (Exception ignored) {}
|
||||
e.close()
|
||||
eventBus.publish(new ConnectionEvent(endpoint: e, incoming: true, leaf: leaf, status: ConnectionAttemptStatus.REJECTED))
|
||||
}
|
||||
@@ -278,18 +292,8 @@ class ConnectionAcceptor {
|
||||
if (!searchManager.hasLocalSearch(resultsUUID))
|
||||
throw new UnexpectedResultsException(resultsUUID.toString())
|
||||
|
||||
|
||||
// parse all headers
|
||||
Map<String,String> headers = new HashMap<>()
|
||||
String header
|
||||
while((header = DataUtil.readTillRN(is)) != "" && headers.size() < Constants.MAX_HEADERS) {
|
||||
int colon = header.indexOf(':')
|
||||
if (colon == -1 || colon == header.length() - 1)
|
||||
throw new IOException("invalid header $header")
|
||||
String key = header.substring(0, colon)
|
||||
String value = header.substring(colon + 1)
|
||||
headers[key] = value.trim()
|
||||
}
|
||||
Map<String,String> headers = DataUtil.readAllHeaders(is);
|
||||
|
||||
if (!headers.containsKey("Sender"))
|
||||
throw new IOException("No Sender header")
|
||||
@@ -330,8 +334,14 @@ class ConnectionAcceptor {
|
||||
dis.readFully(rowse)
|
||||
if (rowse != "ROWSE\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
throw new IOException("Invalid BROWSE connection")
|
||||
String header
|
||||
while ((header = DataUtil.readTillRN(dis)) != ""); // ignore headers for now
|
||||
|
||||
Persona browser = null
|
||||
Map<String,String> headers = DataUtil.readAllHeaders(dis);
|
||||
if (headers.containsKey('Persona')) {
|
||||
browser = new Persona(new ByteArrayInputStream(Base64.decode(headers['Persona'])))
|
||||
if (browser.destination != e.destination)
|
||||
throw new IOException("browser persona mismatch")
|
||||
}
|
||||
|
||||
OutputStream os = e.getOutputStream()
|
||||
if (!settings.browseFiles) {
|
||||
@@ -352,8 +362,9 @@ class ConnectionAcceptor {
|
||||
DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))
|
||||
JsonOutput jsonOutput = new JsonOutput()
|
||||
sharedFiles.each {
|
||||
it.hit()
|
||||
def obj = ResultsSender.sharedFileToObj(it, false)
|
||||
it.hit(browser, System.currentTimeMillis(), "Browse Host");
|
||||
int certificates = certificateManager.getByInfoHash(it.getInfoHash()).size()
|
||||
def obj = ResultsSender.sharedFileToObj(it, false, certificates)
|
||||
def json = jsonOutput.toJson(obj)
|
||||
dos.writeShort((short)json.length())
|
||||
dos.write(json.getBytes(StandardCharsets.US_ASCII))
|
||||
@@ -372,10 +383,10 @@ class ConnectionAcceptor {
|
||||
dis.readFully(RUST)
|
||||
if (RUST != "RUST\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
throw new IOException("Invalid TRUST connection")
|
||||
String header
|
||||
while ((header = DataUtil.readTillRN(dis)) != ""); // ignore headers for now
|
||||
|
||||
OutputStream os = e.getOutputStream()
|
||||
|
||||
Map<String,String> headers = DataUtil.readAllHeaders(dis)
|
||||
|
||||
OutputStream os = e.getOutputStream()
|
||||
if (!settings.allowTrustLists) {
|
||||
os.write("403 Not Allowed\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.flush()
|
||||
@@ -383,22 +394,53 @@ class ConnectionAcceptor {
|
||||
return
|
||||
}
|
||||
|
||||
os.write("200 OK\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
List<Persona> good = new ArrayList<>(trustService.good.values())
|
||||
int size = Math.min(Short.MAX_VALUE * 2, good.size())
|
||||
good = good.subList(0, size)
|
||||
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
boolean json = headers.containsKey('Json') && Boolean.parseBoolean(headers['Json'])
|
||||
|
||||
List<TrustService.TrustEntry> good = new ArrayList<>(trustService.good.values())
|
||||
List<TrustService.TrustEntry> bad = new ArrayList<>(trustService.bad.values())
|
||||
DataOutputStream dos = new DataOutputStream(os)
|
||||
dos.writeShort(size)
|
||||
good.each {
|
||||
it.write(dos)
|
||||
}
|
||||
|
||||
List<Persona> bad = new ArrayList<>(trustService.bad.values())
|
||||
size = Math.min(Short.MAX_VALUE * 2, bad.size())
|
||||
bad = bad.subList(0, size)
|
||||
dos.writeShort(size)
|
||||
bad.each {
|
||||
it.write(dos)
|
||||
if (!json) {
|
||||
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
int size = Math.min(Short.MAX_VALUE * 2, good.size())
|
||||
good = good.subList(0, size)
|
||||
dos.writeShort(size)
|
||||
good.each {
|
||||
it.persona.write(dos)
|
||||
}
|
||||
|
||||
size = Math.min(Short.MAX_VALUE * 2, bad.size())
|
||||
bad = bad.subList(0, size)
|
||||
dos.writeShort(size)
|
||||
bad.each {
|
||||
it.persona.write(dos)
|
||||
}
|
||||
} else {
|
||||
dos.write("Json: true\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
dos.write("Good:${good.size()}\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
dos.write("Bad:${bad.size()}\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
dos.write("\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
good.each {
|
||||
def obj = [:]
|
||||
obj.persona = it.persona.toBase64()
|
||||
obj.reason = it.reason
|
||||
String toJson = JsonOutput.toJson(obj)
|
||||
byte [] payload = toJson.getBytes(StandardCharsets.US_ASCII)
|
||||
dos.writeShort(payload.length)
|
||||
dos.write(payload)
|
||||
}
|
||||
bad.each {
|
||||
def obj = [:]
|
||||
obj.persona = it.persona.toBase64()
|
||||
obj.reason = it.reason
|
||||
String toJson = JsonOutput.toJson(obj)
|
||||
byte [] payload = toJson.getBytes(StandardCharsets.US_ASCII)
|
||||
dos.writeShort(payload.length)
|
||||
dos.write(payload)
|
||||
}
|
||||
}
|
||||
|
||||
dos.flush()
|
||||
@@ -406,5 +448,55 @@ class ConnectionAcceptor {
|
||||
e.close()
|
||||
}
|
||||
}
|
||||
|
||||
private void processCERTIFICATES(Endpoint e) {
|
||||
try {
|
||||
byte [] ERTIFICATES = new byte[12]
|
||||
DataInputStream dis = new DataInputStream(e.getInputStream())
|
||||
dis.readFully(ERTIFICATES)
|
||||
if (ERTIFICATES != "ERTIFICATES ".getBytes(StandardCharsets.US_ASCII))
|
||||
throw new IOException("Invalid CERTIFICATES connection")
|
||||
|
||||
byte [] infoHashStringBytes = new byte[44]
|
||||
dis.readFully(infoHashStringBytes)
|
||||
String infoHashString = new String(infoHashStringBytes, StandardCharsets.US_ASCII)
|
||||
|
||||
byte[] rn = new byte[2]
|
||||
dis.readFully(rn)
|
||||
if (rn != "\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
throw new IOException("Malformed CERTIFICATES request")
|
||||
|
||||
String header
|
||||
while ((header = DataUtil.readTillRN(dis)) != ""); // ignore headers for now
|
||||
|
||||
log.info("responding to certificates request for $infoHashString")
|
||||
byte [] root = Base64.decode(infoHashString)
|
||||
|
||||
Set<Certificate> certs = certificateManager.getByInfoHash(new InfoHash(root))
|
||||
if (certs.isEmpty()) {
|
||||
log.info("certs not found")
|
||||
e.getOutputStream().write("404 Certs Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
e.getOutputStream().flush()
|
||||
return
|
||||
}
|
||||
|
||||
OutputStream os = e.getOutputStream()
|
||||
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("Count: ${certs.size()}\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
DataOutputStream dos = new DataOutputStream(os)
|
||||
certs.each {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
||||
it.write(baos)
|
||||
byte [] payload = baos.toByteArray()
|
||||
dos.writeShort(payload.length)
|
||||
dos.write(payload)
|
||||
}
|
||||
dos.close()
|
||||
} finally {
|
||||
e.close()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -276,6 +276,57 @@ public class Downloader {
|
||||
activeWorkers.put(d, newWorker)
|
||||
executorService.submit(newWorker)
|
||||
}
|
||||
|
||||
boolean isSequential() {
|
||||
pieces.ratio == 0f
|
||||
}
|
||||
|
||||
File generatePreview() {
|
||||
int lastCompletePiece = pieces.firstIncomplete() - 1
|
||||
if (lastCompletePiece == -1)
|
||||
return null
|
||||
if (lastCompletePiece < -1)
|
||||
return file
|
||||
long previewableLength = (lastCompletePiece + 1) * ((long)pieceSize)
|
||||
|
||||
// generate name
|
||||
long now = System.currentTimeMillis()
|
||||
File previewFile
|
||||
File parentFile = file.getParentFile()
|
||||
int lastDot = file.getName().lastIndexOf('.')
|
||||
if (lastDot < 0)
|
||||
previewFile = new File(parentFile, file.getName() + "." + String.valueOf(now) + ".mwpreview")
|
||||
else {
|
||||
String name = file.getName().substring(0, lastDot)
|
||||
String extension = file.getName().substring(lastDot + 1)
|
||||
String previewName = name + "." + String.valueOf(now) + ".mwpreview."+extension
|
||||
previewFile = new File(parentFile, previewName)
|
||||
}
|
||||
|
||||
// copy
|
||||
InputStream is = null
|
||||
OutputStream os = null
|
||||
try {
|
||||
is = new BufferedInputStream(new FileInputStream(incompleteFile))
|
||||
os = new BufferedOutputStream(new FileOutputStream(previewFile))
|
||||
byte [] tmp = new byte[0x1 << 13]
|
||||
long totalCopied = 0
|
||||
while(totalCopied < previewableLength) {
|
||||
int read = is.read(tmp, 0, (int)Math.min(tmp.length, previewableLength - totalCopied))
|
||||
if (read < 0)
|
||||
throw new IOException("EOF?")
|
||||
os.write(tmp, 0, read)
|
||||
totalCopied += read
|
||||
}
|
||||
return previewFile
|
||||
} catch (IOException bad) {
|
||||
log.log(Level.WARNING,"Preview failed",bad)
|
||||
return null
|
||||
} finally {
|
||||
try {is?.close() } catch (IOException ignore) {}
|
||||
try {os?.close() } catch (IOException ignore) {}
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadWorker implements Runnable {
|
||||
private final Destination destination
|
||||
|
@@ -108,6 +108,10 @@ class Pieces {
|
||||
partials.clear()
|
||||
}
|
||||
|
||||
synchronized int firstIncomplete() {
|
||||
done.nextClearBit(0)
|
||||
}
|
||||
|
||||
synchronized void write(PrintWriter writer) {
|
||||
for (int i = done.nextSetBit(0); i >= 0; i = done.nextSetBit(i+1)) {
|
||||
writer.println(i)
|
||||
|
152
core/src/main/groovy/com/muwire/core/filecert/Certificate.groovy
Normal file
152
core/src/main/groovy/com/muwire/core/filecert/Certificate.groovy
Normal 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
|
||||
}
|
||||
}
|
@@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
package com.muwire.core.filecert
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class CertificateCreatedEvent extends Event {
|
||||
Certificate certificate
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
package com.muwire.core.filecert
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class CertificateFetchEvent extends Event {
|
||||
CertificateFetchStatus status
|
||||
int count
|
||||
}
|
@@ -0,0 +1,5 @@
|
||||
package com.muwire.core.filecert;
|
||||
|
||||
public enum CertificateFetchStatus {
|
||||
CONNECTING, FETCHING, DONE, FAILED
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
package com.muwire.core.filecert
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class CertificateFetchedEvent extends Event {
|
||||
Certificate certificate
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
@@ -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
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
package com.muwire.core.filecert
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class UIImportCertificateEvent extends Event {
|
||||
Certificate certificate
|
||||
}
|
@@ -143,6 +143,7 @@ class FileManager {
|
||||
|
||||
String comment = sf.getComment()
|
||||
if (comment != null) {
|
||||
comment = DataUtil.readi18nString(Base64.decode(comment))
|
||||
Set<File> existingComment = commentToFile.get(comment)
|
||||
if (existingComment != null) {
|
||||
existingComment.remove(sf.getFile())
|
||||
@@ -198,7 +199,7 @@ class FileManager {
|
||||
found = rootToFiles.get new InfoHash(e.searchHash)
|
||||
found = filter(found, e.oobInfohash)
|
||||
if (found != null && !found.isEmpty()) {
|
||||
found.each { it.hit() }
|
||||
found.each { it.hit(e.persona, e.timestamp, "Hash Search") }
|
||||
re = new ResultsEvent(results: found.asList(), uuid: e.uuid, searchEvent: e)
|
||||
}
|
||||
} else {
|
||||
@@ -214,7 +215,7 @@ class FileManager {
|
||||
files = filter(sharedFiles, e.oobInfohash)
|
||||
|
||||
if (!sharedFiles.isEmpty()) {
|
||||
sharedFiles.each { it.hit() }
|
||||
sharedFiles.each { it.hit(e.persona, e.timestamp, String.join(" ", e.searchTerms)) }
|
||||
re = new ResultsEvent(results: sharedFiles.asList(), uuid: e.uuid, searchEvent: e)
|
||||
}
|
||||
|
||||
@@ -229,7 +230,7 @@ class FileManager {
|
||||
return files
|
||||
Set<SharedFile> rv = new HashSet<>()
|
||||
files.each {
|
||||
if (it.getPieceSize() != 0)
|
||||
if (it != null && it.getPieceSize() != 0)
|
||||
rv.add(it)
|
||||
}
|
||||
rv
|
||||
|
@@ -12,6 +12,7 @@ import java.util.stream.Collectors
|
||||
import com.muwire.core.DownloadedFile
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.Service
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.UILoadedEvent
|
||||
@@ -129,15 +130,21 @@ class PersisterService extends Service {
|
||||
return new FileLoadedEvent(loadedFile : df)
|
||||
}
|
||||
|
||||
int hits = 0
|
||||
if (json.hits != null)
|
||||
hits = json.hits
|
||||
|
||||
SharedFile sf = new SharedFile(file, ih, pieceSize)
|
||||
sf.setComment(json.comment)
|
||||
sf.hits = hits
|
||||
if (json.downloaders != null)
|
||||
sf.getDownloaders().addAll(json.downloaders)
|
||||
if (json.searchers != null) {
|
||||
json.searchers.each {
|
||||
Persona searcher = null
|
||||
if (it.searcher != null)
|
||||
searcher = new Persona(new ByteArrayInputStream(Base64.decode(it.searcher)))
|
||||
long timestamp = it.timestamp
|
||||
String query = it.query
|
||||
sf.hit(searcher, timestamp, query)
|
||||
}
|
||||
}
|
||||
return new FileLoadedEvent(loadedFile: sf)
|
||||
|
||||
}
|
||||
@@ -172,6 +179,19 @@ class PersisterService extends Service {
|
||||
json.hits = sf.getHits()
|
||||
json.downloaders = sf.getDownloaders()
|
||||
|
||||
if (!sf.searches.isEmpty()) {
|
||||
Set searchers = new HashSet<>()
|
||||
sf.searches.each {
|
||||
def search = [:]
|
||||
if (it.searcher != null)
|
||||
search.searcher = it.searcher.toBase64()
|
||||
search.timestamp = it.timestamp
|
||||
search.query = it.query
|
||||
searchers.add(search)
|
||||
}
|
||||
json.searchers = searchers
|
||||
}
|
||||
|
||||
if (sf instanceof DownloadedFile) {
|
||||
json.sources = sf.sources.stream().map( {d -> d.toBase64()}).collect(Collectors.toList())
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ package com.muwire.core.search
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.connection.Endpoint
|
||||
import com.muwire.core.connection.I2PConnector
|
||||
import com.muwire.core.util.DataUtil
|
||||
@@ -20,12 +21,14 @@ class BrowseManager {
|
||||
|
||||
private final I2PConnector connector
|
||||
private final EventBus eventBus
|
||||
private final Persona me
|
||||
|
||||
private final Executor browserThread = Executors.newSingleThreadExecutor()
|
||||
|
||||
BrowseManager(I2PConnector connector, EventBus eventBus) {
|
||||
BrowseManager(I2PConnector connector, EventBus eventBus, Persona me) {
|
||||
this.connector = connector
|
||||
this.eventBus = eventBus
|
||||
this.me = me
|
||||
}
|
||||
|
||||
void onUIBrowseEvent(UIBrowseEvent e) {
|
||||
@@ -35,7 +38,9 @@ class BrowseManager {
|
||||
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.CONNECTING))
|
||||
endpoint = connector.connect(e.host.destination)
|
||||
OutputStream os = endpoint.getOutputStream()
|
||||
os.write("BROWSE\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("BROWSE\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("Persona:${me.toBase64()}\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
InputStream is = endpoint.getInputStream()
|
||||
String code = DataUtil.readTillRN(is)
|
||||
@@ -43,16 +48,7 @@ class BrowseManager {
|
||||
throw new IOException("Invalid code $code")
|
||||
|
||||
// parse all headers
|
||||
Map<String,String> headers = new HashMap<>()
|
||||
String header
|
||||
while((header = DataUtil.readTillRN(is)) != "" && headers.size() < Constants.MAX_HEADERS) {
|
||||
int colon = header.indexOf(':')
|
||||
if (colon == -1 || colon == header.length() - 1)
|
||||
throw new IOException("invalid header $header")
|
||||
String key = header.substring(0, colon)
|
||||
String value = header.substring(colon + 1)
|
||||
headers[key] = value.trim()
|
||||
}
|
||||
Map<String,String> headers = DataUtil.readAllHeaders(is)
|
||||
|
||||
if (!headers.containsKey("Count"))
|
||||
throw new IOException("No count header")
|
||||
|
@@ -13,6 +13,8 @@ class QueryEvent extends Event {
|
||||
Persona originator
|
||||
Destination receivedOn
|
||||
byte[] sig
|
||||
long queryTime
|
||||
byte[] sig2
|
||||
|
||||
String toString() {
|
||||
"searchEvent: $searchEvent firstHop:$firstHop, replyTo:${replyTo.toBase32()}" +
|
||||
|
@@ -99,6 +99,10 @@ class ResultsParser {
|
||||
boolean browse = false
|
||||
if (json.browse != null)
|
||||
browse = json.browse
|
||||
|
||||
int certificates = 0
|
||||
if (json.certificates != null)
|
||||
certificates = json.certificates
|
||||
|
||||
return new UIResultEvent( sender : p,
|
||||
name : name,
|
||||
@@ -108,7 +112,8 @@ class ResultsParser {
|
||||
sources : sources,
|
||||
comment : comment,
|
||||
browse : browse,
|
||||
uuid: uuid)
|
||||
uuid: uuid,
|
||||
certificates : certificates)
|
||||
} catch (Exception e) {
|
||||
throw new InvalidSearchResultException("parsing search result failed",e)
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ package com.muwire.core.search
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.connection.Endpoint
|
||||
import com.muwire.core.connection.I2PConnector
|
||||
import com.muwire.core.filecert.CertificateManager
|
||||
import com.muwire.core.files.FileHasher
|
||||
import com.muwire.core.util.DataUtil
|
||||
import com.muwire.core.Persona
|
||||
@@ -46,12 +47,14 @@ class ResultsSender {
|
||||
private final Persona me
|
||||
private final EventBus eventBus
|
||||
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.eventBus = eventBus
|
||||
this.me = me
|
||||
this.settings = settings
|
||||
this.certificateManager = certificateManager
|
||||
}
|
||||
|
||||
void sendResults(UUID uuid, SharedFile[] results, Destination target, boolean oobInfohash, boolean compressedResults) {
|
||||
@@ -70,6 +73,7 @@ class ResultsSender {
|
||||
if (it.getComment() != null) {
|
||||
comment = DataUtil.readi18nString(Base64.decode(it.getComment()))
|
||||
}
|
||||
int certificates = certificateManager.getByInfoHash(it.getInfoHash()).size()
|
||||
def uiResultEvent = new UIResultEvent( sender : me,
|
||||
name : it.getFile().getName(),
|
||||
size : length,
|
||||
@@ -77,7 +81,8 @@ class ResultsSender {
|
||||
pieceSize : pieceSize,
|
||||
uuid : uuid,
|
||||
sources : suggested,
|
||||
comment : comment
|
||||
comment : comment,
|
||||
certificates : certificates
|
||||
)
|
||||
uiResultEvents << uiResultEvent
|
||||
}
|
||||
@@ -108,7 +113,8 @@ class ResultsSender {
|
||||
me.write(os)
|
||||
os.writeShort((short)results.length)
|
||||
results.each {
|
||||
def obj = sharedFileToObj(it, settings.browseFiles)
|
||||
int certificates = certificateManager.getByInfoHash(it.getInfoHash()).size()
|
||||
def obj = sharedFileToObj(it, settings.browseFiles, certificates)
|
||||
def json = jsonOutput.toJson(obj)
|
||||
os.writeShort((short)json.length())
|
||||
os.write(json.getBytes(StandardCharsets.US_ASCII))
|
||||
@@ -127,7 +133,8 @@ class ResultsSender {
|
||||
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))
|
||||
results.each {
|
||||
def obj = sharedFileToObj(it, settings.browseFiles)
|
||||
int certificates = certificateManager.getByInfoHash(it.getInfoHash()).size()
|
||||
def obj = sharedFileToObj(it, settings.browseFiles, certificates)
|
||||
def json = jsonOutput.toJson(obj)
|
||||
dos.writeShort((short)json.length())
|
||||
dos.write(json.getBytes(StandardCharsets.US_ASCII))
|
||||
@@ -143,7 +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)
|
||||
def baos = new ByteArrayOutputStream()
|
||||
def daos = new DataOutputStream(baos)
|
||||
@@ -166,6 +173,7 @@ class ResultsSender {
|
||||
obj.comment = sf.getComment()
|
||||
|
||||
obj.browse = browseFiles
|
||||
obj.certificates = certificates
|
||||
obj
|
||||
}
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ package com.muwire.core.search
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Persona
|
||||
|
||||
class SearchEvent extends Event {
|
||||
|
||||
@@ -11,6 +12,7 @@ class SearchEvent extends Event {
|
||||
boolean oobInfohash
|
||||
boolean searchComments
|
||||
boolean compressedResults
|
||||
Persona persona
|
||||
|
||||
String toString() {
|
||||
def infoHash = null
|
||||
|
@@ -31,25 +31,49 @@ class SearchIndex {
|
||||
}
|
||||
}
|
||||
|
||||
private static String[] split(String source) {
|
||||
source = source.replaceAll(SplitPattern.SPLIT_PATTERN, " ").toLowerCase()
|
||||
String [] split = source.split(" ")
|
||||
private static String[] split(final String source) {
|
||||
// first split by split pattern
|
||||
String sourceSplit = source.replaceAll(SplitPattern.SPLIT_PATTERN, " ").toLowerCase()
|
||||
String [] split = sourceSplit.split(" ")
|
||||
def rv = []
|
||||
split.each { if (it.length() > 0) rv << it }
|
||||
|
||||
// then just by ' '
|
||||
source.toLowerCase().split(' ').each { if (it.length() > 0) rv << it }
|
||||
|
||||
// and add original string
|
||||
rv << source
|
||||
rv << source.toLowerCase()
|
||||
rv.toArray(new String[0])
|
||||
}
|
||||
|
||||
String[] search(List<String> terms) {
|
||||
Set<String> rv = null;
|
||||
|
||||
Set<String> powerSet = new HashSet<>()
|
||||
terms.each {
|
||||
powerSet.addAll(it.toLowerCase().split(' '))
|
||||
}
|
||||
|
||||
powerSet.each {
|
||||
Set<String> forWord = keywords.getOrDefault(it,[])
|
||||
if (rv == null) {
|
||||
rv = new HashSet<>(forWord)
|
||||
} else {
|
||||
rv.retainAll(forWord)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// now, filter by terms
|
||||
for (Iterator<String> iter = rv.iterator(); iter.hasNext();) {
|
||||
String candidate = iter.next()
|
||||
candidate = candidate.toLowerCase()
|
||||
boolean keep = true
|
||||
terms.each {
|
||||
keep &= candidate.contains(it)
|
||||
}
|
||||
if (!keep)
|
||||
iter.remove()
|
||||
}
|
||||
|
||||
if (rv != null)
|
||||
|
@@ -16,6 +16,7 @@ class UIResultEvent extends Event {
|
||||
int pieceSize
|
||||
String comment
|
||||
boolean browse
|
||||
int certificates
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
@@ -3,6 +3,7 @@ package com.muwire.core.trust
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.trust.TrustService.TrustEntry
|
||||
|
||||
import net.i2p.util.ConcurrentHashSet
|
||||
|
||||
@@ -10,7 +11,7 @@ class RemoteTrustList {
|
||||
public enum Status { NEW, UPDATING, UPDATED, UPDATE_FAILED }
|
||||
|
||||
private final Persona persona
|
||||
private final Set<Persona> good, bad
|
||||
private final Set<TrustEntry> good, bad
|
||||
volatile long timestamp
|
||||
volatile boolean forceUpdate
|
||||
Status status = Status.NEW
|
||||
|
@@ -7,4 +7,5 @@ class TrustEvent extends Event {
|
||||
|
||||
Persona persona
|
||||
TrustLevel level
|
||||
String reason
|
||||
}
|
||||
|
@@ -1,21 +1,26 @@
|
||||
package com.muwire.core.trust
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.logging.Level
|
||||
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.Service
|
||||
|
||||
import groovy.json.JsonOutput
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Destination
|
||||
import net.i2p.util.ConcurrentHashSet
|
||||
|
||||
@Log
|
||||
class TrustService extends Service {
|
||||
|
||||
final File persistGood, persistBad
|
||||
final long persistInterval
|
||||
|
||||
final Map<Destination, Persona> good = new ConcurrentHashMap<>()
|
||||
final Map<Destination, Persona> bad = new ConcurrentHashMap<>()
|
||||
final Map<Destination, TrustEntry> good = new ConcurrentHashMap<>()
|
||||
final Map<Destination, TrustEntry> bad = new ConcurrentHashMap<>()
|
||||
|
||||
final Timer timer
|
||||
|
||||
@@ -37,18 +42,41 @@ class TrustService extends Service {
|
||||
}
|
||||
|
||||
void load() {
|
||||
JsonSlurper slurper = new JsonSlurper()
|
||||
if (persistGood.exists()) {
|
||||
persistGood.eachLine {
|
||||
byte [] decoded = Base64.decode(it)
|
||||
Persona persona = new Persona(new ByteArrayInputStream(decoded))
|
||||
good.put(persona.destination, persona)
|
||||
try {
|
||||
byte [] decoded = Base64.decode(it)
|
||||
Persona persona = new Persona(new ByteArrayInputStream(decoded))
|
||||
good.put(persona.destination, new TrustEntry(persona, null))
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
def json = slurper.parseText(it)
|
||||
byte [] decoded = Base64.decode(json.persona)
|
||||
Persona persona = new Persona(new ByteArrayInputStream(decoded))
|
||||
good.put(persona.destination, new TrustEntry(persona, json.reason))
|
||||
} catch (Exception bad) {
|
||||
log.log(Level.WARNING,"couldn't parse trust entry $it",bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (persistBad.exists()) {
|
||||
persistBad.eachLine {
|
||||
byte [] decoded = Base64.decode(it)
|
||||
Persona persona = new Persona(new ByteArrayInputStream(decoded))
|
||||
bad.put(persona.destination, persona)
|
||||
try {
|
||||
byte [] decoded = Base64.decode(it)
|
||||
Persona persona = new Persona(new ByteArrayInputStream(decoded))
|
||||
bad.put(persona.destination, new TrustEntry(persona, null))
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
def json = slurper.parseText(it)
|
||||
byte [] decoded = Base64.decode(json.persona)
|
||||
Persona persona = new Persona(new ByteArrayInputStream(decoded))
|
||||
bad.put(persona.destination, new TrustEntry(persona, json.reason))
|
||||
} catch (Exception bad) {
|
||||
log.log(Level.WARNING,"couldn't parse trust entry $it",bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
timer.schedule({persist()} as TimerTask, persistInterval, persistInterval)
|
||||
@@ -59,13 +87,19 @@ class TrustService extends Service {
|
||||
persistGood.delete()
|
||||
persistGood.withPrintWriter { writer ->
|
||||
good.each {k,v ->
|
||||
writer.println v.toBase64()
|
||||
def json = [:]
|
||||
json.persona = v.persona.toBase64()
|
||||
json.reason = v.reason
|
||||
writer.println JsonOutput.toJson(json)
|
||||
}
|
||||
}
|
||||
persistBad.delete()
|
||||
persistBad.withPrintWriter { writer ->
|
||||
bad.each { k,v ->
|
||||
writer.println v.toBase64()
|
||||
def json = [:]
|
||||
json.persona = v.persona.toBase64()
|
||||
json.reason = v.reason
|
||||
writer.println JsonOutput.toJson(json)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,11 +116,11 @@ class TrustService extends Service {
|
||||
switch(e.level) {
|
||||
case TrustLevel.TRUSTED:
|
||||
bad.remove(e.persona.destination)
|
||||
good.put(e.persona.destination, e.persona)
|
||||
good.put(e.persona.destination, new TrustEntry(e.persona, e.reason))
|
||||
break
|
||||
case TrustLevel.DISTRUSTED:
|
||||
good.remove(e.persona.destination)
|
||||
bad.put(e.persona.destination, e.persona)
|
||||
bad.put(e.persona.destination, new TrustEntry(e.persona, e.reason))
|
||||
break
|
||||
case TrustLevel.NEUTRAL:
|
||||
good.remove(e.persona.destination)
|
||||
@@ -94,4 +128,24 @@ class TrustService extends Service {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public static class TrustEntry {
|
||||
private final Persona persona
|
||||
private final String reason
|
||||
TrustEntry(Persona persona, String reason) {
|
||||
this.persona = persona
|
||||
this.reason = reason
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
persona.hashCode()
|
||||
}
|
||||
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof TrustEntry))
|
||||
return false
|
||||
persona == o.persona
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -12,9 +12,12 @@ import com.muwire.core.Persona
|
||||
import com.muwire.core.UILoadedEvent
|
||||
import com.muwire.core.connection.Endpoint
|
||||
import com.muwire.core.connection.I2PConnector
|
||||
import com.muwire.core.trust.TrustService.TrustEntry
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Destination
|
||||
|
||||
@Log
|
||||
@@ -109,7 +112,9 @@ class TrustSubscriber {
|
||||
endpoint = i2pConnector.connect(trustList.persona.destination)
|
||||
OutputStream os = endpoint.getOutputStream()
|
||||
InputStream is = endpoint.getInputStream()
|
||||
os.write("TRUST\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("TRUST\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("Json:true\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.flush()
|
||||
|
||||
String codeString = DataUtil.readTillRN(is)
|
||||
@@ -123,24 +128,47 @@ class TrustSubscriber {
|
||||
return false
|
||||
}
|
||||
|
||||
// swallow any headers
|
||||
String header
|
||||
while (( header = DataUtil.readTillRN(is)) != "");
|
||||
|
||||
Map<String,String> headers = DataUtil.readAllHeaders(is)
|
||||
DataInputStream dis = new DataInputStream(is)
|
||||
Set<TrustService.TrustEntry> good = new HashSet<>()
|
||||
Set<TrustService.TrustEntry> bad = new HashSet<>()
|
||||
|
||||
if (headers.containsKey('Json') && Boolean.parseBoolean(headers['Json'])) {
|
||||
int countGood = Integer.parseInt(headers['Good'])
|
||||
int countBad = Integer.parseInt(headers['Bad'])
|
||||
|
||||
JsonSlurper slurper = new JsonSlurper()
|
||||
|
||||
for (int i = 0; i < countGood; i++) {
|
||||
int length = dis.readUnsignedShort()
|
||||
byte []payload = new byte[length]
|
||||
dis.readFully(payload)
|
||||
def json = slurper.parse(payload)
|
||||
Persona persona = new Persona(new ByteArrayInputStream(Base64.decode(json.persona)))
|
||||
good.add(new TrustEntry(persona, json.reason))
|
||||
}
|
||||
|
||||
for (int i = 0; i < countBad; i++) {
|
||||
int length = dis.readUnsignedShort()
|
||||
byte []payload = new byte[length]
|
||||
dis.readFully(payload)
|
||||
def json = slurper.parse(payload)
|
||||
Persona persona = new Persona(new ByteArrayInputStream(Base64.decode(json.persona)))
|
||||
bad.add(new TrustEntry(persona, json.reason))
|
||||
}
|
||||
|
||||
} else {
|
||||
int nGood = dis.readUnsignedShort()
|
||||
for (int i = 0; i < nGood; i++) {
|
||||
Persona p = new Persona(dis)
|
||||
good.add(new TrustEntry(p,null))
|
||||
}
|
||||
|
||||
Set<Persona> good = new HashSet<>()
|
||||
int nGood = dis.readUnsignedShort()
|
||||
for (int i = 0; i < nGood; i++) {
|
||||
Persona p = new Persona(dis)
|
||||
good.add(p)
|
||||
}
|
||||
|
||||
Set<Persona> bad = new HashSet<>()
|
||||
int nBad = dis.readUnsignedShort()
|
||||
for (int i = 0; i < nBad; i++) {
|
||||
Persona p = new Persona(dis)
|
||||
bad.add(p)
|
||||
int nBad = dis.readUnsignedShort()
|
||||
for (int i = 0; i < nBad; i++) {
|
||||
Persona p = new Persona(dis)
|
||||
bad.add(new TrustEntry(p, null))
|
||||
}
|
||||
}
|
||||
|
||||
trustList.timestamp = now
|
||||
|
@@ -9,9 +9,11 @@ import com.muwire.core.Persona
|
||||
import com.muwire.core.download.UIDownloadEvent
|
||||
import com.muwire.core.files.FileDownloadedEvent
|
||||
import com.muwire.core.files.FileManager
|
||||
import com.muwire.core.files.FileSharedEvent
|
||||
import com.muwire.core.search.QueryEvent
|
||||
import com.muwire.core.search.SearchEvent
|
||||
import com.muwire.core.search.UIResultBatchEvent
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import groovy.json.JsonOutput
|
||||
import groovy.json.JsonSlurper
|
||||
@@ -21,7 +23,10 @@ import net.i2p.client.I2PSessionMuxedListener
|
||||
import net.i2p.client.SendMessageOptions
|
||||
import net.i2p.client.datagram.I2PDatagramDissector
|
||||
import net.i2p.client.datagram.I2PDatagramMaker
|
||||
import net.i2p.crypto.DSAEngine
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Signature
|
||||
import net.i2p.data.SigningPrivateKey
|
||||
import net.i2p.util.VersionComparator
|
||||
|
||||
@Log
|
||||
@@ -32,6 +37,7 @@ class UpdateClient {
|
||||
final MuWireSettings settings
|
||||
final FileManager fileManager
|
||||
final Persona me
|
||||
final SigningPrivateKey spk
|
||||
|
||||
private final Timer timer
|
||||
|
||||
@@ -43,13 +49,16 @@ class UpdateClient {
|
||||
|
||||
private volatile String text
|
||||
|
||||
UpdateClient(EventBus eventBus, I2PSession session, String myVersion, MuWireSettings settings, FileManager fileManager, Persona me) {
|
||||
UpdateClient(EventBus eventBus, I2PSession session, String myVersion, MuWireSettings settings,
|
||||
FileManager fileManager, Persona me, SigningPrivateKey spk) {
|
||||
this.eventBus = eventBus
|
||||
this.session = session
|
||||
this.myVersion = myVersion
|
||||
this.settings = settings
|
||||
this.fileManager = fileManager
|
||||
this.me = me
|
||||
this.spk = spk
|
||||
this.lastUpdateCheckTime = settings.lastUpdateCheck
|
||||
timer = new Timer("update-client",true)
|
||||
}
|
||||
|
||||
@@ -78,6 +87,8 @@ class UpdateClient {
|
||||
return
|
||||
updateDownloading = false
|
||||
eventBus.publish(new UpdateDownloadedEvent(version : version, signer : signer, text : text))
|
||||
if (!settings.shareDownloadedFiles)
|
||||
eventBus.publish(new FileSharedEvent(file : e.downloadedFile))
|
||||
}
|
||||
|
||||
private void checkUpdate() {
|
||||
@@ -87,6 +98,7 @@ class UpdateClient {
|
||||
return
|
||||
}
|
||||
lastUpdateCheckTime = now
|
||||
settings.lastUpdateCheck = now
|
||||
|
||||
log.info("checking for update")
|
||||
|
||||
@@ -164,9 +176,13 @@ class UpdateClient {
|
||||
version = payload.version
|
||||
signer = payload.signer
|
||||
log.info("starting search for new version hash $payload.infoHash")
|
||||
def searchEvent = new SearchEvent(searchHash : updateInfoHash.getRoot(), uuid : UUID.randomUUID(), oobInfohash : true)
|
||||
Signature sig = DSAEngine.getInstance().sign(updateInfoHash.getRoot(), spk)
|
||||
UUID uuid = UUID.randomUUID()
|
||||
long timestamp = System.currentTimeMillis()
|
||||
byte [] sig2 = DataUtil.signUUID(uuid, timestamp, spk)
|
||||
def searchEvent = new SearchEvent(searchHash : updateInfoHash.getRoot(), uuid : uuid, oobInfohash : true, persona : me)
|
||||
def queryEvent = new QueryEvent(searchEvent : searchEvent, firstHop : true, replyTo : me.destination,
|
||||
receivedOn : me.destination, originator : me)
|
||||
receivedOn : me.destination, originator : me, sig : sig.data, queryTime : timestamp, sig2 : sig2)
|
||||
eventBus.publish(queryEvent)
|
||||
}
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import net.i2p.crypto.SigType;
|
||||
|
||||
public class Constants {
|
||||
public static final byte PERSONA_VERSION = (byte)1;
|
||||
public static final byte FILE_CERT_VERSION = (byte)2;
|
||||
public static final SigType SIG_TYPE = SigType.EdDSA_SHA512_Ed25519;
|
||||
|
||||
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_COMMENT_LENGTH = 0x1 << 15;
|
||||
|
||||
public static final long MAX_QUERY_AGE = 5 * 60 * 1000L;
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
package com.muwire.core
|
||||
package com.muwire.core;
|
||||
|
||||
class InvalidSignatureException extends Exception {
|
||||
|
51
core/src/main/java/com/muwire/core/Name.java
Normal file
51
core/src/main/java/com/muwire/core/Name.java
Normal 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);
|
||||
}
|
||||
}
|
102
core/src/main/java/com/muwire/core/Persona.java
Normal file
102
core/src/main/java/com/muwire/core/Persona.java
Normal 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());
|
||||
}
|
||||
}
|
@@ -6,6 +6,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import com.muwire.core.util.DataUtil;
|
||||
@@ -26,8 +27,8 @@ public class SharedFile {
|
||||
private final List<String> b64EncodedHashList;
|
||||
|
||||
private volatile String comment;
|
||||
private volatile int hits;
|
||||
private final Set<String> downloaders = Collections.synchronizedSet(new HashSet<>());
|
||||
private final Set<SearchEntry> searches = Collections.synchronizedSet(new HashSet<>());
|
||||
|
||||
public SharedFile(File file, InfoHash infoHash, int pieceSize) throws IOException {
|
||||
this.file = file;
|
||||
@@ -97,17 +98,21 @@ public class SharedFile {
|
||||
}
|
||||
|
||||
public int getHits() {
|
||||
return hits;
|
||||
return searches.size();
|
||||
}
|
||||
|
||||
public void hit() {
|
||||
hits++;
|
||||
public void hit(Persona searcher, long timestamp, String query) {
|
||||
searches.add(new SearchEntry(searcher, timestamp, query));
|
||||
}
|
||||
|
||||
public Set<String> getDownloaders() {
|
||||
return downloaders;
|
||||
}
|
||||
|
||||
public Set<SearchEntry> getSearches() {
|
||||
return searches;
|
||||
}
|
||||
|
||||
public void addDownloader(String name) {
|
||||
downloaders.add(name);
|
||||
}
|
||||
@@ -124,4 +129,29 @@ public class SharedFile {
|
||||
SharedFile other = (SharedFile)o;
|
||||
return file.equals(other.file) && infoHash.equals(other.infoHash);
|
||||
}
|
||||
|
||||
public static class SearchEntry {
|
||||
private final Persona searcher;
|
||||
private final long timestamp;
|
||||
private final String query;
|
||||
|
||||
public SearchEntry(Persona searcher, long timestamp, String query) {
|
||||
this.searcher = searcher;
|
||||
this.timestamp = timestamp;
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
return Objects.hash(searcher) ^ Objects.hash(timestamp) ^ query.hashCode();
|
||||
}
|
||||
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof SearchEntry))
|
||||
return false;
|
||||
SearchEntry other = (SearchEntry)o;
|
||||
return Objects.equals(searcher, other.searcher) &&
|
||||
timestamp == other.timestamp &&
|
||||
query.equals(other.query);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -10,14 +10,20 @@ import java.lang.reflect.Method;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.muwire.core.Constants;
|
||||
|
||||
import net.i2p.crypto.DSAEngine;
|
||||
import net.i2p.data.Base64;
|
||||
import net.i2p.data.Signature;
|
||||
import net.i2p.data.SigningPrivateKey;
|
||||
import net.i2p.util.ConcurrentHashSet;
|
||||
|
||||
public class DataUtil {
|
||||
@@ -99,6 +105,20 @@ public class DataUtil {
|
||||
}
|
||||
return new String(baos.toByteArray(), StandardCharsets.US_ASCII);
|
||||
}
|
||||
|
||||
public static Map<String, String> readAllHeaders(InputStream is) throws IOException {
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
String header;
|
||||
while(!(header = readTillRN(is)).equals("") && headers.size() < Constants.MAX_HEADERS) {
|
||||
int colon = header.indexOf(':');
|
||||
if (colon == -1 || colon == header.length() - 1)
|
||||
throw new IOException("Invalid header "+ header);
|
||||
String key = header.substring(0, colon);
|
||||
String value = header.substring(colon + 1);
|
||||
headers.put(key, value.trim());
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
public static String encodeXHave(List<Integer> pieces, int totalPieces) {
|
||||
int bytes = totalPieces / 8;
|
||||
@@ -187,4 +207,10 @@ public class DataUtil {
|
||||
.collect(Collectors.joining(","));
|
||||
props.setProperty(property, encoded);
|
||||
}
|
||||
|
||||
public static byte[] signUUID(UUID uuid, long timestamp, SigningPrivateKey spk) {
|
||||
byte [] payload = (uuid.toString() + String.valueOf(timestamp)).getBytes(StandardCharsets.US_ASCII);
|
||||
Signature sig = DSAEngine.getInstance().sign(payload, spk);
|
||||
return sig.getData();
|
||||
}
|
||||
}
|
||||
|
35
core/src/test/groovy/com/muwire/core/SplitPatternTest.groovy
Normal file
35
core/src/test/groovy/com/muwire/core/SplitPatternTest.groovy
Normal 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
|
||||
}
|
||||
}
|
@@ -95,7 +95,7 @@ class ConnectionAcceptorTest {
|
||||
connectionEstablisher = connectionEstablisherMock.proxyInstance()
|
||||
|
||||
acceptor = new ConnectionAcceptor(eventBus, connectionManager, settings, i2pAcceptor,
|
||||
hostCache, trustService, searchManager, uploadManager, null, connectionEstablisher)
|
||||
hostCache, trustService, searchManager, uploadManager, null, connectionEstablisher, null)
|
||||
acceptor.start()
|
||||
Thread.sleep(100)
|
||||
}
|
||||
|
@@ -1,5 +1,7 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertAll
|
||||
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
@@ -9,6 +11,9 @@ import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.search.ResultsEvent
|
||||
import com.muwire.core.search.SearchEvent
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import net.i2p.data.Base64
|
||||
|
||||
class FileManagerTest {
|
||||
|
||||
@@ -149,7 +154,7 @@ class FileManagerTest {
|
||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf1)
|
||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf2)
|
||||
|
||||
manager.onFileUnsharedEvent new FileUnsharedEvent(unsharedFile: sf2)
|
||||
manager.onFileUnsharedEvent new FileUnsharedEvent(deleted : true, unsharedFile: sf2)
|
||||
|
||||
manager.onSearchEvent new SearchEvent(searchHash : ih.getRoot())
|
||||
Thread.sleep(20)
|
||||
@@ -170,7 +175,7 @@ class FileManagerTest {
|
||||
SharedFile sf2 = new SharedFile(f2, ih2, 0)
|
||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf2)
|
||||
|
||||
manager.onFileUnsharedEvent new FileUnsharedEvent(unsharedFile: sf2)
|
||||
manager.onFileUnsharedEvent new FileUnsharedEvent(deleted : true, unsharedFile: sf2)
|
||||
|
||||
// 1 match left
|
||||
manager.onSearchEvent new SearchEvent(searchTerms: ["c"])
|
||||
@@ -185,4 +190,39 @@ class FileManagerTest {
|
||||
assert results == null
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void testComplicatedScenario() {
|
||||
// this tries to reproduce an NPE when un-sharing then sharing again and searching
|
||||
String comment = "same comment"
|
||||
comment = Base64.encode(DataUtil.encodei18nString(comment))
|
||||
File f1 = new File("MuWire-0.5.10.AppImage")
|
||||
InfoHash ih1 = InfoHash.fromHashList(new byte[32])
|
||||
SharedFile sf1 = new SharedFile(f1, ih1, 0)
|
||||
sf1.setComment(comment)
|
||||
|
||||
manager.onFileLoadedEvent(new FileLoadedEvent(loadedFile : sf1))
|
||||
manager.onFileUnsharedEvent(new FileUnsharedEvent(unsharedFile : sf1, deleted : true))
|
||||
|
||||
File f2 = new File("MuWire-0.6.0.AppImage")
|
||||
InfoHash ih2 = InfoHash.fromHashList(new byte[64])
|
||||
SharedFile sf2 = new SharedFile(f2, ih2, 0)
|
||||
sf2.setComment(comment)
|
||||
|
||||
manager.onFileLoadedEvent(new FileLoadedEvent(loadedFile : sf2))
|
||||
|
||||
manager.onSearchEvent(new SearchEvent(searchTerms : ["muwire"]))
|
||||
Thread.sleep(20)
|
||||
|
||||
assert results != null
|
||||
assert results.results.size() == 1
|
||||
assert results.results.contains(sf2)
|
||||
|
||||
results = null
|
||||
manager.onSearchEvent(new SearchEvent(searchTerms : ['comment'], searchComments : true, oobInfohash : true))
|
||||
Thread.sleep(20)
|
||||
assert results != null
|
||||
assert results.results.size() == 1
|
||||
assert results.results.contains(sf2)
|
||||
}
|
||||
}
|
||||
|
@@ -90,4 +90,56 @@ class SearchIndexTest {
|
||||
def found = index.search(["muwire", "0", "3", "jar"])
|
||||
assert found.size() == 1
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOriginalText() {
|
||||
initIndex(["a-b c-d"])
|
||||
def found = index.search(['a-b'])
|
||||
assert found.size() == 1
|
||||
found = index.search(['c-d'])
|
||||
assert found.size() == 1
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPhrase() {
|
||||
initIndex(["a-b c-d e-f"])
|
||||
def found = index.search(['a-b c-d'])
|
||||
assert found.size() == 1
|
||||
assert index.search(['c-d e-f']).size() == 1
|
||||
assert index.search(['a-b e-f']).size() == 0
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMixedPhraseAndKeyword() {
|
||||
initIndex(["My siamese cat video",
|
||||
"My cat video of a siamese",
|
||||
"Video of a siamese cat"])
|
||||
|
||||
assert index.search(['cat video']).size() == 2
|
||||
assert index.search(['cat video','siamese']).size() == 2
|
||||
assert index.search(['cat', 'video siamese']).size() == 0
|
||||
assert index.search(['cat','video','siamese']).size() == 3
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNewLine() {
|
||||
initIndex(['first\nsecond'])
|
||||
assert index.search(['first']).size() == 1
|
||||
assert index.search(['second']).size() == 1
|
||||
assert index.search(['first','second']).size() == 1
|
||||
assert index.search(['second','first']).size() == 1
|
||||
assert index.search(['second first']).size() == 0
|
||||
assert index.search(['first second']).size() == 0
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDosNewLine() {
|
||||
initIndex(['first\r\nsecond'])
|
||||
assert index.search(['first']).size() == 1
|
||||
assert index.search(['second']).size() == 1
|
||||
assert index.search(['first','second']).size() == 1
|
||||
assert index.search(['second','first']).size() == 1
|
||||
assert index.search(['second first']).size() == 0
|
||||
assert index.search(['first second']).size() == 0
|
||||
}
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@ import com.muwire.core.Destinations
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.Personas
|
||||
|
||||
import groovy.json.JsonSlurper
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Destination
|
||||
|
||||
@@ -55,13 +56,16 @@ class TrustServiceTest {
|
||||
service.onTrustEvent new TrustEvent(level: TrustLevel.DISTRUSTED, persona: personas.persona2)
|
||||
|
||||
Thread.sleep(250)
|
||||
JsonSlurper slurper = new JsonSlurper()
|
||||
def trusted = new HashSet<>()
|
||||
persistGood.eachLine {
|
||||
trusted.add(new Persona(new ByteArrayInputStream(Base64.decode(it))))
|
||||
def json = slurper.parseText(it)
|
||||
trusted.add(new Persona(new ByteArrayInputStream(Base64.decode(json.persona))))
|
||||
}
|
||||
def distrusted = new HashSet<>()
|
||||
persistBad.eachLine {
|
||||
distrusted.add(new Persona(new ByteArrayInputStream(Base64.decode(it))))
|
||||
def json = slurper.parseText(it)
|
||||
distrusted.add(new Persona(new ByteArrayInputStream(Base64.decode(json.persona))))
|
||||
}
|
||||
|
||||
assert trusted.size() == 1
|
||||
|
@@ -1,5 +1,5 @@
|
||||
group = com.muwire
|
||||
version = 0.5.7
|
||||
version = 0.6.2
|
||||
i2pVersion = 0.9.43
|
||||
groovyVersion = 2.4.15
|
||||
slf4jVersion = 1.7.25
|
||||
|
@@ -86,4 +86,29 @@ mvcGroups {
|
||||
view = 'com.muwire.gui.AdvancedSharingView'
|
||||
controller = 'com.muwire.gui.AdvancedSharingController'
|
||||
}
|
||||
'fetch-certificates' {
|
||||
model = 'com.muwire.gui.FetchCertificatesModel'
|
||||
view = 'com.muwire.gui.FetchCertificatesView'
|
||||
controller = 'com.muwire.gui.FetchCertificatesController'
|
||||
}
|
||||
'certificate-warning' {
|
||||
model = 'com.muwire.gui.CertificateWarningModel'
|
||||
view = 'com.muwire.gui.CertificateWarningView'
|
||||
controller = 'com.muwire.gui.CertificateWarningController'
|
||||
}
|
||||
'certificate-control' {
|
||||
model = 'com.muwire.gui.CertificateControlModel'
|
||||
view = 'com.muwire.gui.CertificateControlView'
|
||||
controller = 'com.muwire.gui.CertificateControlController'
|
||||
}
|
||||
'shared-file' {
|
||||
model = 'com.muwire.gui.SharedFileModel'
|
||||
view = 'com.muwire.gui.SharedFileView'
|
||||
controller = 'com.muwire.gui.SharedFileController'
|
||||
}
|
||||
'download-preview' {
|
||||
model = "com.muwire.gui.DownloadPreviewModel"
|
||||
view = "com.muwire.gui.DownloadPreviewView"
|
||||
controller = "com.muwire.gui.DownloadPreviewController"
|
||||
}
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@ import net.i2p.data.Base64
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.download.UIDownloadEvent
|
||||
import com.muwire.core.search.BrowseStatus
|
||||
@@ -22,18 +23,18 @@ class BrowseController {
|
||||
@MVCMember @Nonnull
|
||||
BrowseView view
|
||||
|
||||
EventBus eventBus
|
||||
Core core
|
||||
|
||||
|
||||
void register() {
|
||||
eventBus.register(BrowseStatusEvent.class, this)
|
||||
eventBus.register(UIResultEvent.class, this)
|
||||
eventBus.publish(new UIBrowseEvent(host : model.host))
|
||||
core.eventBus.register(BrowseStatusEvent.class, this)
|
||||
core.eventBus.register(UIResultEvent.class, this)
|
||||
core.eventBus.publish(new UIBrowseEvent(host : model.host))
|
||||
}
|
||||
|
||||
void mvcGroupDestroy() {
|
||||
eventBus.unregister(BrowseStatusEvent.class, this)
|
||||
eventBus.unregister(UIResultEvent.class, this)
|
||||
core.eventBus.unregister(BrowseStatusEvent.class, this)
|
||||
core.eventBus.unregister(UIResultEvent.class, this)
|
||||
}
|
||||
|
||||
void onBrowseStatusEvent(BrowseStatusEvent e) {
|
||||
@@ -69,7 +70,7 @@ class BrowseController {
|
||||
|
||||
selectedResults.each { result ->
|
||||
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
|
||||
eventBus.publish(new UIDownloadEvent(
|
||||
core.eventBus.publish(new UIDownloadEvent(
|
||||
result : [result],
|
||||
sources : [model.host.destination],
|
||||
target : file,
|
||||
@@ -92,8 +93,24 @@ class BrowseController {
|
||||
|
||||
String groupId = Base64.encode(result.infohash.getRoot())
|
||||
Map<String,Object> params = new HashMap<>()
|
||||
params['result'] = result
|
||||
params['text'] = result.comment
|
||||
params['name'] = result.name
|
||||
|
||||
mvcGroup.createMVCGroup("show-comment", groupId, params)
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void viewCertificates() {
|
||||
def selectedResults = view.selectedResults()
|
||||
if (selectedResults == null || selectedResults.size() != 1)
|
||||
return
|
||||
def result = selectedResults[0]
|
||||
if (result.certificates <= 0)
|
||||
return
|
||||
|
||||
def params = [:]
|
||||
params['result'] = result
|
||||
params['core'] = core
|
||||
mvcGroup.createMVCGroup("fetch-certificates", params)
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
@@ -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()
|
||||
}
|
||||
}
|
@@ -5,6 +5,7 @@ import griffon.core.controller.ControllerAction
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import javax.annotation.Nonnull
|
||||
import javax.swing.JOptionPane
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.EventBus
|
||||
@@ -83,8 +84,9 @@ class ContentPanelController {
|
||||
int selectedHit = view.getSelectedHit()
|
||||
if (selectedHit < 0)
|
||||
return
|
||||
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
|
||||
Match m = model.hits[selectedHit]
|
||||
core.eventBus.publish(new TrustEvent(persona : m.persona, level : TrustLevel.TRUSTED))
|
||||
core.eventBus.publish(new TrustEvent(persona : m.persona, level : TrustLevel.TRUSTED, reason : reason))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
@@ -92,8 +94,9 @@ class ContentPanelController {
|
||||
int selectedHit = view.getSelectedHit()
|
||||
if (selectedHit < 0)
|
||||
return
|
||||
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
|
||||
Match m = model.hits[selectedHit]
|
||||
core.eventBus.publish(new TrustEvent(persona : m.persona, level : TrustLevel.DISTRUSTED))
|
||||
core.eventBus.publish(new TrustEvent(persona : m.persona, level : TrustLevel.DISTRUSTED, reason : reason))
|
||||
}
|
||||
|
||||
void saveMuWireSettings() {
|
||||
|
@@ -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
|
||||
}
|
@@ -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()
|
||||
}
|
||||
}
|
@@ -7,18 +7,23 @@ import griffon.core.mvc.MVCGroup
|
||||
import griffon.core.mvc.MVCGroupConfiguration
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import groovy.json.StringEscapeUtils
|
||||
import net.i2p.crypto.DSAEngine
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Signature
|
||||
import net.i2p.data.SigningPrivateKey
|
||||
|
||||
import java.awt.Desktop
|
||||
import java.awt.event.ActionEvent
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
import javax.inject.Inject
|
||||
import javax.swing.JOptionPane
|
||||
import javax.swing.JTable
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.SplitPattern
|
||||
@@ -28,6 +33,7 @@ import com.muwire.core.download.UIDownloadCancelledEvent
|
||||
import com.muwire.core.download.UIDownloadEvent
|
||||
import com.muwire.core.download.UIDownloadPausedEvent
|
||||
import com.muwire.core.download.UIDownloadResumedEvent
|
||||
import com.muwire.core.filecert.UICreateCertificateEvent
|
||||
import com.muwire.core.files.DirectoryUnsharedEvent
|
||||
import com.muwire.core.files.FileUnsharedEvent
|
||||
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.TrustLevel
|
||||
import com.muwire.core.trust.TrustSubscriptionEvent
|
||||
import com.muwire.core.upload.HashListUploader
|
||||
import com.muwire.core.upload.Uploader
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
@ArtifactProviderFor(GriffonController)
|
||||
class MainFrameController {
|
||||
@@ -85,6 +94,7 @@ class MainFrameController {
|
||||
params["search-terms"] = search
|
||||
params["uuid"] = uuid.toString()
|
||||
params["core"] = core
|
||||
params["settings"] = view.settings
|
||||
def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params)
|
||||
model.results[uuid.toString()] = group
|
||||
|
||||
@@ -102,25 +112,22 @@ class MainFrameController {
|
||||
def searchEvent
|
||||
byte [] payload
|
||||
if (hashSearch) {
|
||||
searchEvent = new SearchEvent(searchHash : root, uuid : uuid, oobInfohash: true, compressedResults : true)
|
||||
searchEvent = new SearchEvent(searchHash : root, uuid : uuid, oobInfohash: true, compressedResults : true, persona : core.me)
|
||||
payload = root
|
||||
} else {
|
||||
// this can be improved a lot
|
||||
def replaced = search.toLowerCase().trim().replaceAll(SplitPattern.SPLIT_PATTERN, " ")
|
||||
def terms = replaced.split(" ")
|
||||
def nonEmpty = []
|
||||
terms.each { if (it.length() > 0) nonEmpty << it }
|
||||
def nonEmpty = SplitPattern.termify(search)
|
||||
payload = String.join(" ",nonEmpty).getBytes(StandardCharsets.UTF_8)
|
||||
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : uuid, oobInfohash: true,
|
||||
searchComments : core.muOptions.searchComments, compressedResults : true)
|
||||
searchComments : core.muOptions.searchComments, compressedResults : true, persona : core.me)
|
||||
}
|
||||
boolean firstHop = core.muOptions.allowUntrusted || core.muOptions.searchExtraHop
|
||||
|
||||
Signature sig = DSAEngine.getInstance().sign(payload, core.spk)
|
||||
|
||||
long timestamp = System.currentTimeMillis()
|
||||
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : firstHop,
|
||||
replyTo: core.me.destination, receivedOn: core.me.destination,
|
||||
originator : core.me, sig : sig.data))
|
||||
originator : core.me, sig : sig.data, queryTime : timestamp, sig2 : DataUtil.signUUID(uuid, timestamp, core.spk)))
|
||||
|
||||
}
|
||||
|
||||
@@ -135,13 +142,18 @@ class MainFrameController {
|
||||
def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params)
|
||||
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,
|
||||
oobInfohash: true)
|
||||
oobInfohash: true, persona : core.me)
|
||||
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true,
|
||||
replyTo: core.me.destination, receivedOn: core.me.destination,
|
||||
originator : core.me))
|
||||
originator : core.me, sig : sig.data, queryTime : timestamp, sig2 : sig2))
|
||||
}
|
||||
|
||||
|
||||
private int selectedDownload() {
|
||||
def downloadsTable = builder.getVariable("downloads-table")
|
||||
def selected = downloadsTable.getSelectedRow()
|
||||
@@ -156,8 +168,9 @@ class MainFrameController {
|
||||
int selected = builder.getVariable("searches-table").getSelectedRow()
|
||||
if (selected < 0)
|
||||
return
|
||||
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
|
||||
Persona p = model.searches[selected].originator
|
||||
core.eventBus.publish( new TrustEvent(persona : p, level : TrustLevel.TRUSTED) )
|
||||
core.eventBus.publish( new TrustEvent(persona : p, level : TrustLevel.TRUSTED, reason : reason) )
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
@@ -165,8 +178,9 @@ class MainFrameController {
|
||||
int selected = builder.getVariable("searches-table").getSelectedRow()
|
||||
if (selected < 0)
|
||||
return
|
||||
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
|
||||
Persona p = model.searches[selected].originator
|
||||
core.eventBus.publish( new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED) )
|
||||
core.eventBus.publish( new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED, reason : reason) )
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
@@ -190,6 +204,14 @@ class MainFrameController {
|
||||
downloader.pause()
|
||||
core.eventBus.publish(new UIDownloadPausedEvent())
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void preview() {
|
||||
def downloader = model.downloads[selectedDownload()].downloader
|
||||
def params = [:]
|
||||
params['downloader'] = downloader
|
||||
mvcGroup.createMVCGroup("download-preview", params)
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void clear() {
|
||||
@@ -213,7 +235,7 @@ class MainFrameController {
|
||||
if (row < 0)
|
||||
return
|
||||
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
|
||||
@@ -251,7 +273,7 @@ class MainFrameController {
|
||||
int row = view.getSelectedTrustTablesRow("trusted-table")
|
||||
if (row < 0)
|
||||
return
|
||||
Persona p = model.trusted[row]
|
||||
Persona p = model.trusted[row].persona
|
||||
core.muOptions.trustSubscriptions.add(p)
|
||||
saveMuWireSettings()
|
||||
core.eventBus.publish(new TrustSubscriptionEvent(persona : p, subscribe : true))
|
||||
@@ -328,6 +350,28 @@ class MainFrameController {
|
||||
model.uploads.removeAll { it.finished }
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void showInLibrary() {
|
||||
Uploader uploader = view.selectedUploader()
|
||||
if (uploader == null)
|
||||
return
|
||||
SharedFile sf = null
|
||||
if (uploader instanceof HashListUploader) {
|
||||
InfoHash infoHash = uploader.infoHash
|
||||
Set<SharedFile> sfs = core.fileManager.rootToFiles.get(infoHash)
|
||||
if (sfs != null && !sfs.isEmpty())
|
||||
sf = sfs.first()
|
||||
} else {
|
||||
File f = uploader.file
|
||||
sf = core.fileManager.fileToSharedFile.get(f)
|
||||
}
|
||||
|
||||
if (sf == null)
|
||||
return // can happen if user un-shared
|
||||
|
||||
view.focusOnSharedFile(sf)
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void restoreSession() {
|
||||
model.sessionRestored = true
|
||||
@@ -335,6 +379,47 @@ class MainFrameController {
|
||||
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() {
|
||||
core.saveMuSettings()
|
||||
|
@@ -155,6 +155,8 @@ class OptionsController {
|
||||
uiSettings.autoFontSize = model.automaticFontSize
|
||||
uiSettings.fontSize = Integer.parseInt(view.fontSizeField.text)
|
||||
|
||||
uiSettings.groupByFile = model.groupByFile
|
||||
|
||||
boolean clearCancelledDownloads = view.clearCancelledDownloadsCheckbox.model.isSelected()
|
||||
model.clearCancelledDownloads = clearCancelledDownloads
|
||||
uiSettings.clearCancelledDownloads = clearCancelledDownloads
|
||||
@@ -242,6 +244,16 @@ class OptionsController {
|
||||
model.closeDecisionMade = true
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void groupByFile() {
|
||||
model.groupByFile = true
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void groupBySender() {
|
||||
model.groupByFile = false
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void clearHistory() {
|
||||
uiSettings.searchHistory.clear()
|
||||
|
@@ -7,6 +7,7 @@ import griffon.metadata.ArtifactProviderFor
|
||||
import net.i2p.data.Base64
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
import javax.swing.JOptionPane
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.Persona
|
||||
@@ -26,99 +27,109 @@ class SearchTabController {
|
||||
Core core
|
||||
|
||||
private def selectedResults() {
|
||||
int[] rows = view.resultsTable.getSelectedRows()
|
||||
if (rows.length == 0)
|
||||
return null
|
||||
def sortEvt = view.lastSortEvent
|
||||
if (sortEvt != null) {
|
||||
for (int i = 0; i < rows.length; i++) {
|
||||
rows[i] = view.resultsTable.rowSorter.convertRowIndexToModel(rows[i])
|
||||
if (model.groupedByFile) {
|
||||
return [view.getSelectedResult()]
|
||||
} else {
|
||||
int[] rows = view.resultsTable.getSelectedRows()
|
||||
if (rows.length == 0)
|
||||
return null
|
||||
def sortEvt = view.lastSortEvent
|
||||
if (sortEvt != null) {
|
||||
for (int i = 0; i < rows.length; i++) {
|
||||
rows[i] = view.resultsTable.rowSorter.convertRowIndexToModel(rows[i])
|
||||
}
|
||||
}
|
||||
List<UIResultEvent> results = new ArrayList<>()
|
||||
rows.each { results.add(model.results[it]) }
|
||||
return results
|
||||
}
|
||||
List<UIResultEvent> results = new ArrayList<>()
|
||||
rows.each { results.add(model.results[it]) }
|
||||
results
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void download() {
|
||||
def results = selectedResults()
|
||||
if (results == null)
|
||||
return
|
||||
|
||||
results.removeAll {
|
||||
!mvcGroup.parentGroup.model.canDownload(it.infohash)
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void download() {
|
||||
def results = selectedResults()
|
||||
if (results == null)
|
||||
return
|
||||
results.each { result ->
|
||||
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
|
||||
|
||||
results.removeAll {
|
||||
!mvcGroup.parentGroup.model.canDownload(it.infohash)
|
||||
}
|
||||
def resultsBucket = model.hashBucket[result.infohash]
|
||||
def sources = model.sourcesBucket[result.infohash]
|
||||
|
||||
results.each { result ->
|
||||
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
|
||||
|
||||
def resultsBucket = model.hashBucket[result.infohash]
|
||||
def sources = model.sourcesBucket[result.infohash]
|
||||
|
||||
core.eventBus.publish(new UIDownloadEvent(result : resultsBucket, sources: sources,
|
||||
target : file, sequential : view.sequentialDownloadCheckbox.model.isSelected()))
|
||||
}
|
||||
mvcGroup.parentGroup.view.showDownloadsWindow.call()
|
||||
core.eventBus.publish(new UIDownloadEvent(result : resultsBucket, sources: sources,
|
||||
target : file, sequential : view.sequentialDownload()))
|
||||
}
|
||||
mvcGroup.parentGroup.view.showDownloadsWindow.call()
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void trust() {
|
||||
int row = view.selectedSenderRow()
|
||||
if (row < 0)
|
||||
return
|
||||
def sender = model.senders[row]
|
||||
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.TRUSTED))
|
||||
}
|
||||
@ControllerAction
|
||||
void trust() {
|
||||
def sender = view.selectedSender()
|
||||
if (sender == null)
|
||||
return
|
||||
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
|
||||
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.TRUSTED, reason : reason))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void distrust() {
|
||||
int row = view.selectedSenderRow()
|
||||
if (row < 0)
|
||||
return
|
||||
def sender = model.senders[row]
|
||||
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.DISTRUSTED))
|
||||
}
|
||||
@ControllerAction
|
||||
void distrust() {
|
||||
def sender = view.selectedSender()
|
||||
if (sender == null)
|
||||
return
|
||||
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
|
||||
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.DISTRUSTED, reason : reason))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void neutral() {
|
||||
int row = view.selectedSenderRow()
|
||||
if (row < 0)
|
||||
return
|
||||
def sender = model.senders[row]
|
||||
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.NEUTRAL))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void browse() {
|
||||
int selectedSender = view.selectedSenderRow()
|
||||
if (selectedSender < 0)
|
||||
return
|
||||
Persona sender = model.senders[selectedSender]
|
||||
|
||||
String groupId = sender.getHumanReadableName()
|
||||
Map<String,Object> params = new HashMap<>()
|
||||
params['host'] = sender
|
||||
params['eventBus'] = core.eventBus
|
||||
|
||||
mvcGroup.createMVCGroup("browse", groupId, params)
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void showComment() {
|
||||
int[] selectedRows = view.resultsTable.getSelectedRows()
|
||||
if (selectedRows.length != 1)
|
||||
return
|
||||
if (view.lastSortEvent != null)
|
||||
selectedRows[0] = view.resultsTable.rowSorter.convertRowIndexToModel(selectedRows[0])
|
||||
UIResultEvent event = model.results[selectedRows[0]]
|
||||
if (event.comment == null)
|
||||
return
|
||||
|
||||
String groupId = Base64.encode(event.infohash.getRoot())
|
||||
Map<String,Object> params = new HashMap<>()
|
||||
params['result'] = event
|
||||
|
||||
mvcGroup.createMVCGroup("show-comment", groupId, params)
|
||||
}
|
||||
}
|
||||
@ControllerAction
|
||||
void neutral() {
|
||||
def sender = view.selectedSender()
|
||||
if (sender == null)
|
||||
return
|
||||
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.NEUTRAL))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void browse() {
|
||||
def sender = view.selectedSender()
|
||||
if (sender == null)
|
||||
return
|
||||
|
||||
String groupId = sender.getHumanReadableName()
|
||||
Map<String,Object> params = new HashMap<>()
|
||||
params['host'] = sender
|
||||
params['core'] = core
|
||||
|
||||
mvcGroup.createMVCGroup("browse", groupId, params)
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void showComment() {
|
||||
UIResultEvent event = view.getSelectedResult()
|
||||
if (event == null || event.comment == null)
|
||||
return
|
||||
|
||||
String groupId = Base64.encode(event.infohash.getRoot())
|
||||
Map<String,Object> params = new HashMap<>()
|
||||
params['text'] = event.comment
|
||||
params['name'] = event.name
|
||||
|
||||
mvcGroup.createMVCGroup("show-comment", groupId, params)
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void viewCertificates() {
|
||||
UIResultEvent event = view.getSelectedResult()
|
||||
if (event == null || event.certificates <= 0)
|
||||
return
|
||||
|
||||
def params = [:]
|
||||
params['result'] = event
|
||||
params['core'] = core
|
||||
mvcGroup.createMVCGroup("fetch-certificates", params)
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
@@ -5,6 +5,7 @@ import griffon.core.controller.ControllerAction
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import javax.annotation.Nonnull
|
||||
import javax.swing.JOptionPane
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.Persona
|
||||
@@ -25,8 +26,9 @@ class TrustListController {
|
||||
int selectedRow = view.getSelectedRow("trusted-table")
|
||||
if (selectedRow < 0)
|
||||
return
|
||||
Persona p = model.trusted[selectedRow]
|
||||
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.TRUSTED))
|
||||
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
|
||||
Persona p = model.trusted[selectedRow].persona
|
||||
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.TRUSTED, reason : reason))
|
||||
view.fireUpdate("trusted-table")
|
||||
}
|
||||
|
||||
@@ -35,8 +37,9 @@ class TrustListController {
|
||||
int selectedRow = view.getSelectedRow("distrusted-table")
|
||||
if (selectedRow < 0)
|
||||
return
|
||||
Persona p = model.distrusted[selectedRow]
|
||||
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.TRUSTED))
|
||||
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
|
||||
Persona p = model.distrusted[selectedRow].persona
|
||||
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.TRUSTED, reason : reason))
|
||||
view.fireUpdate("distrusted-table")
|
||||
}
|
||||
|
||||
@@ -45,8 +48,9 @@ class TrustListController {
|
||||
int selectedRow = view.getSelectedRow("trusted-table")
|
||||
if (selectedRow < 0)
|
||||
return
|
||||
Persona p = model.trusted[selectedRow]
|
||||
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED))
|
||||
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
|
||||
Persona p = model.trusted[selectedRow].persona
|
||||
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED, reason : reason))
|
||||
view.fireUpdate("trusted-table")
|
||||
}
|
||||
|
||||
@@ -55,8 +59,9 @@ class TrustListController {
|
||||
int selectedRow = view.getSelectedRow("distrusted-table")
|
||||
if (selectedRow < 0)
|
||||
return
|
||||
Persona p = model.distrusted[selectedRow]
|
||||
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED))
|
||||
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
|
||||
Persona p = model.distrusted[selectedRow].persona
|
||||
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED, reason : reason))
|
||||
view.fireUpdate("distrusted-table")
|
||||
}
|
||||
}
|
@@ -14,6 +14,7 @@ class BrowseModel {
|
||||
@Observable BrowseStatus status
|
||||
@Observable boolean downloadActionEnabled
|
||||
@Observable boolean viewCommentActionEnabled
|
||||
@Observable boolean viewCertificatesActionEnabled
|
||||
@Observable int totalResults
|
||||
@Observable int resultCount
|
||||
|
||||
|
@@ -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())
|
||||
}
|
||||
}
|
@@ -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 {
|
||||
}
|
@@ -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
|
||||
}
|
@@ -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 = []
|
||||
}
|
@@ -27,6 +27,7 @@ import com.muwire.core.connection.DisconnectionEvent
|
||||
import com.muwire.core.content.ContentControlEvent
|
||||
import com.muwire.core.download.DownloadStartedEvent
|
||||
import com.muwire.core.download.Downloader
|
||||
import com.muwire.core.filecert.CertificateCreatedEvent
|
||||
import com.muwire.core.files.AllFilesLoadedEvent
|
||||
import com.muwire.core.files.DirectoryUnsharedEvent
|
||||
import com.muwire.core.files.DirectoryWatchedEvent
|
||||
@@ -99,6 +100,7 @@ class MainFrameModel {
|
||||
@Observable boolean retryButtonEnabled
|
||||
@Observable boolean pauseButtonEnabled
|
||||
@Observable boolean clearButtonEnabled
|
||||
@Observable boolean previewButtonEnabled
|
||||
@Observable String resumeButtonText
|
||||
@Observable boolean addCommentButtonEnabled
|
||||
@Observable boolean subscribeButtonEnabled
|
||||
@@ -175,8 +177,7 @@ class MainFrameModel {
|
||||
}
|
||||
}
|
||||
|
||||
builder.getVariable("uploads-table")?.model.fireTableDataChanged()
|
||||
|
||||
updateTablePreservingSelection("uploads-table")
|
||||
updateTablePreservingSelection("downloads-table")
|
||||
updateTablePreservingSelection("trusted-table")
|
||||
updateTablePreservingSelection("distrusted-table")
|
||||
@@ -208,6 +209,7 @@ class MainFrameModel {
|
||||
core.eventBus.register(UpdateDownloadedEvent.class, this)
|
||||
core.eventBus.register(TrustSubscriptionUpdatedEvent.class, this)
|
||||
core.eventBus.register(SearchEvent.class, this)
|
||||
core.eventBus.register(CertificateCreatedEvent.class, this)
|
||||
|
||||
core.muOptions.watchedKeywords.each {
|
||||
core.eventBus.publish(new ContentControlEvent(term : it, regex: false, add: true))
|
||||
@@ -407,8 +409,7 @@ class MainFrameModel {
|
||||
wrapper.finished = false
|
||||
} else
|
||||
uploads << new UploaderWrapper(uploader : e.uploader)
|
||||
JTable table = builder.getVariable("uploads-table")
|
||||
table.model.fireTableDataChanged()
|
||||
updateTablePreservingSelection("uploads-table")
|
||||
view.refreshSharedFiles()
|
||||
}
|
||||
}
|
||||
@@ -427,8 +428,7 @@ class MainFrameModel {
|
||||
} else {
|
||||
wrapper.finished = true
|
||||
}
|
||||
JTable table = builder.getVariable("uploads-table")
|
||||
table.model.fireTableDataChanged()
|
||||
updateTablePreservingSelection("uploads-table")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -571,6 +571,12 @@ class MainFrameModel {
|
||||
}
|
||||
}
|
||||
|
||||
void onCertificateCreatedEvent(CertificateCreatedEvent e) {
|
||||
runInsideUIAsync {
|
||||
view.refreshSharedFiles()
|
||||
}
|
||||
}
|
||||
|
||||
private void insertIntoTree(SharedFile file) {
|
||||
List<File> parents = new ArrayList<>()
|
||||
File tmp = file.file.getParentFile()
|
||||
|
@@ -42,6 +42,7 @@ class OptionsModel {
|
||||
@Observable boolean excludeLocalResult
|
||||
@Observable boolean showSearchHashes
|
||||
@Observable boolean clearUploads
|
||||
@Observable boolean groupByFile
|
||||
@Observable boolean exitOnClose
|
||||
@Observable boolean closeDecisionMade
|
||||
|
||||
@@ -92,6 +93,7 @@ class OptionsModel {
|
||||
clearUploads = uiSettings.clearUploads
|
||||
exitOnClose = uiSettings.exitOnClose
|
||||
storeSearchHistory = uiSettings.storeSearchHistory
|
||||
groupByFile = uiSettings.groupByFile
|
||||
|
||||
if (core.router != null) {
|
||||
inBw = String.valueOf(settings.inBw)
|
||||
|
@@ -23,6 +23,8 @@ class SearchTabModel {
|
||||
@Observable boolean trustButtonsEnabled
|
||||
@Observable boolean browseActionEnabled
|
||||
@Observable boolean viewCommentActionEnabled
|
||||
@Observable boolean viewCertificatesActionEnabled
|
||||
@Observable boolean groupedByFile
|
||||
|
||||
Core core
|
||||
UISettings uiSettings
|
||||
@@ -33,6 +35,9 @@ class SearchTabModel {
|
||||
def sourcesBucket = [:]
|
||||
def sendersBucket = new LinkedHashMap<>()
|
||||
|
||||
def results2 = []
|
||||
def senders2 = []
|
||||
|
||||
|
||||
void mvcGroupInit(Map<String, String> args) {
|
||||
core = mvcGroup.parentGroup.model.core
|
||||
@@ -71,9 +76,14 @@ class SearchTabModel {
|
||||
sourcesBucket.put(e.infohash, sourceBucket)
|
||||
}
|
||||
sourceBucket.addAll(e.sources)
|
||||
|
||||
results2.clear()
|
||||
results2.addAll(hashBucket.keySet())
|
||||
|
||||
JTable table = builder.getVariable("senders-table")
|
||||
table.model.fireTableDataChanged()
|
||||
table = builder.getVariable("results-table2")
|
||||
table.model.fireTableDataChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,9 +116,16 @@ class SearchTabModel {
|
||||
|
||||
bucket << it
|
||||
senderBucket << it
|
||||
|
||||
}
|
||||
results2.clear()
|
||||
results2.addAll(hashBucket.keySet())
|
||||
JTable table = builder.getVariable("senders-table")
|
||||
table.model.fireTableDataChanged()
|
||||
table = builder.getVariable("results-table2")
|
||||
int selectedRow = table.getSelectedRow()
|
||||
table.model.fireTableDataChanged()
|
||||
table.selectionModel.setSelectionInterval(selectedRow, selectedRow)
|
||||
}
|
||||
}
|
||||
}
|
26
gui/griffon-app/models/com/muwire/gui/SharedFileModel.groovy
Normal file
26
gui/griffon-app/models/com/muwire/gui/SharedFileModel.groovy
Normal 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,[]))
|
||||
}
|
||||
}
|
@@ -8,5 +8,6 @@ import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
@ArtifactProviderFor(GriffonModel)
|
||||
class ShowCommentModel {
|
||||
UIResultEvent result
|
||||
String name
|
||||
String text
|
||||
}
|
@@ -40,6 +40,7 @@ class BrowseView {
|
||||
def resultsTable
|
||||
def lastSortEvent
|
||||
void initUI() {
|
||||
int rowHeight = application.context.get("row-height")
|
||||
mainFrame = application.windowManager.findWindow("main-frame")
|
||||
dialog = new JDialog(mainFrame, model.host.getHumanReadableName(), true)
|
||||
dialog.setResizable(true)
|
||||
@@ -52,17 +53,19 @@ class BrowseView {
|
||||
label(text : bind {model.totalResults == 0 ? "" : Math.round(model.resultCount * 100 / model.totalResults)+ "%"})
|
||||
}
|
||||
scrollPane (constraints : BorderLayout.CENTER){
|
||||
resultsTable = table(autoCreateRowSorter : true) {
|
||||
resultsTable = 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: "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) {
|
||||
button(text : "Download", enabled : bind {model.downloadActionEnabled}, downloadAction)
|
||||
button(text : "View Comment", enabled : bind{model.viewCommentActionEnabled}, viewCommentAction)
|
||||
button(text : "View Certificates", enabled : bind{model.viewCertificatesActionEnabled}, viewCertificatesAction)
|
||||
button(text : "Dismiss", dismissAction)
|
||||
}
|
||||
}
|
||||
@@ -84,6 +87,7 @@ class BrowseView {
|
||||
if (rows.length == 0) {
|
||||
model.downloadActionEnabled = false
|
||||
model.viewCommentActionEnabled = false
|
||||
model.viewCertificatesActionEnabled = false
|
||||
return
|
||||
}
|
||||
|
||||
@@ -94,6 +98,9 @@ class BrowseView {
|
||||
}
|
||||
|
||||
boolean downloadActionEnabled = true
|
||||
|
||||
model.viewCertificatesActionEnabled = (rows.length == 1 && model.results[rows[0]].certificates > 0)
|
||||
|
||||
if (rows.length == 1 && model.results[rows[0]].comment != null)
|
||||
model.viewCommentActionEnabled = true
|
||||
else
|
||||
@@ -104,16 +111,16 @@ class BrowseView {
|
||||
}
|
||||
model.downloadActionEnabled = downloadActionEnabled
|
||||
|
||||
resultsTable.addMouseListener(new MouseAdapter() {
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
if (e.isPopupTrigger())
|
||||
showMenu(e)
|
||||
}
|
||||
public void mousePressed(MouseEvent e) {
|
||||
if (e.isPopupTrigger())
|
||||
showMenu(e)
|
||||
}
|
||||
})
|
||||
})
|
||||
resultsTable.addMouseListener(new MouseAdapter() {
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
if (e.isPopupTrigger())
|
||||
showMenu(e)
|
||||
}
|
||||
public void mousePressed(MouseEvent e) {
|
||||
if (e.isPopupTrigger())
|
||||
showMenu(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -129,6 +136,11 @@ class BrowseView {
|
||||
viewComment.addActionListener({controller.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")
|
||||
copyHash.addActionListener({
|
||||
|
@@ -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())
|
||||
}
|
||||
|
||||
}
|
@@ -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()
|
||||
}
|
||||
}
|
@@ -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()
|
||||
}
|
||||
}
|
@@ -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()
|
||||
}
|
||||
|
||||
}
|
@@ -46,6 +46,7 @@ import java.awt.FlowLayout
|
||||
import java.awt.GridBagConstraints
|
||||
import java.awt.GridBagLayout
|
||||
import java.awt.Insets
|
||||
import java.awt.Rectangle
|
||||
import java.awt.Toolkit
|
||||
import java.awt.datatransfer.DataFlavor
|
||||
import java.awt.datatransfer.StringSelection
|
||||
@@ -70,6 +71,7 @@ class MainFrameView {
|
||||
|
||||
def downloadsTable
|
||||
def lastDownloadSortEvent
|
||||
def lastUploadsSortEvent
|
||||
def lastSharedSortEvent
|
||||
def trustTablesSortEvents = [:]
|
||||
def expansionListener = new TreeExpansions()
|
||||
@@ -122,6 +124,11 @@ class MainFrameView {
|
||||
env["core"] = model.core
|
||||
mvcGroup.createMVCGroup("advanced-sharing",env)
|
||||
})
|
||||
menuItem("Certificates", actionPerformed : {
|
||||
def env = [:]
|
||||
env['core'] = model.core
|
||||
mvcGroup.createMVCGroup("certificate-control",env)
|
||||
})
|
||||
}
|
||||
}
|
||||
borderLayout()
|
||||
@@ -209,6 +216,7 @@ class MainFrameView {
|
||||
button(text: "Pause", enabled : bind {model.pauseButtonEnabled}, pauseAction)
|
||||
button(text: bind { model.resumeButtonText }, enabled : bind {model.retryButtonEnabled}, resumeAction)
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -269,6 +277,10 @@ class MainFrameView {
|
||||
closureColumn(header : "Name", preferredWidth : 500, type : String, read : {row -> row.getCachedPath()})
|
||||
closureColumn(header : "Size", preferredWidth : 50, type : Long, read : {row -> row.getCachedLength() })
|
||||
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 : "Downloaders", preferredWidth: 50, type : Integer, read : {it.getDownloaders().size()})
|
||||
}
|
||||
@@ -295,6 +307,7 @@ class MainFrameView {
|
||||
panel {
|
||||
button(text : "Share files", actionPerformed : shareFiles)
|
||||
button(text : "Add Comment", enabled : bind {model.addCommentButtonEnabled}, addCommentAction)
|
||||
button(text : "Certify", enabled : bind {model.addCommentButtonEnabled}, issueCertificateAction)
|
||||
}
|
||||
panel {
|
||||
panel {
|
||||
@@ -310,7 +323,7 @@ class MainFrameView {
|
||||
label("Uploads")
|
||||
}
|
||||
scrollPane (constraints : BorderLayout.CENTER) {
|
||||
table(id : "uploads-table", rowHeight : rowHeight) {
|
||||
table(id : "uploads-table", autoCreateRowSorter: true, rowHeight : rowHeight) {
|
||||
tableModel(list : model.uploads) {
|
||||
closureColumn(header : "Name", type : String, read : {row -> row.uploader.getName() })
|
||||
closureColumn(header : "Progress", type : String, read : { row ->
|
||||
@@ -410,7 +423,8 @@ class MainFrameView {
|
||||
scrollPane(constraints : BorderLayout.CENTER) {
|
||||
table(id : "trusted-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
|
||||
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) {
|
||||
table(id : "distrusted-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
|
||||
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 : "Distrusted", preferredWidth: 20, type: Integer, read : {it.bad.size()})
|
||||
closureColumn(header : "Status", preferredWidth: 30, type: String, read : {it.status.toString()})
|
||||
closureColumn(header : "Last Updated", preferredWidth: 200, type : String, read : {
|
||||
if (it.timestamp == 0)
|
||||
return "Never"
|
||||
else
|
||||
return String.valueOf(new Date(it.timestamp))
|
||||
})
|
||||
closureColumn(header : "Last Updated", preferredWidth: 200, type : Long, read : { it.timestamp })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -527,6 +537,7 @@ class MainFrameView {
|
||||
model.cancelButtonEnabled = false
|
||||
model.retryButtonEnabled = false
|
||||
model.pauseButtonEnabled = false
|
||||
model.previewButtonEnabled = false
|
||||
model.downloader = null
|
||||
downloadDetailsPanel.getLayout().show(downloadDetailsPanel,"select-download")
|
||||
return
|
||||
@@ -535,6 +546,7 @@ class MainFrameView {
|
||||
if (downloader == null)
|
||||
return
|
||||
model.downloader = downloader
|
||||
model.previewButtonEnabled = true
|
||||
downloadDetailsPanel.getLayout().show(downloadDetailsPanel,"download-selected")
|
||||
switch(downloader.getCurrentState()) {
|
||||
case Downloader.DownloadState.CONNECTING :
|
||||
@@ -596,6 +608,15 @@ class MainFrameView {
|
||||
JMenuItem commentSelectedFiles = new JMenuItem("Comment selected files")
|
||||
commentSelectedFiles.addActionListener({mvcGroup.controller.addComment()})
|
||||
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() {
|
||||
@Override
|
||||
@@ -637,6 +658,29 @@ class MainFrameView {
|
||||
|
||||
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
|
||||
def searchesTable = builder.getVariable("searches-table")
|
||||
JPopupMenu searchTableMenu = new JPopupMenu()
|
||||
@@ -702,6 +746,8 @@ class MainFrameView {
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
subscriptionTable.setDefaultRenderer(Long.class, new DateRenderer())
|
||||
|
||||
// trusted table
|
||||
def trustedTable = builder.getVariable("trusted-table")
|
||||
@@ -889,6 +935,36 @@ class MainFrameView {
|
||||
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() {
|
||||
def searchWindow = builder.getVariable("search window")
|
||||
String id
|
||||
|
@@ -200,10 +200,16 @@ class OptionsView {
|
||||
panel (border : titledBorder(title : "Search Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP),
|
||||
constraints : gbc(gridx : 0, gridy : 1, fill : GridBagConstraints.HORIZONTAL, weightx : 100)) {
|
||||
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},
|
||||
constraints : gbc(gridx : 1, gridy:0, anchor : GridBagConstraints.LINE_END))
|
||||
button(text : "Clear history", constraints : gbc(gridx : 1, gridy : 1, anchor : GridBagConstraints.LINE_END), clearHistoryAction)
|
||||
constraints : gbc(gridx : 1, gridy:1, anchor : GridBagConstraints.LINE_END))
|
||||
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),
|
||||
@@ -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))
|
||||
excludeLocalResultCheckbox = checkBox(selected : bind {model.excludeLocalResult},
|
||||
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},
|
||||
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))
|
||||
|
@@ -17,6 +17,7 @@ import javax.swing.ListSelectionModel
|
||||
import javax.swing.SwingConstants
|
||||
import javax.swing.table.DefaultTableCellRenderer
|
||||
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
import com.muwire.core.util.DataUtil
|
||||
@@ -38,80 +39,175 @@ class SearchTabView {
|
||||
FactoryBuilderSupport builder
|
||||
@MVCMember @Nonnull
|
||||
SearchTabModel model
|
||||
|
||||
UISettings settings
|
||||
|
||||
def pane
|
||||
def parent
|
||||
def searchTerms
|
||||
def sendersTable
|
||||
def sendersTable, sendersTable2
|
||||
def lastSendersSortEvent
|
||||
def resultsTable
|
||||
def resultsTable, resultsTable2
|
||||
def lastSortEvent
|
||||
def lastResults2SortEvent, lastSenders2SortEvent
|
||||
def sequentialDownloadCheckbox
|
||||
def sequentialDownloadCheckbox2
|
||||
|
||||
void initUI() {
|
||||
int rowHeight = application.context.get("row-height")
|
||||
builder.with {
|
||||
def resultsTable
|
||||
def sendersTable
|
||||
def sequentialDownloadCheckbox
|
||||
def resultsTable, resultsTable2
|
||||
def sendersTable, sendersTable2
|
||||
def sequentialDownloadCheckbox, sequentialDownloadCheckbox2
|
||||
def pane = panel {
|
||||
gridLayout(rows :1, cols : 1)
|
||||
splitPane(orientation: JSplitPane.VERTICAL_SPLIT, continuousLayout : true, dividerLocation: 300 ) {
|
||||
panel {
|
||||
borderLayout()
|
||||
scrollPane (constraints : BorderLayout.CENTER) {
|
||||
sendersTable = table(id : "senders-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
|
||||
tableModel(list : model.senders) {
|
||||
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()
|
||||
})
|
||||
borderLayout()
|
||||
panel (id : "results-panel", constraints : BorderLayout.CENTER) {
|
||||
cardLayout()
|
||||
panel (constraints : "grouped-by-sender"){
|
||||
gridLayout(rows :1, cols : 1)
|
||||
splitPane(orientation: JSplitPane.VERTICAL_SPLIT, continuousLayout : true, dividerLocation: 300 ) {
|
||||
panel {
|
||||
borderLayout()
|
||||
scrollPane (constraints : BorderLayout.CENTER) {
|
||||
sendersTable = table(id : "senders-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
|
||||
tableModel(list : model.senders) {
|
||||
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 {
|
||||
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})
|
||||
panel (constraints : "grouped-by-file") {
|
||||
gridLayout(rows : 1, cols : 1)
|
||||
splitPane(orientation: JSplitPane.VERTICAL_SPLIT, continuousLayout : true, dividerLocation: 300 ) {
|
||||
panel {
|
||||
borderLayout()
|
||||
scrollPane(constraints : BorderLayout.CENTER) {
|
||||
resultsTable2 = table(id : "results-table2", autoCreateRowSorter : true, rowHeight : rowHeight) {
|
||||
tableModel(list : model.results2) {
|
||||
closureColumn(header : "Name", preferredWidth : 350, type : String, read : {
|
||||
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
|
||||
@@ -120,7 +216,10 @@ class SearchTabView {
|
||||
|
||||
this.resultsTable = resultsTable
|
||||
this.sendersTable = sendersTable
|
||||
this.resultsTable2 = resultsTable2
|
||||
this.sendersTable2 = sendersTable2
|
||||
this.sequentialDownloadCheckbox = sequentialDownloadCheckbox
|
||||
this.sequentialDownloadCheckbox2 = sequentialDownloadCheckbox2
|
||||
|
||||
def selectionModel = resultsTable.getSelectionModel()
|
||||
selectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION)
|
||||
@@ -196,9 +295,11 @@ class SearchTabView {
|
||||
def result = getSelectedResult()
|
||||
if (result == null) {
|
||||
model.viewCommentActionEnabled = false
|
||||
model.viewCertificatesActionEnabled = false
|
||||
return
|
||||
} else {
|
||||
model.viewCommentActionEnabled = result.comment != null
|
||||
model.viewCertificatesActionEnabled = result.certificates > 0
|
||||
}
|
||||
})
|
||||
|
||||
@@ -223,7 +324,67 @@ class SearchTabView {
|
||||
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 = {
|
||||
@@ -258,19 +419,47 @@ class SearchTabView {
|
||||
showComment.addActionListener({mvcGroup.controller.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)
|
||||
menu.show(e.getComponent(), e.getX(), e.getY())
|
||||
}
|
||||
|
||||
private UIResultEvent getSelectedResult() {
|
||||
int[] selectedRows = resultsTable.getSelectedRows()
|
||||
if (selectedRows.length != 1)
|
||||
return null
|
||||
int selected = selectedRows[0]
|
||||
if (lastSortEvent != null)
|
||||
selected = resultsTable.rowSorter.convertRowIndexToModel(selected)
|
||||
model.results[selected]
|
||||
UIResultEvent getSelectedResult() {
|
||||
if (model.groupedByFile) {
|
||||
int selectedRow = resultsTable2.getSelectedRow()
|
||||
if (selectedRow < 0)
|
||||
return null
|
||||
if (lastResults2SortEvent != null)
|
||||
selectedRow = resultsTable2.rowSorter.convertRowIndexToModel(selectedRow)
|
||||
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() {
|
||||
@@ -293,11 +482,49 @@ class SearchTabView {
|
||||
}
|
||||
|
||||
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)
|
||||
return -1
|
||||
if (lastSendersSortEvent != null)
|
||||
row = sendersTable.rowSorter.convertRowIndexToModel(row)
|
||||
row
|
||||
return null
|
||||
if (model.groupedByFile)
|
||||
return model.senders2[row]?.sender
|
||||
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()
|
||||
}
|
||||
}
|
154
gui/griffon-app/views/com/muwire/gui/SharedFileView.groovy
Normal file
154
gui/griffon-app/views/com/muwire/gui/SharedFileView.groovy
Normal 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())
|
||||
}
|
||||
}
|
@@ -29,7 +29,7 @@ class ShowCommentView {
|
||||
|
||||
void initUI() {
|
||||
mainFrame = application.windowManager.findWindow("main-frame")
|
||||
dialog = new JDialog(mainFrame, model.result.name, true)
|
||||
dialog = new JDialog(mainFrame, model.name, true)
|
||||
dialog.setResizable(true)
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ class ShowCommentView {
|
||||
borderLayout()
|
||||
panel (constraints : BorderLayout.CENTER) {
|
||||
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) {
|
||||
|
@@ -49,8 +49,9 @@ class TrustListView {
|
||||
scrollPane (constraints : BorderLayout.CENTER){
|
||||
table(id : "trusted-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
|
||||
tableModel(list : model.trusted) {
|
||||
closureColumn(header: "Trusted Users", type : String, read : {it.getHumanReadableName()})
|
||||
closureColumn(header: "Your Trust", type : String, read : {model.trustService.getLevel(it.destination).toString()})
|
||||
closureColumn(header: "Trusted Users", type : String, read : {it.persona.getHumanReadableName()})
|
||||
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 ){
|
||||
table(id : "distrusted-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
|
||||
tableModel(list : model.distrusted) {
|
||||
closureColumn(header: "Distrusted Users", type : String, read : {it.getHumanReadableName()})
|
||||
closureColumn(header: "Your Trust", type : String, read : {model.trustService.getLevel(it.destination).toString()})
|
||||
closureColumn(header: "Distrusted Users", type : String, read : {it.persona.getHumanReadableName()})
|
||||
closureColumn(header: "Reason", type:String, read : {it.reason})
|
||||
closureColumn(header: "Your Trust", type : String, read : {model.trustService.getLevel(it.persona.destination).toString()})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
33
gui/src/main/groovy/com/muwire/gui/DateRenderer.groovy
Normal file
33
gui/src/main/groovy/com/muwire/gui/DateRenderer.groovy
Normal 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
|
||||
}
|
||||
}
|
36
gui/src/main/groovy/com/muwire/gui/DownloadPreviewer.groovy
Normal file
36
gui/src/main/groovy/com/muwire/gui/DownloadPreviewer.groovy
Normal 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)
|
||||
}
|
||||
}
|
@@ -14,6 +14,10 @@ class SizeRenderer extends DefaultTableCellRenderer {
|
||||
@Override
|
||||
JComponent getTableCellRendererComponent(JTable table, Object value,
|
||||
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
|
||||
String formatted = DataHelper.formatSize2Decimal(l, false)+"B"
|
||||
setText(formatted)
|
||||
|
@@ -14,9 +14,11 @@ class UISettings {
|
||||
boolean excludeLocalResult
|
||||
boolean showSearchHashes
|
||||
boolean closeWarning
|
||||
boolean certificateWarning
|
||||
boolean exitOnClose
|
||||
boolean clearUploads
|
||||
boolean storeSearchHistory
|
||||
boolean groupByFile
|
||||
Set<String> searchHistory
|
||||
Set<String> openTabs
|
||||
|
||||
@@ -31,9 +33,11 @@ class UISettings {
|
||||
autoFontSize = Boolean.parseBoolean(props.getProperty("autoFontSize","false"))
|
||||
fontSize = Integer.parseInt(props.getProperty("fontSize","12"))
|
||||
closeWarning = Boolean.parseBoolean(props.getProperty("closeWarning","true"))
|
||||
certificateWarning = Boolean.parseBoolean(props.getProperty("certificateWarning","true"))
|
||||
exitOnClose = Boolean.parseBoolean(props.getProperty("exitOnClose","false"))
|
||||
clearUploads = Boolean.parseBoolean(props.getProperty("clearUploads","false"))
|
||||
storeSearchHistory = Boolean.parseBoolean(props.getProperty("storeSearchHistory","true"))
|
||||
groupByFile = Boolean.parseBoolean(props.getProperty("groupByFile","false"))
|
||||
|
||||
searchHistory = DataUtil.readEncodedSet(props, "searchHistory")
|
||||
openTabs = DataUtil.readEncodedSet(props, "openTabs")
|
||||
@@ -50,9 +54,11 @@ class UISettings {
|
||||
props.setProperty("autoFontSize", String.valueOf(autoFontSize))
|
||||
props.setProperty("fontSize", String.valueOf(fontSize))
|
||||
props.setProperty("closeWarning", String.valueOf(closeWarning))
|
||||
props.setProperty("certificateWarning", String.valueOf(certificateWarning))
|
||||
props.setProperty("exitOnClose", String.valueOf(exitOnClose))
|
||||
props.setProperty("clearUploads", String.valueOf(clearUploads))
|
||||
props.setProperty("storeSearchHistory", String.valueOf(storeSearchHistory))
|
||||
props.setProperty("groupByFile", String.valueOf(groupByFile))
|
||||
if (font != null)
|
||||
props.setProperty("font", font)
|
||||
|
||||
|
Reference in New Issue
Block a user