Compare commits
119 Commits
muwire-0.5
...
muwire-0.5
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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 | ||
![]() |
ff1df88601 | ||
![]() |
4ed572ba51 | ||
![]() |
fd3f55ab4d | ||
![]() |
1358e14467 | ||
![]() |
e22d5fea11 | ||
![]() |
7ade4aa10d | ||
![]() |
a9f623a91a | ||
![]() |
1ce410e943 | ||
![]() |
27aad9d75d | ||
![]() |
24591b10f2 | ||
![]() |
e4f1ea5c10 | ||
![]() |
c73c44c5f2 | ||
![]() |
309cbcc580 | ||
![]() |
86894f242b | ||
![]() |
568255140f | ||
![]() |
f6d2bac5bb | ||
![]() |
1c396711ed | ||
![]() |
c154d9538d | ||
![]() |
8043782446 | ||
![]() |
00c529cca1 | ||
![]() |
094b9ac2b0 | ||
![]() |
0dae0a561b | ||
![]() |
82eaafc2c3 | ||
![]() |
a3fc1a62e7 | ||
![]() |
2fd8f45107 | ||
![]() |
2429bbf59e | ||
![]() |
f7e28e04f6 | ||
![]() |
cc0188f20e | ||
![]() |
af9b4f4679 | ||
![]() |
625a559d02 | ||
![]() |
6e20193d57 | ||
![]() |
88ac267f99 | ||
![]() |
9b3a7473d1 | ||
![]() |
5b0180280e | ||
![]() |
d0462034fc | ||
![]() |
f3e4098107 | ||
![]() |
26e7ca0b21 | ||
![]() |
11007e5f19 | ||
![]() |
ae651cb6bd | ||
![]() |
cad3a88517 | ||
![]() |
29c81646af | ||
![]() |
8a0257927b | ||
![]() |
3b882ae644 | ||
![]() |
5b61738ca9 | ||
![]() |
c77d79513e | ||
![]() |
9f12442897 | ||
![]() |
477b0a47ad | ||
![]() |
7f1041dd96 | ||
![]() |
99393c59bd | ||
![]() |
a78d8c84ca | ||
![]() |
fa9c697bfa | ||
![]() |
e5b12701f5 | ||
![]() |
f69727ab43 | ||
![]() |
d7c7afe2c0 | ||
![]() |
6c806c4441 | ||
![]() |
c4095abdb4 | ||
![]() |
8801546854 | ||
![]() |
f6ee49c0f5 | ||
![]() |
2320d650f6 | ||
![]() |
e9e6e6920a | ||
![]() |
87e5007f39 | ||
![]() |
8df6715e24 | ||
![]() |
6d587bf228 | ||
![]() |
8684452848 | ||
![]() |
7d652fabcb | ||
![]() |
5eb8d75bba | ||
![]() |
9ca8d1738c | ||
![]() |
2bb9480137 | ||
![]() |
7a6365f87a | ||
![]() |
56540ca3ca | ||
![]() |
eb5a5198b1 | ||
![]() |
29562c42ea | ||
![]() |
f5284f9483 | ||
![]() |
9bd3c4f141 | ||
![]() |
817dd68faf | ||
![]() |
5954cdb342 | ||
![]() |
56d44e6458 | ||
![]() |
c6fb76610d |
@@ -4,7 +4,7 @@ MuWire is an easy to use file-sharing program which offers anonymity using [I2P
|
||||
|
||||
It is inspired by the LimeWire Gnutella client and developped by a former LimeWire developer.
|
||||
|
||||
The current stable release - 0.5.3 is avaiable for download at https://muwire.com. You can find technical documentation in the "doc" folder.
|
||||
The current stable release - 0.5.5 is avaiable for download at https://muwire.com. You can find technical documentation in the "doc" folder.
|
||||
|
||||
### Building
|
||||
|
||||
@@ -31,7 +31,7 @@ If you have an I2P router running on the same machine that is all you need to do
|
||||
|
||||
### Running the CLI
|
||||
|
||||
Look inside `cli-lanterna/build/distributions`. Untar/unzip one of the `shadow` files and then run the jar contained inside by typing `java -jar cli-lanterna-x.y.z.jar` in a terminal. The CLI will ask you about the router host and port on startup, no need to edit any files.
|
||||
Look inside `cli-lanterna/build/distributions`. Untar/unzip one of the `shadow` files and then run the jar contained inside by typing `java -jar cli-lanterna-x.y.z.jar` in a terminal. The CLI will ask you about the router host and port on startup, no need to edit any files. However, the CLI does not have an options window yet, so if you need to change any options you will need to edit the configuration files. The CLI options are documented here https://github.com/zlatinb/muwire/wiki/CLI-Configuration-Options
|
||||
|
||||
The CLI is under active development and doesn't have all the features of the GUI.
|
||||
|
||||
|
@@ -11,10 +11,14 @@ buildscript {
|
||||
}
|
||||
|
||||
apply plugin : 'application'
|
||||
mainClassName = 'com.muwire.clilanterna.CliLanterna'
|
||||
application {
|
||||
mainClassName = 'com.muwire.clilanterna.CliLanterna'
|
||||
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties','-Xmx256M']
|
||||
applicationName = 'MuWire-cli'
|
||||
}
|
||||
|
||||
apply plugin : 'com.github.johnrengelman.shadow'
|
||||
|
||||
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
|
||||
|
||||
dependencies {
|
||||
compile project(":core")
|
||||
|
@@ -3,6 +3,8 @@ package com.muwire.clilanterna
|
||||
import com.googlecode.lanterna.gui2.TextBox
|
||||
import com.googlecode.lanterna.gui2.TextGUI
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
|
||||
import com.googlecode.lanterna.TerminalSize
|
||||
import com.googlecode.lanterna.gui2.BasicWindow
|
||||
import com.googlecode.lanterna.gui2.Button
|
||||
@@ -10,6 +12,7 @@ import com.googlecode.lanterna.gui2.GridLayout
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.LayoutData
|
||||
import com.googlecode.lanterna.gui2.Panel
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.files.UICommentEvent
|
||||
@@ -49,11 +52,16 @@ class AddCommentView extends BasicWindow {
|
||||
|
||||
Button saveButton = new Button("Save", {
|
||||
String newComment = textBox.getText()
|
||||
newComment = Base64.encode(DataUtil.encodei18nString(newComment))
|
||||
String encodedOldComment = sharedFile.getComment()
|
||||
sharedFile.setComment(newComment)
|
||||
core.eventBus.publish(new UICommentEvent(sharedFile : sharedFile, oldComment : encodedOldComment))
|
||||
close()
|
||||
if (newComment.length() > Constants.MAX_COMMENT_LENGTH) {
|
||||
String error = "Your comment is too long - ${newComment.length()} bytes. Maximum is $Constants.MAX_COMMENT_LENGTH bytes"
|
||||
MessageDialog.showMessageDialog(textGUI, "Comment Too Long", error, MessageDialogButton.Close)
|
||||
} else {
|
||||
newComment = Base64.encode(DataUtil.encodei18nString(newComment))
|
||||
String encodedOldComment = sharedFile.getComment()
|
||||
sharedFile.setComment(newComment)
|
||||
core.eventBus.publish(new UICommentEvent(sharedFile : sharedFile, oldComment : encodedOldComment))
|
||||
close()
|
||||
}
|
||||
})
|
||||
Button cancelButton = new Button("Cancel", {close()})
|
||||
|
||||
|
@@ -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.4"
|
||||
private static final String MW_VERSION = "0.5.9"
|
||||
|
||||
private static volatile Core core
|
||||
|
||||
@@ -82,14 +82,14 @@ class CliLanterna {
|
||||
props.setDownloadLocation(downloadLocationFile)
|
||||
props.incompleteLocation = incompletesLocationFile
|
||||
|
||||
propsFile.withOutputStream {
|
||||
propsFile.withPrintWriter("UTF-8", {
|
||||
props.write(it)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
props = new Properties()
|
||||
propsFile.withInputStream {
|
||||
propsFile.withReader("UTF-8", {
|
||||
props.load(it)
|
||||
}
|
||||
})
|
||||
props = new MuWireSettings(props)
|
||||
}
|
||||
props.updateType = "cli-lanterna"
|
||||
@@ -104,7 +104,18 @@ class CliLanterna {
|
||||
i2pProps["i2cp.tcp.port"] = String.valueOf(i2pPort)
|
||||
i2pPropsFile.withOutputStream { i2pProps.store(it, "") }
|
||||
}
|
||||
|
||||
|
||||
def cliProps
|
||||
def cliPropsFile = new File(home, "cli.properties")
|
||||
if (cliPropsFile.exists()) {
|
||||
Properties p = new Properties()
|
||||
cliPropsFile.withInputStream {
|
||||
p.load(it)
|
||||
}
|
||||
cliProps = new CliSettings(p)
|
||||
} else
|
||||
cliProps = new CliSettings(new Properties())
|
||||
|
||||
|
||||
Window window = new BasicWindow("MuWire "+ MW_VERSION)
|
||||
window.setHints([Window.Hint.CENTERED])
|
||||
@@ -155,7 +166,7 @@ class CliLanterna {
|
||||
System.exit(1)
|
||||
}
|
||||
|
||||
window = new MainWindowView("MuWire "+MW_VERSION, core, textGUI, screen)
|
||||
window = new MainWindowView("MuWire "+MW_VERSION, core, textGUI, screen, cliProps)
|
||||
core.startServices()
|
||||
|
||||
core.eventBus.publish(new UILoadedEvent())
|
||||
|
@@ -0,0 +1,25 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
class CliSettings {
|
||||
|
||||
boolean clearCancelledDownloads
|
||||
boolean clearFinishedDownloads
|
||||
boolean clearUploads
|
||||
|
||||
CliSettings(Properties props) {
|
||||
clearCancelledDownloads = Boolean.parseBoolean(props.getProperty("clearCancelledDownloads","true"))
|
||||
clearFinishedDownloads = Boolean.parseBoolean(props.getProperty("clearFinishedDownloads", "false"))
|
||||
clearUploads = Boolean.parseBoolean(props.getProperty("clearUploads", "false"))
|
||||
}
|
||||
|
||||
void write(OutputStream os) {
|
||||
Properties props = new Properties()
|
||||
props.with {
|
||||
setProperty("clearCancelledDownloads", String.valueOf(clearCancelledDownloads))
|
||||
setProperty("clearFinishedDownloads", String.valueOf(clearFinishedDownloads))
|
||||
setProperty("clearUploads", String.valueOf(clearUploads))
|
||||
|
||||
store(os, "CLI Properties")
|
||||
}
|
||||
}
|
||||
}
|
@@ -13,15 +13,17 @@ import net.i2p.data.DataHelper
|
||||
class DownloadsModel {
|
||||
private final TextGUIThread guiThread
|
||||
private final Core core
|
||||
private final CliSettings props
|
||||
private final List<Downloader> downloaders = new ArrayList<>()
|
||||
private final TableModel model = new TableModel("Name", "Status", "Progress", "Speed", "ETA")
|
||||
|
||||
|
||||
private long lastRetryTime
|
||||
|
||||
DownloadsModel(TextGUIThread guiThread, Core core) {
|
||||
DownloadsModel(TextGUIThread guiThread, Core core, CliSettings props) {
|
||||
this.guiThread = guiThread
|
||||
this.core = core
|
||||
this.props = props
|
||||
|
||||
core.eventBus.register(DownloadStartedEvent.class, this)
|
||||
Timer timer = new Timer(true)
|
||||
@@ -46,6 +48,14 @@ class DownloadsModel {
|
||||
private void refreshModel() {
|
||||
int rowCount = model.getRowCount()
|
||||
rowCount.times { model.removeRow(0) }
|
||||
|
||||
if (props.clearCancelledDownloads) {
|
||||
downloaders.removeAll { it.cancelled }
|
||||
}
|
||||
if (props.clearFinishedDownloads) {
|
||||
downloaders.removeAll { it.getCurrentState() == Downloader.DownloadState.FINISHED }
|
||||
}
|
||||
|
||||
downloaders.each {
|
||||
String status = it.getCurrentState().toString()
|
||||
int speedInt = it.speed()
|
||||
|
@@ -28,6 +28,7 @@ class DownloadsView extends BasicWindow {
|
||||
this.textGUI = textGUI
|
||||
|
||||
setHints([Window.Hint.EXPANDED])
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false)
|
||||
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(1))
|
||||
@@ -36,10 +37,18 @@ class DownloadsView extends BasicWindow {
|
||||
table.setSelectAction({rowSelected()})
|
||||
table.setTableModel(model.model)
|
||||
table.setVisibleRows(terminalSize.getRows())
|
||||
contentPanel.addComponent(table, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER,true,false))
|
||||
contentPanel.addComponent(table, layoutData)
|
||||
|
||||
Panel buttonsPanel = new Panel()
|
||||
buttonsPanel.setLayoutManager(new GridLayout(2))
|
||||
|
||||
Button clearButton = new Button("Clear Done",{clearDone()})
|
||||
buttonsPanel.addComponent(clearButton, layoutData)
|
||||
|
||||
Button closeButton = new Button("Close",{close()})
|
||||
contentPanel.addComponent(closeButton, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER,true,false))
|
||||
buttonsPanel.addComponent(closeButton, layoutData)
|
||||
|
||||
contentPanel.addComponent(buttonsPanel, layoutData)
|
||||
|
||||
setComponent(contentPanel)
|
||||
closeButton.takeFocus()
|
||||
@@ -75,4 +84,11 @@ class DownloadsView extends BasicWindow {
|
||||
close.takeFocus()
|
||||
textGUI.addWindowAndWait(prompt)
|
||||
}
|
||||
|
||||
private void clearDone() {
|
||||
model.downloaders.removeAll {
|
||||
def state = it.getCurrentState()
|
||||
state == Downloader.DownloadState.CANCELLED || state == Downloader.DownloadState.FINISHED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ import com.googlecode.lanterna.gui2.table.TableModel
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.files.AllFilesLoadedEvent
|
||||
import com.muwire.core.files.DirectoryWatchedEvent
|
||||
import com.muwire.core.files.FileHashedEvent
|
||||
import com.muwire.core.files.FileLoadedEvent
|
||||
import com.muwire.core.files.FileSharedEvent
|
||||
@@ -17,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")
|
||||
private final TableModel model = new TableModel("Name","Size","Comment","Certified","Search Hits","Downloaders")
|
||||
|
||||
FilesModel(TextGUIThread guiThread, Core core) {
|
||||
this.guiThread = guiThread
|
||||
@@ -40,7 +41,7 @@ class FilesModel {
|
||||
def eventBus = core.eventBus
|
||||
guiThread.invokeLater {
|
||||
core.muOptions.watchedDirectories.each {
|
||||
eventBus.publish(new FileSharedEvent(file : new File(it)))
|
||||
eventBus.publish(new FileSharedEvent(file: new File(it)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,7 +72,10 @@ class FilesModel {
|
||||
sharedFiles.each {
|
||||
long size = it.getCachedLength()
|
||||
boolean comment = it.comment != null
|
||||
model.addRow(new SharedFileWrapper(it), DataHelper.formatSize2(size, false)+"B", comment)
|
||||
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, 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")
|
||||
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)
|
||||
|
@@ -30,6 +30,7 @@ import com.muwire.core.update.UpdateAvailableEvent
|
||||
import com.muwire.core.update.UpdateDownloadedEvent
|
||||
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.DataHelper
|
||||
|
||||
class MainWindowView extends BasicWindow {
|
||||
|
||||
@@ -47,17 +48,19 @@ class MainWindowView extends BasicWindow {
|
||||
private final Label connectionCount, incoming, outgoing
|
||||
private final Label known, failing, hopeless
|
||||
private final Label sharedFiles
|
||||
private final Label timesBrowsed
|
||||
private final Label updateStatus
|
||||
private final Label usedRam, totalRam, maxRam
|
||||
|
||||
public MainWindowView(String title, Core core, TextGUI textGUI, Screen screen) {
|
||||
public MainWindowView(String title, Core core, TextGUI textGUI, Screen screen, CliSettings props) {
|
||||
super(title);
|
||||
|
||||
this.core = core
|
||||
this.textGUI = textGUI
|
||||
this.screen = screen
|
||||
|
||||
downloadsModel = new DownloadsModel(textGUI.getGUIThread(),core)
|
||||
uploadsModel = new UploadsModel(textGUI.getGUIThread(), core)
|
||||
downloadsModel = new DownloadsModel(textGUI.getGUIThread(),core, props)
|
||||
uploadsModel = new UploadsModel(textGUI.getGUIThread(), core, props)
|
||||
filesModel = new FilesModel(textGUI.getGUIThread(),core)
|
||||
trustModel = new TrustModel(textGUI.getGUIThread(), core)
|
||||
|
||||
@@ -127,7 +130,11 @@ class MainWindowView extends BasicWindow {
|
||||
failing = new Label("0")
|
||||
hopeless = new Label("0")
|
||||
sharedFiles = new Label("0")
|
||||
timesBrowsed = new Label("0")
|
||||
updateStatus = new Label("Unknown")
|
||||
usedRam = new Label("0")
|
||||
maxRam = new Label("0")
|
||||
totalRam = new Label("0")
|
||||
|
||||
statusPanel.with {
|
||||
addComponent(new Label("Incoming Connections: "), layoutData)
|
||||
@@ -142,8 +149,18 @@ class MainWindowView extends BasicWindow {
|
||||
addComponent(hopeless, layoutData)
|
||||
addComponent(new Label("Shared Files: "), layoutData)
|
||||
addComponent(sharedFiles, layoutData)
|
||||
addComponent(new Label("Times Browsed: "), layoutData)
|
||||
addComponent(timesBrowsed, layoutData)
|
||||
addComponent(new Label("Update Status: "), layoutData)
|
||||
addComponent(updateStatus, layoutData)
|
||||
addComponent(new Label("Java Version: "), layoutData)
|
||||
addComponent(new Label(System.getProperty("java.vendor")+ " " + System.getProperty("java.version")), layoutData)
|
||||
addComponent(new Label("Used Memory: "), layoutData)
|
||||
addComponent(usedRam, layoutData)
|
||||
addComponent(new Label("Total Memory: "), layoutData)
|
||||
addComponent(totalRam, layoutData)
|
||||
addComponent(new Label("Maximum Memory: "), layoutData)
|
||||
addComponent(maxRam, layoutData)
|
||||
}
|
||||
|
||||
refreshStats()
|
||||
@@ -200,8 +217,11 @@ class MainWindowView extends BasicWindow {
|
||||
textGUI.getGUIThread().invokeLater {
|
||||
String label = "$e.version is available with hash $e.infoHash"
|
||||
updateStatus.setText(label)
|
||||
String message = "Version $e.version is available from $e.signer, search for $e.infoHash"
|
||||
MessageDialog.showMessageDialog(textGUI, "Update Available", message, MessageDialogButton.OK)
|
||||
String message = "Version $e.version is available, with hash $e.infoHash . Show details?"
|
||||
def button = MessageDialog.showMessageDialog(textGUI, "Update Available", message, MessageDialogButton.Yes, MessageDialogButton.No)
|
||||
if (button == MessageDialogButton.No)
|
||||
return
|
||||
textGUI.addWindowAndWait(new UpdateTextView(e.text, sizeForTables()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,8 +229,11 @@ class MainWindowView extends BasicWindow {
|
||||
textGUI.getGUIThread().invokeLater {
|
||||
String label = "$e.version downloaded"
|
||||
updateStatus.setText(label)
|
||||
String message = "Version $e.version from $e.signer has been downloaded. You can update now."
|
||||
MessageDialog.showMessageDialog(textGUI, "Update Available", message, MessageDialogButton.OK)
|
||||
String message = "MuWire version $e.version has been downloaded. Show details?."
|
||||
def button = MessageDialog.showMessageDialog(textGUI, "Update Available", message, MessageDialogButton.Yes, MessageDialogButton.No)
|
||||
if (button == MessageDialogButton.No)
|
||||
return
|
||||
textGUI.addWindowAndWait(new UpdateTextView(e.text, sizeForTables()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,6 +284,18 @@ class MainWindowView extends BasicWindow {
|
||||
int failingHosts = core.hostCache.countFailingHosts()
|
||||
int hopelessHosts = core.hostCache.countHopelessHosts()
|
||||
int shared = core.fileManager.fileToSharedFile.size()
|
||||
int browsed = core.connectionAcceptor.browsed
|
||||
long freeMemL = Runtime.getRuntime().freeMemory()
|
||||
long totalMemL = Runtime.getRuntime().totalMemory()
|
||||
String usedMem = DataHelper.formatSize2Decimal(freeMemL, false) + "B"
|
||||
String totalMem = DataHelper.formatSize2Decimal(totalMemL, false)+"B"
|
||||
String maxMem
|
||||
long maxMemL = Runtime.getRuntime().maxMemory()
|
||||
if (maxMemL >= Long.MAX_VALUE / 2)
|
||||
maxMem = "Unlimited"
|
||||
else
|
||||
maxMem = DataHelper.formatSize2Decimal(maxMemL, false) + "B"
|
||||
|
||||
|
||||
incoming.setText(String.valueOf(inCon))
|
||||
outgoing.setText(String.valueOf(outCon))
|
||||
@@ -268,5 +303,9 @@ class MainWindowView extends BasicWindow {
|
||||
failing.setText(String.valueOf(failingHosts))
|
||||
hopeless.setText(String.valueOf(hopelessHosts))
|
||||
sharedFiles.setText(String.valueOf(shared))
|
||||
timesBrowsed.setText(String.valueOf(browsed))
|
||||
usedRam.setText(usedMem)
|
||||
totalRam.setText(totalMem)
|
||||
maxRam.setText(maxMem)
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
|
@@ -8,7 +8,11 @@ import com.muwire.core.search.SearchEvent
|
||||
import com.muwire.core.search.UIResultBatchEvent
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
|
||||
import net.i2p.crypto.DSAEngine
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Signature
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
import com.googlecode.lanterna.gui2.TextGUIThread
|
||||
import com.googlecode.lanterna.gui2.table.TableModel
|
||||
@@ -40,20 +44,24 @@ class SearchModel {
|
||||
}
|
||||
|
||||
def searchEvent
|
||||
byte [] payload
|
||||
if (hashSearch) {
|
||||
searchEvent = new SearchEvent(searchHash : root, uuid : UUID.randomUUID(), 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,
|
||||
searchComments : core.muOptions.searchComments, compressedResults : true)
|
||||
}
|
||||
|
||||
boolean firstHop = core.muOptions.allowUntrusted || core.muOptions.searchExtraHop
|
||||
|
||||
Signature sig = DSAEngine.getInstance().sign(payload, core.spk)
|
||||
|
||||
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : firstHop,
|
||||
replyTo: core.me.destination, receivedOn: core.me.destination,
|
||||
originator : core.me))
|
||||
originator : core.me, sig: sig.data))
|
||||
}
|
||||
|
||||
void unregister() {
|
||||
|
@@ -199,6 +199,6 @@ class TrustView extends BasicWindow {
|
||||
|
||||
private void saveMuSettings() {
|
||||
File settingsFile = new File(core.home,"MuWire.properties")
|
||||
settingsFile.withOutputStream { core.muOptions.write(it) }
|
||||
settingsFile.withPrintWriter("UTF-8",{ core.muOptions.write(it) })
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,34 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.TerminalSize
|
||||
import com.googlecode.lanterna.gui2.BasicWindow
|
||||
import com.googlecode.lanterna.gui2.Button
|
||||
import com.googlecode.lanterna.gui2.GridLayout
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.LayoutData
|
||||
import com.googlecode.lanterna.gui2.Panel
|
||||
import com.googlecode.lanterna.gui2.TextBox
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
|
||||
class UpdateTextView extends BasicWindow {
|
||||
private final TextBox textBox
|
||||
private final LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
|
||||
UpdateTextView(String text, TerminalSize terminalSize) {
|
||||
super("Update Details")
|
||||
|
||||
setHints([Window.Hint.CENTERED])
|
||||
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(1))
|
||||
|
||||
TerminalSize boxSize = new TerminalSize((terminalSize.getColumns() / 2).toInteger(), (terminalSize.getRows() / 2).toInteger())
|
||||
textBox = new TextBox(boxSize, text, TextBox.Style.MULTI_LINE)
|
||||
contentPanel.addComponent(textBox, layoutData)
|
||||
|
||||
Button closeButton = new Button("Close", {close()})
|
||||
contentPanel.addComponent(closeButton, layoutData)
|
||||
|
||||
setComponent(contentPanel)
|
||||
}
|
||||
}
|
@@ -13,12 +13,14 @@ import net.i2p.data.DataHelper
|
||||
class UploadsModel {
|
||||
private final TextGUIThread guiThread
|
||||
private final Core core
|
||||
private final List<Uploader> uploaders = new ArrayList<>()
|
||||
private final TableModel model = new TableModel("Name","Progress","Downloader","Remote Pieces")
|
||||
private CliSettings props
|
||||
private final List<UploaderWrapper> uploaders = new ArrayList<>()
|
||||
private final TableModel model = new TableModel("Name","Progress","Downloader","Remote Pieces", "Speed")
|
||||
|
||||
UploadsModel(TextGUIThread guiThread, Core core) {
|
||||
UploadsModel(TextGUIThread guiThread, Core core, CliSettings props) {
|
||||
this.guiThread = guiThread
|
||||
this.core = core
|
||||
this.props = props
|
||||
|
||||
core.eventBus.register(UploadEvent.class, this)
|
||||
core.eventBus.register(UploadFinishedEvent.class, this)
|
||||
@@ -32,35 +34,74 @@ class UploadsModel {
|
||||
}
|
||||
|
||||
void onUploadEvent(UploadEvent e) {
|
||||
guiThread.invokeLater({uploaders.add(e.uploader)})
|
||||
guiThread.invokeLater {
|
||||
UploaderWrapper found = null
|
||||
uploaders.each {
|
||||
if (it.uploader == e.uploader) {
|
||||
found = it
|
||||
return
|
||||
}
|
||||
}
|
||||
if (found != null) {
|
||||
found.uploader = e.uploader
|
||||
found.finished = false
|
||||
} else
|
||||
uploaders << new UploaderWrapper(uploader : e.uploader)
|
||||
}
|
||||
}
|
||||
|
||||
void onUploadFinishedEvent(UploadFinishedEvent e) {
|
||||
guiThread.invokeLater({uploaders.remove(e.uploader)})
|
||||
guiThread.invokeLater {
|
||||
uploaders.each {
|
||||
if (it.uploader == e.uploader) {
|
||||
it.finished = true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshModel() {
|
||||
int uploadersSize = model.getRowCount()
|
||||
uploadersSize.times { model.removeRow(0) }
|
||||
|
||||
|
||||
if (props.clearUploads) {
|
||||
uploaders.removeAll { it.finished }
|
||||
}
|
||||
|
||||
uploaders.each {
|
||||
String name = it.getName()
|
||||
int percent = it.getProgress()
|
||||
String name = it.uploader.getName()
|
||||
int percent = it.uploader.getProgress()
|
||||
String percentString = "$percent% of piece".toString()
|
||||
String downloader = it.getDownloader()
|
||||
String downloader = it.uploader.getDownloader()
|
||||
|
||||
int pieces = it.getTotalPieces()
|
||||
int done = it.getDonePieces()
|
||||
int pieces = it.uploader.getTotalPieces()
|
||||
int done = it.uploader.getDonePieces()
|
||||
if (percent == 100)
|
||||
done++
|
||||
int percentTotal = -1
|
||||
if (pieces != 0)
|
||||
percentTotal = (done * 100) / pieces
|
||||
long size = it.getTotalSize()
|
||||
long size = it.uploader.getTotalSize()
|
||||
String totalSize = ""
|
||||
if (size > 0)
|
||||
totalSize = " of " + DataHelper.formatSize2Decimal(size, false) + "B"
|
||||
String remotePieces = String.format("%02d", percentTotal) + "% ${totalSize} ($done/$pieces) pcs".toString()
|
||||
|
||||
model.addRow([name, percentString, downloader, remotePieces])
|
||||
String speed = DataHelper.formatSize2Decimal(it.uploader.speed(), false) + "B/sec"
|
||||
|
||||
|
||||
model.addRow([name, percentString, downloader, remotePieces, speed])
|
||||
}
|
||||
}
|
||||
|
||||
private static class UploaderWrapper {
|
||||
Uploader uploader
|
||||
boolean finished
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
uploader.getName()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -24,14 +24,24 @@ class UploadsView extends BasicWindow {
|
||||
contentPanel.setLayoutManager(new GridLayout(1))
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false)
|
||||
|
||||
table = new Table("Name","Progress","Downloader","Remote Pieces")
|
||||
table = new Table("Name","Progress","Downloader","Remote Pieces","Speed")
|
||||
table.setCellSelection(false)
|
||||
table.setTableModel(model.model)
|
||||
table.setVisibleRows(terminalSize.getRows())
|
||||
contentPanel.addComponent(table, layoutData)
|
||||
|
||||
Panel buttonsPanel = new Panel()
|
||||
buttonsPanel.setLayoutManager(new GridLayout(2))
|
||||
|
||||
Button clearDoneButton = new Button("Clear Finished",{
|
||||
model.uploaders.removeAll { it.finished }
|
||||
})
|
||||
Button closeButton = new Button("Close",{close()})
|
||||
contentPanel.addComponent(closeButton, layoutData)
|
||||
|
||||
buttonsPanel.addComponent(clearDoneButton, layoutData)
|
||||
buttonsPanel.addComponent(closeButton, layoutData)
|
||||
|
||||
contentPanel.addComponent(buttonsPanel, layoutData)
|
||||
|
||||
setComponent(contentPanel)
|
||||
closeButton.takeFocus()
|
||||
|
@@ -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
|
||||
@@ -28,10 +33,12 @@ import com.muwire.core.files.FileSharedEvent
|
||||
import com.muwire.core.files.FileUnsharedEvent
|
||||
import com.muwire.core.files.HasherService
|
||||
import com.muwire.core.files.PersisterService
|
||||
import com.muwire.core.files.SideCarFileEvent
|
||||
import com.muwire.core.files.UICommentEvent
|
||||
import com.muwire.core.files.UIPersistFilesEvent
|
||||
import com.muwire.core.files.AllFilesLoadedEvent
|
||||
import com.muwire.core.files.DirectoryUnsharedEvent
|
||||
import com.muwire.core.files.DirectoryWatchedEvent
|
||||
import com.muwire.core.files.DirectoryWatcher
|
||||
import com.muwire.core.hostcache.CacheClient
|
||||
import com.muwire.core.hostcache.HostCache
|
||||
@@ -97,10 +104,13 @@ public class Core {
|
||||
final FileManager fileManager
|
||||
final UploadManager uploadManager
|
||||
final ContentManager contentManager
|
||||
final CertificateManager certificateManager
|
||||
|
||||
private final Router router
|
||||
|
||||
final AtomicBoolean shutdown = new AtomicBoolean()
|
||||
|
||||
final SigningPrivateKey spk
|
||||
|
||||
public Core(MuWireSettings props, File home, String myVersion) {
|
||||
this.home = home
|
||||
@@ -178,7 +188,7 @@ public class Core {
|
||||
i2pSession = socketManager.getSession()
|
||||
|
||||
def destination = new Destination()
|
||||
def spk = new SigningPrivateKey(Constants.SIG_TYPE)
|
||||
spk = new SigningPrivateKey(Constants.SIG_TYPE)
|
||||
keyDat.withInputStream {
|
||||
destination.readBytes(it)
|
||||
def privateKey = new PrivateKey()
|
||||
@@ -189,8 +199,9 @@ public class Core {
|
||||
def baos = new ByteArrayOutputStream()
|
||||
def daos = new DataOutputStream(baos)
|
||||
daos.write(Constants.PERSONA_VERSION)
|
||||
daos.writeShort((short)props.getNickname().length())
|
||||
daos.write(props.getNickname().getBytes(StandardCharsets.UTF_8))
|
||||
byte [] name = props.getNickname().getBytes(StandardCharsets.UTF_8)
|
||||
daos.writeShort((short)name.length)
|
||||
daos.write(name)
|
||||
destination.writeBytes(daos)
|
||||
daos.flush()
|
||||
byte [] payload = baos.toByteArray()
|
||||
@@ -204,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")
|
||||
@@ -220,6 +237,7 @@ public class Core {
|
||||
eventBus.register(SearchEvent.class, fileManager)
|
||||
eventBus.register(DirectoryUnsharedEvent.class, fileManager)
|
||||
eventBus.register(UICommentEvent.class, fileManager)
|
||||
eventBus.register(SideCarFileEvent.class, fileManager)
|
||||
|
||||
log.info("initializing mesh manager")
|
||||
MeshManager meshManager = new MeshManager(fileManager, home, props)
|
||||
@@ -249,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)
|
||||
@@ -275,7 +297,7 @@ public class Core {
|
||||
eventBus.register(UIDownloadResumedEvent.class, downloadManager)
|
||||
|
||||
log.info("initializing upload manager")
|
||||
uploadManager = new UploadManager(eventBus, fileManager, meshManager, downloadManager)
|
||||
uploadManager = new UploadManager(eventBus, fileManager, meshManager, downloadManager, props)
|
||||
|
||||
log.info("initializing connection establisher")
|
||||
connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache)
|
||||
@@ -283,11 +305,12 @@ 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)
|
||||
eventBus.register(FileSharedEvent.class, directoryWatcher)
|
||||
eventBus.register(DirectoryWatchedEvent.class, directoryWatcher)
|
||||
eventBus.register(AllFilesLoadedEvent.class, directoryWatcher)
|
||||
eventBus.register(DirectoryUnsharedEvent.class, directoryWatcher)
|
||||
|
||||
@@ -308,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)
|
||||
|
||||
}
|
||||
@@ -331,6 +354,8 @@ public class Core {
|
||||
log.info("already shutting down")
|
||||
return
|
||||
}
|
||||
log.info("saving settings")
|
||||
saveMuSettings()
|
||||
log.info("shutting down trust subscriber")
|
||||
trustSubscriber.stop()
|
||||
log.info("shutting down download manageer")
|
||||
@@ -349,6 +374,12 @@ public class Core {
|
||||
log.info("shutting down embedded router")
|
||||
router.shutdown(0)
|
||||
}
|
||||
log.info("shutdown complete")
|
||||
}
|
||||
|
||||
public void saveMuSettings() {
|
||||
File f = new File(home, "MuWire.properties")
|
||||
f.withPrintWriter("UTF-8", { muOptions.write(it) })
|
||||
}
|
||||
|
||||
static main(args) {
|
||||
@@ -375,7 +406,7 @@ public class Core {
|
||||
}
|
||||
}
|
||||
|
||||
Core core = new Core(props, home, "0.5.3")
|
||||
Core core = new Core(props, home, "0.5.9")
|
||||
core.startServices()
|
||||
|
||||
// ... at the end, sleep or execute script
|
||||
|
@@ -17,7 +17,10 @@ class MuWireSettings {
|
||||
int trustListInterval
|
||||
Set<Persona> trustSubscriptions
|
||||
int downloadRetryInterval
|
||||
int totalUploadSlots
|
||||
int uploadSlotsPerUser
|
||||
int updateCheckInterval
|
||||
long lastUpdateCheck
|
||||
boolean autoDownloadUpdate
|
||||
String updateType
|
||||
String nickname
|
||||
@@ -37,6 +40,7 @@ class MuWireSettings {
|
||||
int inBw, outBw
|
||||
Set<String> watchedKeywords
|
||||
Set<String> watchedRegexes
|
||||
Set<String> negativeFileTree
|
||||
|
||||
MuWireSettings() {
|
||||
this(new Properties())
|
||||
@@ -57,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"))
|
||||
@@ -72,10 +77,13 @@ class MuWireSettings {
|
||||
searchComments = Boolean.valueOf(props.getProperty("searchComments","true"))
|
||||
browseFiles = Boolean.valueOf(props.getProperty("browseFiles","true"))
|
||||
speedSmoothSeconds = Integer.valueOf(props.getProperty("speedSmoothSeconds","60"))
|
||||
totalUploadSlots = Integer.valueOf(props.getProperty("totalUploadSlots","-1"))
|
||||
uploadSlotsPerUser = Integer.valueOf(props.getProperty("uploadSlotsPerUser","-1"))
|
||||
|
||||
watchedDirectories = readEncodedSet(props, "watchedDirectories")
|
||||
watchedKeywords = readEncodedSet(props, "watchedKeywords")
|
||||
watchedRegexes = readEncodedSet(props, "watchedRegexes")
|
||||
watchedDirectories = DataUtil.readEncodedSet(props, "watchedDirectories")
|
||||
watchedKeywords = DataUtil.readEncodedSet(props, "watchedKeywords")
|
||||
watchedRegexes = DataUtil.readEncodedSet(props, "watchedRegexes")
|
||||
negativeFileTree = DataUtil.readEncodedSet(props, "negativeFileTree")
|
||||
|
||||
trustSubscriptions = new HashSet<>()
|
||||
if (props.containsKey("trustSubscriptions")) {
|
||||
@@ -87,7 +95,7 @@ class MuWireSettings {
|
||||
|
||||
}
|
||||
|
||||
void write(OutputStream out) throws IOException {
|
||||
void write(Writer out) throws IOException {
|
||||
Properties props = new Properties()
|
||||
props.setProperty("leaf", isLeaf.toString())
|
||||
props.setProperty("allowUntrusted", allowUntrusted.toString())
|
||||
@@ -101,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))
|
||||
@@ -116,10 +125,13 @@ class MuWireSettings {
|
||||
props.setProperty("searchComments", String.valueOf(searchComments))
|
||||
props.setProperty("browseFiles", String.valueOf(browseFiles))
|
||||
props.setProperty("speedSmoothSeconds", String.valueOf(speedSmoothSeconds))
|
||||
props.setProperty("totalUploadSlots", String.valueOf(totalUploadSlots))
|
||||
props.setProperty("uploadSlotsPerUser", String.valueOf(uploadSlotsPerUser))
|
||||
|
||||
writeEncodedSet(watchedDirectories, "watchedDirectories", props)
|
||||
writeEncodedSet(watchedKeywords, "watchedKeywords", props)
|
||||
writeEncodedSet(watchedRegexes, "watchedRegexes", props)
|
||||
DataUtil.writeEncodedSet(watchedDirectories, "watchedDirectories", props)
|
||||
DataUtil.writeEncodedSet(watchedKeywords, "watchedKeywords", props)
|
||||
DataUtil.writeEncodedSet(watchedRegexes, "watchedRegexes", props)
|
||||
DataUtil.writeEncodedSet(negativeFileTree, "negativeFileTree", props)
|
||||
|
||||
if (!trustSubscriptions.isEmpty()) {
|
||||
String encoded = trustSubscriptions.stream().
|
||||
@@ -128,25 +140,7 @@ class MuWireSettings {
|
||||
props.setProperty("trustSubscriptions", encoded)
|
||||
}
|
||||
|
||||
props.store(out, "")
|
||||
}
|
||||
|
||||
private static Set<String> readEncodedSet(Properties props, String property) {
|
||||
Set<String> rv = new ConcurrentHashSet<>()
|
||||
if (props.containsKey(property)) {
|
||||
String[] encoded = props.getProperty(property).split(",")
|
||||
encoded.each { rv << DataUtil.readi18nString(Base64.decode(it)) }
|
||||
}
|
||||
rv
|
||||
}
|
||||
|
||||
private static void writeEncodedSet(Set<String> set, String property, Properties props) {
|
||||
if (set.isEmpty())
|
||||
return
|
||||
String encoded = set.stream().
|
||||
map({Base64.encode(DataUtil.encodei18nString(it))}).
|
||||
collect(Collectors.joining(","))
|
||||
props.setProperty(property, encoded)
|
||||
props.store(out, "This file is UTF-8")
|
||||
}
|
||||
|
||||
boolean isLeaf() {
|
||||
|
@@ -1,45 +0,0 @@
|
||||
package com.muwire.core
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
/**
|
||||
* A name of persona, file or search term
|
||||
*/
|
||||
public class Name {
|
||||
final String name
|
||||
|
||||
Name(String name) {
|
||||
this.name = name
|
||||
}
|
||||
|
||||
Name(InputStream nameStream) throws IOException {
|
||||
DataInputStream dis = new DataInputStream(nameStream)
|
||||
int length = dis.readUnsignedShort()
|
||||
byte [] nameBytes = new byte[length]
|
||||
dis.readFully(nameBytes)
|
||||
this.name = new String(nameBytes, StandardCharsets.UTF_8)
|
||||
}
|
||||
|
||||
public void write(OutputStream out) throws IOException {
|
||||
DataOutputStream dos = new DataOutputStream(out)
|
||||
dos.writeShort(name.length())
|
||||
dos.write(name.getBytes(StandardCharsets.UTF_8))
|
||||
}
|
||||
|
||||
public getName() {
|
||||
name
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
name.hashCode()
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof Name))
|
||||
return false
|
||||
Name other = (Name)o
|
||||
name.equals(other.name)
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,10 +1,17 @@
|
||||
package com.muwire.core.connection
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.BlockingQueue
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
import java.util.concurrent.ThreadFactory
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.logging.Level
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.Persona
|
||||
@@ -16,12 +23,14 @@ import com.muwire.core.trust.TrustLevel
|
||||
import com.muwire.core.trust.TrustService
|
||||
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.crypto.DSAEngine
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Destination
|
||||
import net.i2p.data.Signature
|
||||
|
||||
@Log
|
||||
abstract class Connection implements Closeable {
|
||||
|
||||
|
||||
private static final int SEARCHES = 10
|
||||
private static final long INTERVAL = 1000
|
||||
|
||||
@@ -83,6 +92,7 @@ abstract class Connection implements Closeable {
|
||||
reader.interrupt()
|
||||
writer.interrupt()
|
||||
endpoint.close()
|
||||
log.info("closed $name")
|
||||
eventBus.publish(new DisconnectionEvent(destination: endpoint.destination))
|
||||
}
|
||||
|
||||
@@ -91,6 +101,7 @@ abstract class Connection implements Closeable {
|
||||
while(running.get()) {
|
||||
read()
|
||||
}
|
||||
} catch (InterruptedException ok) {
|
||||
} catch (SocketTimeoutException e) {
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING,"unhandled exception in reader",e)
|
||||
@@ -107,6 +118,7 @@ abstract class Connection implements Closeable {
|
||||
def message = messages.take()
|
||||
write(message)
|
||||
}
|
||||
} catch (InterruptedException ok) {
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING, "unhandled exception in writer",e)
|
||||
} finally {
|
||||
@@ -139,6 +151,8 @@ abstract class Connection implements Closeable {
|
||||
query.replyTo = e.replyTo.toBase64()
|
||||
if (e.originator != null)
|
||||
query.originator = e.originator.toBase64()
|
||||
if (e.sig != null)
|
||||
query.sig = Base64.encode(e.sig)
|
||||
messages.put(query)
|
||||
}
|
||||
|
||||
@@ -217,18 +231,38 @@ abstract class Connection implements Closeable {
|
||||
boolean compressedResults = false
|
||||
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
|
||||
if (infohash != null)
|
||||
payload = infohash
|
||||
else
|
||||
payload = String.join(" ",search.keywords).getBytes(StandardCharsets.UTF_8)
|
||||
def spk = originator.destination.getSigningPublicKey()
|
||||
def signature = new Signature(Constants.SIG_TYPE, sig)
|
||||
if (!DSAEngine.getInstance().verifySignature(signature, payload, spk)) {
|
||||
log.info("signature didn't match keywords")
|
||||
return
|
||||
} else
|
||||
log.info("query signature verified")
|
||||
} else
|
||||
log.info("no signature in query")
|
||||
|
||||
SearchEvent searchEvent = new SearchEvent(searchTerms : search.keywords,
|
||||
searchHash : infohash,
|
||||
uuid : uuid,
|
||||
oobInfohash : oob,
|
||||
searchComments : searchComments,
|
||||
compressedResults : compressedResults)
|
||||
compressedResults : compressedResults,
|
||||
persona : originator)
|
||||
QueryEvent event = new QueryEvent ( searchEvent : searchEvent,
|
||||
replyTo : replyTo,
|
||||
originator : originator,
|
||||
receivedOn : endpoint.destination,
|
||||
firstHop : search.firstHop )
|
||||
firstHop : search.firstHop,
|
||||
sig : sig )
|
||||
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,16 +49,19 @@ class ConnectionAcceptor {
|
||||
final UploadManager uploadManager
|
||||
final FileManager fileManager
|
||||
final ConnectionEstablisher establisher
|
||||
final CertificateManager certificateManager
|
||||
|
||||
final ExecutorService acceptorThread
|
||||
final ExecutorService handshakerThreads
|
||||
|
||||
private volatile shutdown
|
||||
|
||||
private volatile int browsed
|
||||
|
||||
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
|
||||
@@ -65,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)
|
||||
@@ -143,11 +151,15 @@ 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)
|
||||
e.getOutputStream().close()
|
||||
e.close()
|
||||
eventBus.publish new ConnectionEvent(endpoint: e, incoming: true, leaf: null, status: ConnectionAttemptStatus.FAILED)
|
||||
}
|
||||
@@ -196,7 +208,7 @@ class ConnectionAcceptor {
|
||||
os.writeShort(json.bytes.length)
|
||||
os.write(json.bytes)
|
||||
}
|
||||
e.outputStream.flush()
|
||||
e.outputStream.close()
|
||||
e.close()
|
||||
eventBus.publish(new ConnectionEvent(endpoint: e, incoming: true, leaf: leaf, status: ConnectionAttemptStatus.REJECTED))
|
||||
}
|
||||
@@ -276,18 +288,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")
|
||||
@@ -328,8 +330,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) {
|
||||
@@ -339,7 +347,8 @@ class ConnectionAcceptor {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
browsed++
|
||||
|
||||
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
def sharedFiles = fileManager.getSharedFiles().values()
|
||||
@@ -349,7 +358,9 @@ class ConnectionAcceptor {
|
||||
DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))
|
||||
JsonOutput jsonOutput = new JsonOutput()
|
||||
sharedFiles.each {
|
||||
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))
|
||||
@@ -402,5 +413,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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -36,9 +36,8 @@ abstract class ConnectionManager {
|
||||
timer.schedule({sendPings()} as TimerTask, 1000,1000)
|
||||
}
|
||||
|
||||
void stop() {
|
||||
void shutdown() {
|
||||
timer.cancel()
|
||||
getConnections().each { it.close() }
|
||||
}
|
||||
|
||||
void onTrustEvent(TrustEvent e) {
|
||||
@@ -62,8 +61,6 @@ abstract class ConnectionManager {
|
||||
|
||||
abstract void onDisconnectionEvent(DisconnectionEvent e)
|
||||
|
||||
abstract void shutdown()
|
||||
|
||||
protected void sendPings() {
|
||||
final long now = System.currentTimeMillis()
|
||||
getConnections().each {
|
||||
|
@@ -31,9 +31,6 @@ class Endpoint implements Closeable {
|
||||
if (inputStream != null) {
|
||||
try {inputStream.close()} catch (Exception ignore) {}
|
||||
}
|
||||
if (outputStream != null) {
|
||||
try {outputStream.close()} catch (Exception ignore) {}
|
||||
}
|
||||
if (toClose != null) {
|
||||
try {toClose.reset()} catch (Exception ignore) {}
|
||||
}
|
||||
|
@@ -104,6 +104,7 @@ class UltrapeerConnectionManager extends ConnectionManager {
|
||||
|
||||
@Override
|
||||
void shutdown() {
|
||||
super.shutdown()
|
||||
peerConnections.values().stream().parallel().forEach({v -> v.close()})
|
||||
leafConnections.values().stream().parallel().forEach({v -> v.close()})
|
||||
peerConnections.clear()
|
||||
|
@@ -12,6 +12,7 @@ import com.muwire.core.util.DataUtil
|
||||
import groovy.json.JsonBuilder
|
||||
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
|
||||
@@ -25,7 +26,9 @@ import com.muwire.core.UILoadedEvent
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.logging.Level
|
||||
|
||||
@Log
|
||||
public class DownloadManager {
|
||||
|
||||
private final EventBus eventBus
|
||||
@@ -135,22 +138,33 @@ public class DownloadManager {
|
||||
else
|
||||
incompletes = new File(home, "incompletes")
|
||||
|
||||
if (json.pieceSizePow2 == null || json.pieceSizePow2 == 0) {
|
||||
log.warning("Skipping $file because pieceSizePow2=$json.pieceSizePow2")
|
||||
return // skip this download as it's corrupt anyway
|
||||
}
|
||||
|
||||
Pieces pieces = getPieces(infoHash, (long)json.length, json.pieceSizePow2, sequential)
|
||||
|
||||
def downloader = new Downloader(eventBus, this, me, file, (long)json.length,
|
||||
infoHash, json.pieceSizePow2, connector, destinations, incompletes, pieces)
|
||||
if (json.paused != null)
|
||||
downloader.paused = json.paused
|
||||
downloaders.put(infoHash, downloader)
|
||||
downloader.readPieces()
|
||||
if (!downloader.paused)
|
||||
downloader.download()
|
||||
eventBus.publish(new DownloadStartedEvent(downloader : downloader))
|
||||
|
||||
try {
|
||||
downloader.readPieces()
|
||||
if (!downloader.paused)
|
||||
downloader.download()
|
||||
downloaders.put(infoHash, downloader)
|
||||
eventBus.publish(new DownloadStartedEvent(downloader : downloader))
|
||||
} catch (IllegalArgumentException bad) {
|
||||
log.log(Level.WARNING,"cannot start downloader, skipping", bad)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Pieces getPieces(InfoHash infoHash, long length, int pieceSizePow2, boolean sequential) {
|
||||
int pieceSize = 0x1 << pieceSizePow2
|
||||
long pieceSize = 0x1L << pieceSizePow2
|
||||
int nPieces = (int)(length / pieceSize)
|
||||
if (length % pieceSize != 0)
|
||||
nPieces++
|
||||
|
@@ -21,6 +21,7 @@ import java.nio.file.Files
|
||||
import java.nio.file.StandardOpenOption
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import java.util.logging.Level
|
||||
|
||||
@Log
|
||||
@@ -37,13 +38,12 @@ class DownloadSession {
|
||||
private final Set<Integer> available
|
||||
private final MessageDigest digest
|
||||
|
||||
private long lastSpeedRead = System.currentTimeMillis()
|
||||
private long dataSinceLastRead
|
||||
private final AtomicLong dataSinceLastRead
|
||||
|
||||
private MappedByteBuffer mapped
|
||||
|
||||
DownloadSession(EventBus eventBus, String meB64, Pieces pieces, InfoHash infoHash, Endpoint endpoint, File file,
|
||||
int pieceSize, long fileLength, Set<Integer> available) {
|
||||
int pieceSize, long fileLength, Set<Integer> available, AtomicLong dataSinceLastRead) {
|
||||
this.eventBus = eventBus
|
||||
this.meB64 = meB64
|
||||
this.pieces = pieces
|
||||
@@ -53,6 +53,7 @@ class DownloadSession {
|
||||
this.pieceSize = pieceSize
|
||||
this.fileLength = fileLength
|
||||
this.available = available
|
||||
this.dataSinceLastRead = dataSinceLastRead
|
||||
try {
|
||||
digest = MessageDigest.getInstance("SHA-256")
|
||||
} catch (NoSuchAlgorithmException impossible) {
|
||||
@@ -141,6 +142,8 @@ class DownloadSession {
|
||||
// parse X-Have if present
|
||||
if (headers.containsKey("X-Have")) {
|
||||
DataUtil.decodeXHave(headers["X-Have"]).each {
|
||||
if (it >= pieces.nPieces)
|
||||
throw new IOException("Invalid X-Have header, available piece $it/$pieces.nPieces")
|
||||
available.add(it)
|
||||
}
|
||||
if (!available.contains(piece))
|
||||
@@ -188,7 +191,7 @@ class DownloadSession {
|
||||
throw new IOException()
|
||||
synchronized(this) {
|
||||
mapped.put(tmp, 0, read)
|
||||
dataSinceLastRead += read
|
||||
dataSinceLastRead.addAndGet(read)
|
||||
pieces.markPartial(piece, mapped.position())
|
||||
}
|
||||
}
|
||||
@@ -220,13 +223,4 @@ class DownloadSession {
|
||||
return 0
|
||||
mapped.position()
|
||||
}
|
||||
|
||||
synchronized int speed() {
|
||||
final long now = System.currentTimeMillis()
|
||||
long interval = Math.max(1000, now - lastSpeedRead)
|
||||
lastSpeedRead = now;
|
||||
int rv = (int) (dataSinceLastRead * 1000.0 / interval)
|
||||
dataSinceLastRead = 0
|
||||
rv
|
||||
}
|
||||
}
|
||||
|
@@ -12,6 +12,7 @@ import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import java.util.logging.Level
|
||||
|
||||
import com.muwire.core.Constants
|
||||
@@ -61,10 +62,11 @@ public class Downloader {
|
||||
private final AtomicBoolean eventFired = new AtomicBoolean()
|
||||
private boolean piecesFileClosed
|
||||
|
||||
private final AtomicLong dataSinceLastRead = new AtomicLong(0)
|
||||
private volatile long lastSpeedRead = System.currentTimeMillis()
|
||||
private ArrayList speedArr = new ArrayList<Integer>()
|
||||
private int speedPos = 0
|
||||
private int speedAvg = 0
|
||||
private long timestamp = Instant.now().toEpochMilli()
|
||||
|
||||
public Downloader(EventBus eventBus, DownloadManager downloadManager,
|
||||
Persona me, File file, long length, InfoHash infoHash,
|
||||
@@ -139,10 +141,11 @@ public class Downloader {
|
||||
public int speed() {
|
||||
int currSpeed = 0
|
||||
if (getCurrentState() == DownloadState.DOWNLOADING) {
|
||||
activeWorkers.values().each {
|
||||
if (it.currentState == WorkerState.DOWNLOADING)
|
||||
currSpeed += it.speed()
|
||||
}
|
||||
long dataRead = dataSinceLastRead.getAndSet(0)
|
||||
long now = System.currentTimeMillis()
|
||||
if (now > lastSpeedRead)
|
||||
currSpeed = (int) (dataRead * 1000.0 / (now - lastSpeedRead))
|
||||
lastSpeedRead = now
|
||||
}
|
||||
|
||||
if (speedArr.size() != downloadManager.muSettings.speedSmoothSeconds) {
|
||||
@@ -302,7 +305,7 @@ public class Downloader {
|
||||
boolean requestPerformed
|
||||
while(!pieces.isComplete()) {
|
||||
currentSession = new DownloadSession(eventBus, me.toBase64(), pieces, getInfoHash(),
|
||||
endpoint, incompleteFile, pieceSize, length, available)
|
||||
endpoint, incompleteFile, pieceSize, length, available, dataSinceLastRead)
|
||||
requestPerformed = currentSession.request()
|
||||
if (!requestPerformed)
|
||||
break
|
||||
@@ -339,12 +342,6 @@ public class Downloader {
|
||||
}
|
||||
}
|
||||
|
||||
int speed() {
|
||||
if (currentSession == null)
|
||||
return 0
|
||||
currentSession.speed()
|
||||
}
|
||||
|
||||
void cancel() {
|
||||
downloadThread?.interrupt()
|
||||
}
|
||||
|
@@ -75,6 +75,8 @@ class Pieces {
|
||||
}
|
||||
|
||||
synchronized void markDownloaded(int piece) {
|
||||
if (piece >= nPieces)
|
||||
throw new IllegalArgumentException("invalid piece marked as downloaded? $piece/$nPieces")
|
||||
done.set(piece)
|
||||
claimed.set(piece)
|
||||
partials.remove(piece)
|
||||
|
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
|
||||
}
|
@@ -4,4 +4,8 @@ import com.muwire.core.Event
|
||||
|
||||
class DirectoryUnsharedEvent extends Event {
|
||||
File directory
|
||||
|
||||
public String toString() {
|
||||
super.toString() + " unshared directory "+ directory.toString()
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,7 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class DirectoryWatchedEvent extends Event {
|
||||
File directory
|
||||
}
|
@@ -66,10 +66,8 @@ class DirectoryWatcher {
|
||||
watchService?.close()
|
||||
}
|
||||
|
||||
void onFileSharedEvent(FileSharedEvent e) {
|
||||
if (!e.file.isDirectory())
|
||||
return
|
||||
File canonical = e.file.getCanonicalFile()
|
||||
void onDirectoryWatchedEvent(DirectoryWatchedEvent e) {
|
||||
File canonical = e.directory.getCanonicalFile()
|
||||
Path path = canonical.toPath()
|
||||
WatchKey wk = path.register(watchService, kinds)
|
||||
watchedDirectories.put(canonical, wk)
|
||||
@@ -88,9 +86,9 @@ class DirectoryWatcher {
|
||||
|
||||
private void saveMuSettings() {
|
||||
File muSettingsFile = new File(home, "MuWire.properties")
|
||||
muSettingsFile.withOutputStream {
|
||||
muSettingsFile.withPrintWriter("UTF-8", {
|
||||
muOptions.write(it)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private void watch() {
|
||||
@@ -125,7 +123,8 @@ class DirectoryWatcher {
|
||||
private void processModified(Path parent, Path path) {
|
||||
File f = join(parent, path)
|
||||
log.fine("modified entry $f")
|
||||
waitingFiles.put(f, System.currentTimeMillis())
|
||||
if (!fileManager.getNegativeTree().fileToNode.containsKey(f))
|
||||
waitingFiles.put(f, System.currentTimeMillis())
|
||||
}
|
||||
|
||||
private void processDeleted(Path parent, Path path) {
|
||||
@@ -133,7 +132,7 @@ class DirectoryWatcher {
|
||||
log.fine("deleted entry $f")
|
||||
SharedFile sf = fileManager.fileToSharedFile.get(f)
|
||||
if (sf != null)
|
||||
eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
|
||||
eventBus.publish(new FileUnsharedEvent(unsharedFile : sf, deleted : true))
|
||||
}
|
||||
|
||||
private static File join(Path parent, Path path) {
|
||||
|
@@ -13,8 +13,10 @@ import java.security.NoSuchAlgorithmException
|
||||
|
||||
class FileHasher {
|
||||
|
||||
public static final int MIN_PIECE_SIZE_POW2 = 17
|
||||
public static final int MAX_PIECE_SIZE_POW2 = 37
|
||||
/** max size of shared file is 128 GB */
|
||||
public static final long MAX_SIZE = 0x1L << 37
|
||||
public static final long MAX_SIZE = 0x1L << MAX_PIECE_SIZE_POW2
|
||||
|
||||
/**
|
||||
* @param size of the file to be shared
|
||||
@@ -24,9 +26,9 @@ class FileHasher {
|
||||
*/
|
||||
static int getPieceSize(long size) {
|
||||
if (size <= 0x1 << 30)
|
||||
return 17
|
||||
return MIN_PIECE_SIZE_POW2
|
||||
|
||||
for (int i = 31; i <= 37; i++) {
|
||||
for (int i = 31; i <= MAX_PIECE_SIZE_POW2; i++) {
|
||||
if (size <= 0x1L << i) {
|
||||
return i-13
|
||||
}
|
||||
@@ -48,27 +50,28 @@ class FileHasher {
|
||||
|
||||
InfoHash hashFile(File file) {
|
||||
final long length = file.length()
|
||||
final int size = 0x1 << getPieceSize(length)
|
||||
int numPieces = (int) (length / size)
|
||||
final long size = 0x1L << getPieceSize(length)
|
||||
int numPieces = (length / size).toInteger()
|
||||
if (numPieces * size < length)
|
||||
numPieces++
|
||||
|
||||
def output = new ByteArrayOutputStream()
|
||||
RandomAccessFile raf = new RandomAccessFile(file, "r")
|
||||
MappedByteBuffer buf = null
|
||||
try {
|
||||
MappedByteBuffer buf
|
||||
for (int i = 0; i < numPieces - 1; i++) {
|
||||
buf = raf.getChannel().map(MapMode.READ_ONLY, ((long)size) * i, size)
|
||||
buf = raf.getChannel().map(MapMode.READ_ONLY, size * i, size.toInteger())
|
||||
digest.update buf
|
||||
DataUtil.tryUnmap(buf)
|
||||
output.write(digest.digest(), 0, 32)
|
||||
}
|
||||
def lastPieceLength = length - (numPieces - 1) * ((long)size)
|
||||
buf = raf.getChannel().map(MapMode.READ_ONLY, length - lastPieceLength, lastPieceLength)
|
||||
long lastPieceLength = length - (numPieces - 1) * size
|
||||
buf = raf.getChannel().map(MapMode.READ_ONLY, length - lastPieceLength, lastPieceLength.toInteger())
|
||||
digest.update buf
|
||||
output.write(digest.digest(), 0, 32)
|
||||
} finally {
|
||||
raf.close()
|
||||
DataUtil.tryUnmap(buf)
|
||||
}
|
||||
|
||||
byte [] hashList = output.toByteArray()
|
||||
|
@@ -24,15 +24,28 @@ class FileManager {
|
||||
final Map<String, Set<File>> nameToFiles = new HashMap<>()
|
||||
final Map<String, Set<File>> commentToFile = new HashMap<>()
|
||||
final SearchIndex index = new SearchIndex()
|
||||
final FileTree negativeTree = new FileTree()
|
||||
final Set<File> sideCarFiles = new HashSet<>()
|
||||
|
||||
FileManager(EventBus eventBus, MuWireSettings settings) {
|
||||
this.settings = settings
|
||||
this.eventBus = eventBus
|
||||
|
||||
for (String negative : settings.negativeFileTree) {
|
||||
negativeTree.add(new File(negative))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void onFileHashedEvent(FileHashedEvent e) {
|
||||
if (e.sharedFile != null)
|
||||
addToIndex(e.sharedFile)
|
||||
if (e.sharedFile == null)
|
||||
return
|
||||
File f = e.sharedFile.getFile()
|
||||
if (sideCarFiles.remove(f)) {
|
||||
File sideCar = new File(f.getParentFile(), f.getName() + ".mwcomment")
|
||||
if (sideCar.exists())
|
||||
e.sharedFile.setComment(Base64.encode(DataUtil.encodei18nString(sideCar.text)))
|
||||
}
|
||||
addToIndex(e.sharedFile)
|
||||
}
|
||||
|
||||
void onFileLoadedEvent(FileLoadedEvent e) {
|
||||
@@ -44,6 +57,21 @@ class FileManager {
|
||||
addToIndex(e.downloadedFile)
|
||||
}
|
||||
}
|
||||
|
||||
void onSideCarFileEvent(SideCarFileEvent e) {
|
||||
String name = e.file.getName()
|
||||
name = name.substring(0, name.length() - ".mwcomment".length())
|
||||
File target = new File(e.file.getParentFile(), name)
|
||||
SharedFile existing = fileToSharedFile.get(target)
|
||||
if (existing == null) {
|
||||
sideCarFiles.add(target)
|
||||
return
|
||||
}
|
||||
String comment = Base64.encode(DataUtil.encodei18nString(e.file.text))
|
||||
String oldComment = existing.getComment()
|
||||
existing.setComment(comment)
|
||||
eventBus.publish(new UICommentEvent(oldComment : oldComment, sharedFile : existing))
|
||||
}
|
||||
|
||||
private void addToIndex(SharedFile sf) {
|
||||
log.info("Adding shared file " + sf.getFile())
|
||||
@@ -56,6 +84,13 @@ class FileManager {
|
||||
}
|
||||
existing.add(sf)
|
||||
fileToSharedFile.put(sf.file, sf)
|
||||
|
||||
negativeTree.remove(sf.file)
|
||||
String parent = sf.getFile().getParent()
|
||||
if (parent != null && settings.watchedDirectories.contains(parent)) {
|
||||
negativeTree.add(sf.file.getParentFile())
|
||||
}
|
||||
saveNegativeTree()
|
||||
|
||||
String name = sf.getFile().getName()
|
||||
Set<File> existingFiles = nameToFiles.get(name)
|
||||
@@ -92,6 +127,10 @@ class FileManager {
|
||||
}
|
||||
|
||||
fileToSharedFile.remove(sf.file)
|
||||
if (!e.deleted && negativeTree.fileToNode.containsKey(sf.file.getParentFile())) {
|
||||
negativeTree.add(sf.file)
|
||||
saveNegativeTree()
|
||||
}
|
||||
|
||||
String name = sf.getFile().getName()
|
||||
Set<File> existingFiles = nameToFiles.get(name)
|
||||
@@ -158,8 +197,10 @@ class FileManager {
|
||||
Set<SharedFile> found
|
||||
found = rootToFiles.get new InfoHash(e.searchHash)
|
||||
found = filter(found, e.oobInfohash)
|
||||
if (found != null && !found.isEmpty())
|
||||
if (found != null && !found.isEmpty()) {
|
||||
found.each { it.hit(e.persona, e.timestamp, "Hash Search") }
|
||||
re = new ResultsEvent(results: found.asList(), uuid: e.uuid, searchEvent: e)
|
||||
}
|
||||
} else {
|
||||
def names = index.search e.searchTerms
|
||||
Set<File> files = new HashSet<>()
|
||||
@@ -172,8 +213,10 @@ class FileManager {
|
||||
files.each { sharedFiles.add fileToSharedFile[it] }
|
||||
files = filter(sharedFiles, e.oobInfohash)
|
||||
|
||||
if (!sharedFiles.isEmpty())
|
||||
if (!sharedFiles.isEmpty()) {
|
||||
sharedFiles.each { it.hit(e.persona, e.timestamp, String.join(" ", e.searchTerms)) }
|
||||
re = new ResultsEvent(results: sharedFiles.asList(), uuid: e.uuid, searchEvent: e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -191,8 +234,10 @@ class FileManager {
|
||||
}
|
||||
rv
|
||||
}
|
||||
|
||||
|
||||
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent e) {
|
||||
negativeTree.remove(e.directory)
|
||||
saveNegativeTree()
|
||||
e.directory.listFiles().each {
|
||||
if (it.isDirectory())
|
||||
eventBus.publish(new DirectoryUnsharedEvent(directory : it))
|
||||
@@ -203,4 +248,9 @@ class FileManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void saveNegativeTree() {
|
||||
settings.negativeFileTree.clear()
|
||||
settings.negativeFileTree.addAll(negativeTree.fileToNode.keySet().collect { it.getAbsolutePath() })
|
||||
}
|
||||
}
|
||||
|
64
core/src/main/groovy/com/muwire/core/files/FileTree.groovy
Normal file
64
core/src/main/groovy/com/muwire/core/files/FileTree.groovy
Normal file
@@ -0,0 +1,64 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class FileTree {
|
||||
|
||||
private final TreeNode root = new TreeNode()
|
||||
private final Map<File, TreeNode> fileToNode = new ConcurrentHashMap<>()
|
||||
|
||||
void add(File file) {
|
||||
List<File> path = new ArrayList<>()
|
||||
path.add(file)
|
||||
while (file.getParentFile() != null) {
|
||||
path.add(file.getParentFile())
|
||||
file = file.getParentFile()
|
||||
}
|
||||
|
||||
Collections.reverse(path)
|
||||
|
||||
TreeNode current = root
|
||||
for (File element : path) {
|
||||
TreeNode existing = fileToNode.get(element)
|
||||
if (existing == null) {
|
||||
existing = new TreeNode()
|
||||
existing.file = element
|
||||
existing.parent = current
|
||||
fileToNode.put(element, existing)
|
||||
current.children.add(existing)
|
||||
}
|
||||
current = existing
|
||||
}
|
||||
}
|
||||
|
||||
boolean remove(File file) {
|
||||
TreeNode node = fileToNode.remove(file)
|
||||
if (node == null) {
|
||||
return false
|
||||
}
|
||||
node.parent.children.remove(node)
|
||||
if (node.parent.children.isEmpty() && node.parent != root)
|
||||
remove(node.parent.file)
|
||||
def copy = new ArrayList(node.children)
|
||||
for (TreeNode child : copy)
|
||||
remove(child.file)
|
||||
true
|
||||
}
|
||||
|
||||
public static class TreeNode {
|
||||
TreeNode parent
|
||||
File file
|
||||
final Set<TreeNode> children = new HashSet<>()
|
||||
|
||||
public int hashCode() {
|
||||
file.hashCode()
|
||||
}
|
||||
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof TreeNode))
|
||||
return false
|
||||
TreeNode other = (TreeNode)o
|
||||
file == other.file
|
||||
}
|
||||
}
|
||||
}
|
@@ -5,4 +5,5 @@ import com.muwire.core.SharedFile
|
||||
|
||||
class FileUnsharedEvent extends Event {
|
||||
SharedFile unsharedFile
|
||||
boolean deleted
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ package com.muwire.core.files
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.SharedFile
|
||||
@@ -33,7 +34,12 @@ class HasherService {
|
||||
return
|
||||
if (fileManager.fileToSharedFile.containsKey(canonical))
|
||||
return
|
||||
if (hashed.add(canonical))
|
||||
if (canonical.isFile() && fileManager.negativeTree.fileToNode.containsKey(canonical))
|
||||
return
|
||||
if (canonical.getName().endsWith(".mwcomment")) {
|
||||
if (canonical.length() <= Constants.MAX_COMMENT_LENGTH)
|
||||
eventBus.publish(new SideCarFileEvent(file : canonical))
|
||||
} else if (hashed.add(canonical))
|
||||
executor.execute( { -> process(canonical) } as Runnable)
|
||||
}
|
||||
|
||||
@@ -47,7 +53,10 @@ class HasherService {
|
||||
|
||||
private void process(File f) {
|
||||
if (f.isDirectory()) {
|
||||
f.listFiles().each {eventBus.publish new FileSharedEvent(file: it) }
|
||||
eventBus.publish(new DirectoryWatchedEvent(directory : f))
|
||||
f.listFiles().each {
|
||||
eventBus.publish new FileSharedEvent(file: it)
|
||||
}
|
||||
} else {
|
||||
if (f.length() == 0) {
|
||||
eventBus.publish new FileHashedEvent(error: "Not sharing empty file $f")
|
||||
|
@@ -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
|
||||
@@ -82,7 +83,7 @@ class PersisterService extends Service {
|
||||
} else {
|
||||
listener.publish(new AllFilesLoadedEvent())
|
||||
}
|
||||
timer.schedule({persistFiles()} as TimerTask, 0, interval)
|
||||
timer.schedule({persistFiles()} as TimerTask, 1000, interval)
|
||||
loaded = true
|
||||
}
|
||||
|
||||
@@ -132,6 +133,18 @@ class PersisterService extends Service {
|
||||
|
||||
SharedFile sf = new SharedFile(file, ih, pieceSize)
|
||||
sf.setComment(json.comment)
|
||||
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)
|
||||
|
||||
}
|
||||
@@ -163,7 +176,22 @@ class PersisterService extends Service {
|
||||
json.pieceSize = sf.getPieceSize()
|
||||
json.hashList = sf.getB64EncodedHashList()
|
||||
json.comment = sf.getComment()
|
||||
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())
|
||||
}
|
||||
|
@@ -0,0 +1,12 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class SideCarFileEvent extends Event {
|
||||
File file
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString() + " file: "+file.getAbsolutePath()
|
||||
}
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
package com.muwire.core.mesh
|
||||
|
||||
import java.util.logging.Level
|
||||
import java.util.stream.Collectors
|
||||
|
||||
import com.muwire.core.Constants
|
||||
@@ -13,8 +14,10 @@ import com.muwire.core.util.DataUtil
|
||||
|
||||
import groovy.json.JsonOutput
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.data.Base64
|
||||
|
||||
@Log
|
||||
class MeshManager {
|
||||
|
||||
private final Map<InfoHash, Mesh> meshes = Collections.synchronizedMap(new HashMap<>())
|
||||
@@ -67,7 +70,10 @@ class MeshManager {
|
||||
json.infoHash = Base64.encode(mesh.infoHash.getRoot())
|
||||
json.sources = mesh.sources.stream().map({it.toBase64()}).collect(Collectors.toList())
|
||||
json.nPieces = mesh.pieces.nPieces
|
||||
json.xHave = DataUtil.encodeXHave(mesh.pieces.downloaded, mesh.pieces.nPieces)
|
||||
List<Integer> downloaded = mesh.pieces.getDownloaded()
|
||||
if( downloaded.size() > mesh.pieces.nPieces)
|
||||
return
|
||||
json.xHave = DataUtil.encodeXHave(downloaded, mesh.pieces.nPieces)
|
||||
writer.println(JsonOutput.toJson(json))
|
||||
}
|
||||
}
|
||||
@@ -82,6 +88,9 @@ class MeshManager {
|
||||
JsonSlurper slurper = new JsonSlurper()
|
||||
meshFile.eachLine {
|
||||
def json = slurper.parseText(it)
|
||||
if (json.nPieces == null || json.nPieces == 0)
|
||||
return // skip it, invalid
|
||||
|
||||
if (now - json.timestamp > settings.meshExpiration * 60 * 1000)
|
||||
return
|
||||
InfoHash infoHash = new InfoHash(Base64.decode(json.infoHash))
|
||||
@@ -93,8 +102,13 @@ class MeshManager {
|
||||
mesh.sources.add(persona)
|
||||
}
|
||||
|
||||
if (json.xHave != null)
|
||||
DataUtil.decodeXHave(json.xHave).each { pieces.markDownloaded(it) }
|
||||
if (json.xHave != null) {
|
||||
try {
|
||||
DataUtil.decodeXHave(json.xHave).each { pieces.markDownloaded(it) }
|
||||
} catch (IllegalArgumentException bad) {
|
||||
log.log(Level.WARNING, "couldn't parse XHave", bad)
|
||||
}
|
||||
}
|
||||
|
||||
if (!mesh.sources.isEmpty())
|
||||
meshes.put(infoHash, mesh)
|
||||
|
@@ -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")
|
||||
|
@@ -12,6 +12,7 @@ class QueryEvent extends Event {
|
||||
Destination replyTo
|
||||
Persona originator
|
||||
Destination receivedOn
|
||||
byte[] sig
|
||||
|
||||
String toString() {
|
||||
"searchEvent: $searchEvent firstHop:$firstHop, replyTo:${replyTo.toBase32()}" +
|
||||
|
@@ -6,6 +6,7 @@ import javax.naming.directory.InvalidSearchControlsException
|
||||
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.files.FileHasher
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import net.i2p.data.Base64
|
||||
@@ -30,12 +31,12 @@ class ResultsParser {
|
||||
private static parseV1(Persona p, UUID uuid, def json) {
|
||||
if (json.name == null)
|
||||
throw new InvalidSearchResultException("name missing")
|
||||
if (json.size == null)
|
||||
throw new InvalidSearchResultException("length missing")
|
||||
if (json.size == null || json.size <= 0 || json.size > FileHasher.MAX_SIZE)
|
||||
throw new InvalidSearchResultException("length missing or invalid, $json.size")
|
||||
if (json.infohash == null)
|
||||
throw new InvalidSearchResultException("infohash missing")
|
||||
if (json.pieceSize == null)
|
||||
throw new InvalidSearchResultException("pieceSize missing")
|
||||
if (json.pieceSize == null || json.pieceSize < FileHasher.MIN_PIECE_SIZE_POW2 || json.pieceSize > FileHasher.MAX_PIECE_SIZE_POW2)
|
||||
throw new InvalidSearchResultException("pieceSize missing or invalid, $json.pieceSize")
|
||||
if (!(json.hashList instanceof List))
|
||||
throw new InvalidSearchResultException("hashlist not a list")
|
||||
try {
|
||||
@@ -71,12 +72,12 @@ class ResultsParser {
|
||||
private static UIResultEvent parseV2(Persona p, UUID uuid, def json) {
|
||||
if (json.name == null)
|
||||
throw new InvalidSearchResultException("name missing")
|
||||
if (json.size == null)
|
||||
throw new InvalidSearchResultException("length missing")
|
||||
if (json.size == null || json.size <= 0 || json.size > FileHasher.MAX_SIZE)
|
||||
throw new InvalidSearchResultException("length missing or invalid $json.size")
|
||||
if (json.infohash == null)
|
||||
throw new InvalidSearchResultException("infohash missing")
|
||||
if (json.pieceSize == null)
|
||||
throw new InvalidSearchResultException("pieceSize missing")
|
||||
if (json.pieceSize == null || json.pieceSize < FileHasher.MIN_PIECE_SIZE_POW2 || json.pieceSize > FileHasher.MAX_PIECE_SIZE_POW2)
|
||||
throw new InvalidSearchResultException("pieceSize missing or invalid, $json.pieceSize")
|
||||
if (json.hashList != null)
|
||||
throw new InvalidSearchResultException("V2 result with hashlist")
|
||||
try {
|
||||
@@ -98,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,
|
||||
@@ -107,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,48 @@ 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.split(' ').each { if (it.length() > 0) rv << it }
|
||||
|
||||
// and add original string
|
||||
rv << source
|
||||
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() {
|
||||
|
@@ -7,4 +7,5 @@ class UpdateAvailableEvent extends Event {
|
||||
String version
|
||||
String signer
|
||||
String infoHash
|
||||
String text
|
||||
}
|
||||
|
@@ -9,6 +9,7 @@ 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
|
||||
@@ -21,7 +22,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 +36,7 @@ class UpdateClient {
|
||||
final MuWireSettings settings
|
||||
final FileManager fileManager
|
||||
final Persona me
|
||||
final SigningPrivateKey spk
|
||||
|
||||
private final Timer timer
|
||||
|
||||
@@ -40,14 +45,19 @@ class UpdateClient {
|
||||
private volatile InfoHash updateInfoHash
|
||||
private volatile String version, signer
|
||||
private volatile boolean updateDownloading
|
||||
|
||||
private volatile String text
|
||||
|
||||
UpdateClient(EventBus eventBus, I2PSession session, String myVersion, MuWireSettings settings, FileManager fileManager, Persona me) {
|
||||
UpdateClient(EventBus eventBus, I2PSession session, String myVersion, MuWireSettings settings,
|
||||
FileManager fileManager, Persona me, SigningPrivateKey spk) {
|
||||
this.eventBus = eventBus
|
||||
this.session = session
|
||||
this.myVersion = myVersion
|
||||
this.settings = settings
|
||||
this.fileManager = fileManager
|
||||
this.me = me
|
||||
this.spk = spk
|
||||
this.lastUpdateCheckTime = settings.lastUpdateCheck
|
||||
timer = new Timer("update-client",true)
|
||||
}
|
||||
|
||||
@@ -75,7 +85,9 @@ class UpdateClient {
|
||||
if (e.downloadedFile.infoHash != updateInfoHash)
|
||||
return
|
||||
updateDownloading = false
|
||||
eventBus.publish(new UpdateDownloadedEvent(version : version, signer : signer))
|
||||
eventBus.publish(new UpdateDownloadedEvent(version : version, signer : signer, text : text))
|
||||
if (!settings.shareDownloadedFiles)
|
||||
eventBus.publish(new FileSharedEvent(file : e.downloadedFile))
|
||||
}
|
||||
|
||||
private void checkUpdate() {
|
||||
@@ -85,6 +97,7 @@ class UpdateClient {
|
||||
return
|
||||
}
|
||||
lastUpdateCheckTime = now
|
||||
settings.lastUpdateCheck = now
|
||||
|
||||
log.info("checking for update")
|
||||
|
||||
@@ -147,23 +160,25 @@ class UpdateClient {
|
||||
} else
|
||||
infoHash = payload[settings.updateType]
|
||||
|
||||
|
||||
text = payload.text
|
||||
|
||||
if (!settings.autoDownloadUpdate) {
|
||||
log.info("new version $payload.version available, publishing event")
|
||||
eventBus.publish(new UpdateAvailableEvent(version : payload.version, signer : payload.signer, infoHash : infoHash))
|
||||
eventBus.publish(new UpdateAvailableEvent(version : payload.version, signer : payload.signer, infoHash : infoHash, text : text))
|
||||
} else {
|
||||
log.info("new version $payload.version available")
|
||||
updateInfoHash = new InfoHash(Base64.decode(infoHash))
|
||||
if (fileManager.rootToFiles.containsKey(updateInfoHash))
|
||||
eventBus.publish(new UpdateDownloadedEvent(version : payload.version, signer : payload.signer))
|
||||
eventBus.publish(new UpdateDownloadedEvent(version : payload.version, signer : payload.signer, text : text))
|
||||
else {
|
||||
updateDownloading = false
|
||||
version = payload.version
|
||||
signer = payload.signer
|
||||
log.info("starting search for new version hash $payload.infoHash")
|
||||
def searchEvent = new SearchEvent(searchHash : updateInfoHash.getRoot(), uuid : UUID.randomUUID(), oobInfohash : true)
|
||||
Signature sig = DSAEngine.getInstance().sign(updateInfoHash.getRoot(), spk)
|
||||
def searchEvent = new SearchEvent(searchHash : updateInfoHash.getRoot(), uuid : UUID.randomUUID(), 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)
|
||||
eventBus.publish(queryEvent)
|
||||
}
|
||||
}
|
||||
|
@@ -5,4 +5,5 @@ import com.muwire.core.Event
|
||||
class UpdateDownloadedEvent extends Event {
|
||||
String version
|
||||
String signer
|
||||
String text
|
||||
}
|
||||
|
@@ -20,6 +20,8 @@ class ContentUploader extends Uploader {
|
||||
private final ContentRequest request
|
||||
private final Mesh mesh
|
||||
private final int pieceSize
|
||||
|
||||
private volatile boolean done
|
||||
|
||||
ContentUploader(File file, ContentRequest request, Endpoint endpoint, Mesh mesh, int pieceSize) {
|
||||
super(endpoint)
|
||||
@@ -62,20 +64,23 @@ class ContentUploader extends Uploader {
|
||||
mapped = channel.map(FileChannel.MapMode.READ_ONLY, range.start, range.end - range.start + 1)
|
||||
byte [] tmp = new byte[0x1 << 13]
|
||||
while(mapped.hasRemaining()) {
|
||||
int start = mapped.position()
|
||||
int read
|
||||
synchronized(this) {
|
||||
int start = mapped.position()
|
||||
mapped.get(tmp, 0, Math.min(tmp.length, mapped.remaining()))
|
||||
read = mapped.position() - start
|
||||
dataSinceLastRead += read
|
||||
}
|
||||
int read = mapped.position() - start
|
||||
endpoint.getOutputStream().write(tmp, 0, read)
|
||||
}
|
||||
done = true
|
||||
} finally {
|
||||
try {channel?.close() } catch (IOException ignored) {}
|
||||
endpoint.getOutputStream().flush()
|
||||
synchronized(this) {
|
||||
DataUtil.tryUnmap(mapped)
|
||||
mapped = null
|
||||
}
|
||||
endpoint.getOutputStream().flush()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +103,7 @@ class ContentUploader extends Uploader {
|
||||
@Override
|
||||
public synchronized int getProgress() {
|
||||
if (mapped == null)
|
||||
return 0
|
||||
return done ? 100 : 0
|
||||
int position = mapped.position()
|
||||
int total = request.getRange().end - request.getRange().start
|
||||
(int)(position * 100.0 / total)
|
||||
@@ -123,4 +128,13 @@ class ContentUploader extends Uploader {
|
||||
public long getTotalSize() {
|
||||
return file.length();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof ContentUploader))
|
||||
return false
|
||||
ContentUploader other = (ContentUploader)o
|
||||
request.infoHash == other.request.infoHash &&
|
||||
request.getDownloader() == other.request.getDownloader()
|
||||
}
|
||||
}
|
||||
|
@@ -26,11 +26,13 @@ class HashListUploader extends Uploader {
|
||||
|
||||
byte[]tmp = new byte[0x1 << 13]
|
||||
while(mapped.hasRemaining()) {
|
||||
int start = mapped.position()
|
||||
int read
|
||||
synchronized(this) {
|
||||
int start = mapped.position()
|
||||
mapped.get(tmp, 0, Math.min(tmp.length, mapped.remaining()))
|
||||
read = mapped.position() - start
|
||||
dataSinceLastRead += read
|
||||
}
|
||||
int read = mapped.position() - start
|
||||
endpoint.getOutputStream().write(tmp, 0, read)
|
||||
}
|
||||
endpoint.getOutputStream().flush()
|
||||
@@ -65,4 +67,12 @@ class HashListUploader extends Uploader {
|
||||
public long getTotalSize() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof HashListUploader))
|
||||
return false
|
||||
HashListUploader other = (HashListUploader)o
|
||||
infoHash == other.infoHash && request.downloader == other.request.downloader
|
||||
}
|
||||
}
|
||||
|
@@ -4,6 +4,8 @@ import java.nio.charset.StandardCharsets
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.connection.Endpoint
|
||||
import com.muwire.core.download.DownloadManager
|
||||
@@ -22,15 +24,22 @@ public class UploadManager {
|
||||
private final FileManager fileManager
|
||||
private final MeshManager meshManager
|
||||
private final DownloadManager downloadManager
|
||||
private final MuWireSettings props
|
||||
|
||||
/** LOCKING: this on both structures */
|
||||
private int totalUploads
|
||||
private final Map<Persona, Integer> uploadsPerUser = new HashMap<>()
|
||||
|
||||
public UploadManager() {}
|
||||
|
||||
public UploadManager(EventBus eventBus, FileManager fileManager,
|
||||
MeshManager meshManager, DownloadManager downloadManager) {
|
||||
MeshManager meshManager, DownloadManager downloadManager,
|
||||
MuWireSettings props) {
|
||||
this.eventBus = eventBus
|
||||
this.fileManager = fileManager
|
||||
this.meshManager = meshManager
|
||||
this.downloadManager = downloadManager
|
||||
this.props = props
|
||||
}
|
||||
|
||||
public void processGET(Endpoint e) throws IOException {
|
||||
@@ -82,7 +91,15 @@ public class UploadManager {
|
||||
|
||||
if (request.have > 0)
|
||||
eventBus.publish(new SourceDiscoveredEvent(infoHash : request.infoHash, source : request.downloader))
|
||||
|
||||
|
||||
if (!incrementUploads(request.downloader)) {
|
||||
log.info("rejecting due to slot limit")
|
||||
e.getOutputStream().write("429 Too Many Requests\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
e.getOutputStream().flush()
|
||||
e.close()
|
||||
return
|
||||
}
|
||||
|
||||
Mesh mesh
|
||||
File file
|
||||
int pieceSize
|
||||
@@ -91,6 +108,7 @@ public class UploadManager {
|
||||
file = downloader.incompleteFile
|
||||
pieceSize = downloader.pieceSizePow2
|
||||
} else {
|
||||
sharedFiles.each { it.getDownloaders().add(request.downloader.getHumanReadableName()) }
|
||||
SharedFile sharedFile = sharedFiles.iterator().next();
|
||||
mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces, false)
|
||||
file = sharedFile.file
|
||||
@@ -102,6 +120,7 @@ public class UploadManager {
|
||||
try {
|
||||
uploader.respond()
|
||||
} finally {
|
||||
decrementUploads(request.downloader)
|
||||
eventBus.publish(new UploadFinishedEvent(uploader : uploader))
|
||||
}
|
||||
}
|
||||
@@ -156,12 +175,21 @@ public class UploadManager {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (!incrementUploads(request.downloader)) {
|
||||
log.info("rejecting due to slot limit")
|
||||
e.getOutputStream().write("429 Too Many Requests\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
e.getOutputStream().flush()
|
||||
e.close()
|
||||
return
|
||||
}
|
||||
|
||||
Uploader uploader = new HashListUploader(e, fullInfoHash, request)
|
||||
eventBus.publish(new UploadEvent(uploader : uploader))
|
||||
try {
|
||||
uploader.respond()
|
||||
} finally {
|
||||
decrementUploads(request.downloader)
|
||||
eventBus.publish(new UploadFinishedEvent(uploader : uploader))
|
||||
}
|
||||
|
||||
@@ -216,6 +244,7 @@ public class UploadManager {
|
||||
file = downloader.incompleteFile
|
||||
pieceSize = downloader.pieceSizePow2
|
||||
} else {
|
||||
sharedFiles.each { it.getDownloaders().add(request.downloader.getHumanReadableName()) }
|
||||
SharedFile sharedFile = sharedFiles.iterator().next();
|
||||
mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces, false)
|
||||
file = sharedFile.file
|
||||
@@ -231,5 +260,37 @@ public class UploadManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param p downloader
|
||||
* @return true if this upload hasn't hit any slot limits
|
||||
*/
|
||||
private synchronized boolean incrementUploads(Persona p) {
|
||||
if (props.totalUploadSlots >= 0 && totalUploads >= props.totalUploadSlots)
|
||||
return false
|
||||
if (props.uploadSlotsPerUser == 0)
|
||||
return false
|
||||
|
||||
Integer currentUploads = uploadsPerUser.get(p)
|
||||
if (currentUploads == null)
|
||||
currentUploads = 0
|
||||
if (props.uploadSlotsPerUser > 0 && currentUploads >= props.uploadSlotsPerUser)
|
||||
return false
|
||||
uploadsPerUser.put(p, ++currentUploads)
|
||||
totalUploads++
|
||||
true
|
||||
}
|
||||
|
||||
private synchronized void decrementUploads(Persona p) {
|
||||
totalUploads--
|
||||
Integer currentUploads = uploadsPerUser.get(p)
|
||||
if (currentUploads == null || currentUploads == 0)
|
||||
throw new IllegalStateException()
|
||||
currentUploads--
|
||||
if (currentUploads == 0)
|
||||
uploadsPerUser.remove(p)
|
||||
else
|
||||
uploadsPerUser.put(p, currentUploads)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -11,7 +11,13 @@ import com.muwire.core.connection.Endpoint
|
||||
abstract class Uploader {
|
||||
protected final Endpoint endpoint
|
||||
protected ByteBuffer mapped
|
||||
|
||||
private long lastSpeedRead
|
||||
protected int dataSinceLastRead
|
||||
|
||||
private final ArrayList<Integer> speedArr = [0,0,0,0,0]
|
||||
private int speedPos, speedAvg
|
||||
|
||||
Uploader(Endpoint endpoint) {
|
||||
this.endpoint = endpoint
|
||||
}
|
||||
@@ -38,4 +44,34 @@ abstract class Uploader {
|
||||
abstract int getTotalPieces();
|
||||
|
||||
abstract long getTotalSize();
|
||||
|
||||
synchronized int speed() {
|
||||
final long now = System.currentTimeMillis()
|
||||
long interval = Math.max(1000, now - lastSpeedRead)
|
||||
lastSpeedRead = now;
|
||||
int currSpeed = (int) (dataSinceLastRead * 1000.0 / interval)
|
||||
dataSinceLastRead = 0
|
||||
|
||||
// normalize to speedArr.size
|
||||
currSpeed /= speedArr.size()
|
||||
|
||||
// compute new speedAvg and update speedArr
|
||||
if ( speedArr[speedPos] > speedAvg ) {
|
||||
speedAvg = 0
|
||||
} else {
|
||||
speedAvg -= speedArr[speedPos]
|
||||
}
|
||||
speedAvg += currSpeed
|
||||
speedArr[speedPos] = currSpeed
|
||||
// this might be necessary due to rounding errors
|
||||
if (speedAvg < 0)
|
||||
speedAvg = 0
|
||||
|
||||
// rolling index over the speedArr
|
||||
speedPos++
|
||||
if (speedPos >= speedArr.size())
|
||||
speedPos=0
|
||||
|
||||
speedAvg
|
||||
}
|
||||
}
|
||||
|
@@ -4,10 +4,13 @@ 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;
|
||||
public static final int MAX_HEADERS = 16;
|
||||
|
||||
public static final int MAX_RESULTS = 0x1 << 16;
|
||||
|
||||
public static final int MAX_COMMENT_LENGTH = 0x1 << 15;
|
||||
}
|
||||
|
@@ -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());
|
||||
}
|
||||
}
|
@@ -3,7 +3,11 @@ package com.muwire.core;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
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;
|
||||
|
||||
@@ -23,6 +27,8 @@ public class SharedFile {
|
||||
private final List<String> b64EncodedHashList;
|
||||
|
||||
private volatile String comment;
|
||||
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;
|
||||
@@ -90,6 +96,22 @@ public class SharedFile {
|
||||
public String getComment() {
|
||||
return comment;
|
||||
}
|
||||
|
||||
public int getHits() {
|
||||
return searches.size();
|
||||
}
|
||||
|
||||
public void hit(Persona searcher, long timestamp, String query) {
|
||||
searches.add(new SearchEntry(searcher, timestamp, query));
|
||||
}
|
||||
|
||||
public Set<String> getDownloaders() {
|
||||
return downloaders;
|
||||
}
|
||||
|
||||
public void addDownloader(String name) {
|
||||
downloaders.add(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
@@ -103,4 +125,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,11 +10,17 @@ 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.stream.Collectors;
|
||||
|
||||
import com.muwire.core.Constants;
|
||||
|
||||
import net.i2p.data.Base64;
|
||||
import net.i2p.util.ConcurrentHashSet;
|
||||
|
||||
public class DataUtil {
|
||||
|
||||
@@ -95,6 +101,20 @@ public class DataUtil {
|
||||
}
|
||||
return new String(baos.toByteArray(), StandardCharsets.US_ASCII);
|
||||
}
|
||||
|
||||
public static Map<String, String> readAllHeaders(InputStream is) throws IOException {
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
String header;
|
||||
while(!(header = readTillRN(is)).equals("") && headers.size() < Constants.MAX_HEADERS) {
|
||||
int colon = header.indexOf(':');
|
||||
if (colon == -1 || colon == header.length() - 1)
|
||||
throw new IOException("Invalid header "+ header);
|
||||
String key = header.substring(0, colon);
|
||||
String value = header.substring(colon + 1);
|
||||
headers.put(key, value.trim());
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
public static String encodeXHave(List<Integer> pieces, int totalPieces) {
|
||||
int bytes = totalPieces / 8;
|
||||
@@ -165,4 +185,22 @@ public class DataUtil {
|
||||
} catch(Exception ex) { }
|
||||
cb = null;
|
||||
}
|
||||
|
||||
public static Set<String> readEncodedSet(Properties props, String property) {
|
||||
Set<String> rv = new ConcurrentHashSet<>();
|
||||
if (props.containsKey(property)) {
|
||||
String [] encoded = props.getProperty(property).split(",");
|
||||
for(String s : encoded)
|
||||
rv.add(readi18nString(Base64.decode(s)));
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
public static void writeEncodedSet(Set<String> set, String property, Properties props) {
|
||||
if (set.isEmpty())
|
||||
return;
|
||||
String encoded = set.stream().map(s -> Base64.encode(encodei18nString(s)))
|
||||
.collect(Collectors.joining(","));
|
||||
props.setProperty(property, encoded);
|
||||
}
|
||||
}
|
||||
|
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)
|
||||
}
|
||||
|
@@ -2,6 +2,8 @@ package com.muwire.core.download
|
||||
|
||||
import static org.junit.Assert.fail
|
||||
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Ignore
|
||||
@@ -76,7 +78,7 @@ class DownloadSessionTest {
|
||||
toUploader = new PipedOutputStream(fromDownloader)
|
||||
endpoint = new Endpoint(null, fromUploader, toUploader, null)
|
||||
|
||||
session = new DownloadSession(eventBus, "",pieces, infoHash, endpoint, target, pieceSize, size, available)
|
||||
session = new DownloadSession(eventBus, "",pieces, infoHash, endpoint, target, pieceSize, size, available, new AtomicLong())
|
||||
downloadThread = new Thread( { perform() } as Runnable)
|
||||
downloadThread.setDaemon(true)
|
||||
downloadThread.start()
|
||||
|
@@ -149,7 +149,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 +170,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"])
|
||||
|
@@ -0,0 +1,42 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
class FileTreeTest {
|
||||
|
||||
@Test
|
||||
public void testRemoveEmtpyDirs() {
|
||||
File a = new File("a")
|
||||
File b = new File(a, "b")
|
||||
File c = new File(b, "c")
|
||||
|
||||
FileTree tree = new FileTree()
|
||||
tree.add(c)
|
||||
|
||||
assert tree.root.children.size() == 1
|
||||
assert tree.fileToNode.size() == 3
|
||||
|
||||
tree.remove(b)
|
||||
assert tree.root.children.size() == 0
|
||||
assert tree.fileToNode.isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoveFileFromNonEmptyDir() {
|
||||
File a = new File("a")
|
||||
File b = new File(a,"b")
|
||||
File c = new File(b, "c")
|
||||
File d = new File(b, "d")
|
||||
|
||||
FileTree tree = new FileTree()
|
||||
tree.add(c)
|
||||
|
||||
assert tree.fileToNode.size() == 3
|
||||
|
||||
tree.add(d)
|
||||
assert tree.fileToNode.size() == 4
|
||||
|
||||
tree.remove(d)
|
||||
assert tree.fileToNode.size() == 3
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -1,11 +1,12 @@
|
||||
group = com.muwire
|
||||
version = 0.5.4
|
||||
version = 0.5.9
|
||||
i2pVersion = 0.9.43
|
||||
groovyVersion = 2.4.15
|
||||
slf4jVersion = 1.7.25
|
||||
spockVersion = 1.1-groovy-2.4
|
||||
grailsVersion=4.0.0
|
||||
gorm.version=7.0.2.RELEASE
|
||||
griffonEnv=prod
|
||||
|
||||
sourceCompatibility=1.8
|
||||
targetCompatibility=1.8
|
||||
|
@@ -40,8 +40,11 @@ griffon {
|
||||
]
|
||||
}
|
||||
|
||||
mainClassName = 'com.muwire.gui.Launcher'
|
||||
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
|
||||
application {
|
||||
mainClassName = 'com.muwire.gui.Launcher'
|
||||
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties','-Xmx256M']
|
||||
applicationName = 'MuWire'
|
||||
}
|
||||
|
||||
apply from: 'gradle/publishing.gradle'
|
||||
// apply from: 'gradle/code-coverage.gradle'
|
||||
|
@@ -41,6 +41,11 @@ mvcGroups {
|
||||
view = 'com.muwire.gui.I2PStatusView'
|
||||
controller = 'com.muwire.gui.I2PStatusController'
|
||||
}
|
||||
'system-status' {
|
||||
model = 'com.muwire.gui.SystemStatusModel'
|
||||
view = 'com.muwire.gui.SystemStatusView'
|
||||
controller = 'com.muwire.gui.SystemStatusController'
|
||||
}
|
||||
'trust-list' {
|
||||
model = 'com.muwire.gui.TrustListModel'
|
||||
view = 'com.muwire.gui.TrustListView'
|
||||
@@ -71,4 +76,34 @@ mvcGroups {
|
||||
view = 'com.muwire.gui.CloseWarningView'
|
||||
controller = 'com.muwire.gui.CloseWarningController'
|
||||
}
|
||||
'update' {
|
||||
model = 'com.muwire.gui.UpdateModel'
|
||||
view = 'com.muwire.gui.UpdateView'
|
||||
controller = 'com.muwire.gui.UpdateController'
|
||||
}
|
||||
'advanced-sharing' {
|
||||
model = 'com.muwire.gui.AdvancedSharingModel'
|
||||
view = 'com.muwire.gui.AdvancedSharingView'
|
||||
controller = 'com.muwire.gui.AdvancedSharingController'
|
||||
}
|
||||
'fetch-certificates' {
|
||||
model = 'com.muwire.gui.FetchCertificatesModel'
|
||||
view = 'com.muwire.gui.FetchCertificatesView'
|
||||
controller = 'com.muwire.gui.FetchCertificatesController'
|
||||
}
|
||||
'certificate-warning' {
|
||||
model = 'com.muwire.gui.CertificateWarningModel'
|
||||
view = 'com.muwire.gui.CertificateWarningView'
|
||||
controller = 'com.muwire.gui.CertificateWarningController'
|
||||
}
|
||||
'certificate-control' {
|
||||
model = 'com.muwire.gui.CertificateControlModel'
|
||||
view = 'com.muwire.gui.CertificateControlView'
|
||||
controller = 'com.muwire.gui.CertificateControlController'
|
||||
}
|
||||
'shared-file' {
|
||||
model = 'com.muwire.gui.SharedFileModel'
|
||||
view = 'com.muwire.gui.SharedFileView'
|
||||
controller = 'com.muwire.gui.SharedFileController'
|
||||
}
|
||||
}
|
||||
|
@@ -7,7 +7,9 @@ import griffon.metadata.ArtifactProviderFor
|
||||
import net.i2p.data.Base64
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
import javax.swing.JOptionPane
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.files.UICommentEvent
|
||||
import com.muwire.core.util.DataUtil
|
||||
@@ -24,6 +26,11 @@ class AddCommentController {
|
||||
@ControllerAction
|
||||
void save() {
|
||||
String comment = view.textarea.getText()
|
||||
if (comment.length() > Constants.MAX_COMMENT_LENGTH ) {
|
||||
JOptionPane.showMessageDialog(null, "Your comment is too long - ${comment.length()} bytes. The maximum size is $Constants.MAX_COMMENT_LENGTH bytes",
|
||||
"Comment Too Long", JOptionPane.WARNING_MESSAGE)
|
||||
return
|
||||
}
|
||||
if (comment.trim().length() == 0)
|
||||
comment = null
|
||||
else
|
||||
|
@@ -0,0 +1,17 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.artifact.GriffonController
|
||||
import griffon.core.controller.ControllerAction
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
import com.muwire.core.Core
|
||||
|
||||
@ArtifactProviderFor(GriffonController)
|
||||
class AdvancedSharingController {
|
||||
@MVCMember @Nonnull
|
||||
AdvancedSharingModel model
|
||||
@MVCMember @Nonnull
|
||||
AdvancedSharingView view
|
||||
}
|
@@ -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()
|
||||
}
|
||||
}
|
@@ -23,17 +23,38 @@ class CloseWarningController {
|
||||
|
||||
@ControllerAction
|
||||
void close() {
|
||||
boolean showWarning = !view.checkbox.model.isSelected()
|
||||
model.closeWarning = showWarning
|
||||
settings.closeWarning = showWarning
|
||||
|
||||
File props = new File(home, "gui.properties")
|
||||
props.withOutputStream {
|
||||
settings.write(it)
|
||||
boolean rememberDecision = view.checkbox.model.isSelected()
|
||||
if (rememberDecision) {
|
||||
settings.exitOnClose = false
|
||||
settings.closeWarning = false
|
||||
saveMuSettings()
|
||||
}
|
||||
|
||||
view.dialog.setVisible(false)
|
||||
view.mainFrame.setVisible(false)
|
||||
mvcGroup.destroy()
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void exit() {
|
||||
boolean rememberDecision = view.checkbox.model.isSelected()
|
||||
if (rememberDecision) {
|
||||
settings.exitOnClose = true
|
||||
settings.closeWarning = false
|
||||
saveMuSettings()
|
||||
}
|
||||
view.dialog.setVisible(false)
|
||||
view.mainFrame.setVisible(false)
|
||||
def parentView = mvcGroup.parentGroup.view
|
||||
mvcGroup.destroy()
|
||||
parentView.closeApplication()
|
||||
}
|
||||
|
||||
private void saveMuSettings() {
|
||||
|
||||
File props = new File(home, "gui.properties")
|
||||
props.withOutputStream {
|
||||
settings.write(it)
|
||||
}
|
||||
}
|
||||
}
|
@@ -97,9 +97,6 @@ class ContentPanelController {
|
||||
}
|
||||
|
||||
void saveMuWireSettings() {
|
||||
File f = new File(core.home, "MuWire.properties")
|
||||
f.withOutputStream {
|
||||
core.muOptions.write(it)
|
||||
}
|
||||
core.saveMuSettings()
|
||||
}
|
||||
}
|
@@ -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,13 +7,20 @@ import griffon.core.mvc.MVCGroup
|
||||
import griffon.core.mvc.MVCGroupConfiguration
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import net.i2p.crypto.DSAEngine
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Signature
|
||||
|
||||
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
|
||||
@@ -23,6 +30,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
|
||||
@@ -32,6 +40,8 @@ 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
|
||||
|
||||
@ArtifactProviderFor(GriffonController)
|
||||
class MainFrameController {
|
||||
@@ -47,11 +57,29 @@ class MainFrameController {
|
||||
private volatile Core core
|
||||
|
||||
@ControllerAction
|
||||
void search() {
|
||||
void clearSearch() {
|
||||
def searchField = builder.getVariable("search-field")
|
||||
searchField.setSelectedItem(null)
|
||||
searchField.requestFocus()
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void search(ActionEvent evt) {
|
||||
if (evt?.getActionCommand() == null)
|
||||
return
|
||||
def cardsPanel = builder.getVariable("cards-panel")
|
||||
cardsPanel.getLayout().show(cardsPanel, "search window")
|
||||
|
||||
def search = builder.getVariable("search-field").text
|
||||
def searchField = builder.getVariable("search-field")
|
||||
def search = searchField.getSelectedItem()
|
||||
searchField.model.addElement(search)
|
||||
performSearch(search)
|
||||
}
|
||||
|
||||
private void performSearch(String search) {
|
||||
|
||||
model.sessionRestored = true
|
||||
|
||||
search = search.trim()
|
||||
if (search.length() == 0)
|
||||
return
|
||||
@@ -77,21 +105,24 @@ 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 }
|
||||
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : uuid, oobInfohash: true,
|
||||
searchComments : core.muOptions.searchComments, compressedResults : true)
|
||||
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, persona : core.me)
|
||||
}
|
||||
boolean firstHop = core.muOptions.allowUntrusted || core.muOptions.searchExtraHop
|
||||
|
||||
Signature sig = DSAEngine.getInstance().sign(payload, core.spk)
|
||||
|
||||
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : firstHop,
|
||||
replyTo: core.me.destination, receivedOn: core.me.destination,
|
||||
originator : core.me))
|
||||
replyTo: core.me.destination, receivedOn: core.me.destination,
|
||||
originator : core.me, sig : sig.data))
|
||||
|
||||
}
|
||||
|
||||
void search(String infoHash, String tabTitle) {
|
||||
@@ -105,11 +136,14 @@ 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)
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
private int selectedDownload() {
|
||||
@@ -292,12 +326,72 @@ class MainFrameController {
|
||||
params['core'] = core
|
||||
mvcGroup.createMVCGroup("add-comment", "Add Comment", params)
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void clearUploads() {
|
||||
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
|
||||
view.settings.openTabs.each {
|
||||
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.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)
|
||||
}
|
||||
|
||||
void saveMuWireSettings() {
|
||||
File f = new File(core.home, "MuWire.properties")
|
||||
f.withOutputStream {
|
||||
core.muOptions.write(it)
|
||||
}
|
||||
core.saveMuSettings()
|
||||
}
|
||||
|
||||
void mvcGroupInit(Map<String, String> args) {
|
||||
|
@@ -36,8 +36,8 @@ class MuWireStatusController {
|
||||
|
||||
|
||||
model.sharedFiles = core.fileManager.fileToSharedFile.size()
|
||||
|
||||
model.downloads = core.downloadManager.downloaders.size()
|
||||
model.browsed = core.connectionAcceptor.browsed
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
|
@@ -10,6 +10,7 @@ import java.util.logging.Level
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
import javax.swing.JFileChooser
|
||||
import javax.swing.JOptionPane
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.MuWireSettings
|
||||
@@ -20,12 +21,14 @@ class OptionsController {
|
||||
OptionsModel model
|
||||
@MVCMember @Nonnull
|
||||
OptionsView view
|
||||
|
||||
Core core
|
||||
MuWireSettings settings
|
||||
UISettings uiSettings
|
||||
|
||||
@ControllerAction
|
||||
void save() {
|
||||
String text
|
||||
Core core = application.context.get("core")
|
||||
MuWireSettings settings = application.context.get("muwire-settings")
|
||||
|
||||
def i2pProps = core.i2pOptions
|
||||
|
||||
@@ -69,6 +72,16 @@ class OptionsController {
|
||||
text = view.updateField.text
|
||||
model.updateCheckInterval = text
|
||||
settings.updateCheckInterval = Integer.valueOf(text)
|
||||
|
||||
text = view.totalUploadSlotsField.text
|
||||
int totalUploadSlots = Integer.valueOf(text)
|
||||
model.totalUploadSlots = totalUploadSlots
|
||||
settings.totalUploadSlots = totalUploadSlots
|
||||
|
||||
text = view.uploadSlotsPerUserField.text
|
||||
int uploadSlotsPerUser = Integer.valueOf(text)
|
||||
model.uploadSlotsPerUser = uploadSlotsPerUser
|
||||
settings.uploadSlotsPerUser = uploadSlotsPerUser
|
||||
|
||||
boolean searchComments = view.searchCommentsCheckbox.model.isSelected()
|
||||
model.searchComments = searchComments
|
||||
@@ -127,14 +140,10 @@ class OptionsController {
|
||||
model.trustListInterval = trustListInterval
|
||||
settings.trustListInterval = Integer.parseInt(trustListInterval)
|
||||
|
||||
File settingsFile = new File(core.home, "MuWire.properties")
|
||||
settingsFile.withOutputStream {
|
||||
settings.write(it)
|
||||
}
|
||||
core.saveMuSettings()
|
||||
|
||||
// UI Setttings
|
||||
|
||||
UISettings uiSettings = application.context.get("ui-settings")
|
||||
text = view.lnfField.text
|
||||
model.lnf = text
|
||||
uiSettings.lnf = text
|
||||
@@ -157,13 +166,29 @@ class OptionsController {
|
||||
boolean excludeLocalResult = view.excludeLocalResultCheckbox.model.isSelected()
|
||||
model.excludeLocalResult = excludeLocalResult
|
||||
uiSettings.excludeLocalResult = excludeLocalResult
|
||||
|
||||
boolean clearUploads = view.clearUploadsCheckbox.model.isSelected()
|
||||
model.clearUploads = clearUploads
|
||||
uiSettings.clearUploads = clearUploads
|
||||
|
||||
boolean storeSearchHistory = view.storeSearchHistoryCheckbox.model.isSelected()
|
||||
model.storeSearchHistory = storeSearchHistory
|
||||
uiSettings.storeSearchHistory = storeSearchHistory
|
||||
|
||||
uiSettings.exitOnClose = model.exitOnClose
|
||||
if (model.closeDecisionMade)
|
||||
uiSettings.closeWarning = false
|
||||
|
||||
saveUISettings()
|
||||
|
||||
cancel()
|
||||
}
|
||||
|
||||
private void saveUISettings() {
|
||||
File uiSettingsFile = new File(core.home, "gui.properties")
|
||||
uiSettingsFile.withOutputStream {
|
||||
uiSettings.write(it)
|
||||
}
|
||||
|
||||
cancel()
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
@@ -195,13 +220,32 @@ class OptionsController {
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void automaticFontAction() {
|
||||
void automaticFont() {
|
||||
model.automaticFontSize = true
|
||||
model.customFontSize = 12
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void customFontAction() {
|
||||
void customFont() {
|
||||
model.automaticFontSize = false
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void exitOnClose() {
|
||||
model.exitOnClose = true
|
||||
model.closeDecisionMade = true
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void minimizeOnClose() {
|
||||
model.exitOnClose = false
|
||||
model.closeDecisionMade = true
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void clearHistory() {
|
||||
uiSettings.searchHistory.clear()
|
||||
saveUISettings()
|
||||
JOptionPane.showMessageDialog(null, "Search history has been cleared")
|
||||
}
|
||||
}
|
@@ -99,7 +99,7 @@ class SearchTabController {
|
||||
String groupId = sender.getHumanReadableName()
|
||||
Map<String,Object> params = new HashMap<>()
|
||||
params['host'] = sender
|
||||
params['eventBus'] = core.eventBus
|
||||
params['core'] = core
|
||||
|
||||
mvcGroup.createMVCGroup("browse", groupId, params)
|
||||
}
|
||||
@@ -117,8 +117,26 @@ class SearchTabController {
|
||||
|
||||
String groupId = Base64.encode(event.infohash.getRoot())
|
||||
Map<String,Object> params = new HashMap<>()
|
||||
params['result'] = event
|
||||
params['text'] = event.comment
|
||||
params['name'] = event.name
|
||||
|
||||
mvcGroup.createMVCGroup("show-comment", groupId, params)
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void viewCertificates() {
|
||||
int[] selectedRows = view.resultsTable.getSelectedRows()
|
||||
if (selectedRows.length != 1)
|
||||
return
|
||||
if (view.lastSortEvent != null)
|
||||
selectedRows[0] = view.resultsTable.rowSorter.convertRowIndexToModel(selectedRows[0])
|
||||
UIResultEvent event = model.results[selectedRows[0]]
|
||||
if (event.certificates <= 0)
|
||||
return
|
||||
|
||||
def params = [:]
|
||||
params['result'] = event
|
||||
params['core'] = core
|
||||
mvcGroup.createMVCGroup("fetch-certificates", params)
|
||||
}
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
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 SharedFileController {
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user