Compare commits

..

39 Commits

Author SHA1 Message Date
Zlatin Balevsky
a2637570b1 Release 0.6.1 2019-11-10 06:23:28 +00:00
Zlatin Balevsky
6012adbeab fix unsharing of files with comments 2019-11-10 06:04:57 +00:00
Zlatin Balevsky
8f6b6b0caa update test for new json format 2019-11-10 05:20:09 +00:00
Zlatin Balevsky
8f3b5aea8d store lowercases in search index 2019-11-10 05:14:31 +00:00
Zlatin Balevsky
ee098ace8e update readme 2019-11-09 20:11:03 +00:00
Zlatin Balevsky
5d8401e4bf avoid NPE, pending further investigation 2019-11-09 20:10:21 +00:00
Zlatin Balevsky
fbf9add82a Release 0.6.0 2019-11-09 19:27:36 +00:00
Zlatin Balevsky
7379263fef extended signature in cli 2019-11-09 18:34:34 +00:00
Zlatin Balevsky
7d50843754 make signed queries mandatory 2019-11-09 17:03:38 +00:00
Zlatin Balevsky
f4a2864942 add extended signature in queries to prevent replay attacks 2019-11-09 16:39:16 +00:00
Zlatin Balevsky
afaadf65a4 only set selected row if the table contains that many rows. That fixes an AIOOBE 2019-11-09 15:14:14 +00:00
Zlatin Balevsky
7bd422d6b4 another instance of unexplained npe 2019-11-09 12:36:59 +00:00
Zlatin Balevsky
3f47274f61 add option to open containing folder 2019-11-09 11:28:12 +00:00
Zlatin Balevsky
419e9a0ce6 prevent npe when..? unclear when this happens 2019-11-09 11:01:55 +00:00
Zlatin Balevsky
ac1068a681 fix show comment/certificate buttons in group-by-file mode 2019-11-09 10:53:38 +00:00
Zlatin Balevsky
549457e36f close output stream silently 2019-11-08 21:46:44 +00:00
Zlatin Balevsky
14d6d10546 Release 0.5.10 2019-11-08 21:11:20 +00:00
Zlatin Balevsky
878e397aa0 preserve selections on update 2019-11-08 21:04:58 +00:00
Zlatin Balevsky
27831b488b add getter and use it; account for the case where a file has no certificates 2019-11-08 19:20:06 +00:00
Zlatin Balevsky
449f46c62b take list updating out of loop 2019-11-08 18:40:59 +00:00
Zlatin Balevsky
5703b85386 workaround? 2019-11-08 18:36:23 +00:00
Zlatin Balevsky
76d8d847bd wip on grouping by file 2019-11-08 18:15:54 +00:00
Zlatin Balevsky
db84d8e5bf wip on grouping by file 2019-11-08 17:33:41 +00:00
Zlatin Balevsky
cc9b384907 wip on grouping by file 2019-11-08 16:09:05 +00:00
Zlatin Balevsky
72960c24a8 implement trust reason in cli 2019-11-08 14:41:10 +00:00
Zlatin Balevsky
71298e5e73 proper rendering of date on subscriptions table 2019-11-08 08:31:00 +00:00
Zlatin Balevsky
11bc672544 say never if timestamp is 0 2019-11-08 08:30:44 +00:00
Zlatin Balevsky
2f6cd311a0 say never if timestamp is 0 2019-11-08 08:30:29 +00:00
Zlatin Balevsky
0448750491 lowercase for consistency 2019-11-08 08:18:33 +00:00
Zlatin Balevsky
800dd1cbba proper date sorting 2019-11-08 08:17:34 +00:00
Zlatin Balevsky
f95e9450f3 OutputStream.write 2019-11-08 07:47:11 +00:00
Zlatin Balevsky
d842e3f2f2 update for new object 2019-11-08 07:42:33 +00:00
Zlatin Balevsky
2017b53a43 pass comments on trust list subscriptions 2019-11-08 07:37:51 +00:00
Zlatin Balevsky
6e2b3f4f33 prompt for reason from review trust list view 2019-11-08 07:12:17 +00:00
Zlatin Balevsky
dbb305139b update for new type 2019-11-08 06:53:22 +00:00
Zlatin Balevsky
0801bfec08 add optinal reason for trusting/distrusting 2019-11-08 06:46:03 +00:00
Zlatin Balevsky
00a8d100fe show certificate comment form file details view 2019-11-08 04:51:37 +00:00
Zlatin Balevsky
e94b7cb0d4 prevent NPE when browsed from an older host 2019-11-08 04:02:11 +00:00
Zlatin Balevsky
b0357f2ecd update readme 2019-11-08 02:50:42 +00:00
43 changed files with 933 additions and 301 deletions

View File

@@ -4,7 +4,7 @@ MuWire is an easy to use file-sharing program which offers anonymity using [I2P
It is inspired by the LimeWire Gnutella client and developped by a former LimeWire developer.
The current stable release - 0.5.5 is avaiable for download at https://muwire.com. You can find technical documentation in the "doc" folder.
The current stable release - 0.6.0 is avaiable for download at https://muwire.com. You can find technical documentation in the "doc" folder.
### Building

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -406,7 +406,7 @@ public class Core {
}
}
Core core = new Core(props, home, "0.5.9")
Core core = new Core(props, home, "0.6.1")
core.startServices()
// ... at the end, sleep or execute script

View File

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

View File

@@ -159,7 +159,9 @@ class ConnectionAcceptor {
}
} catch (Exception ex) {
log.log(Level.WARNING, "incoming connection failed",ex)
e.getOutputStream().close()
try {
e.getOutputStream().close()
} catch (Exception ignore) {}
e.close()
eventBus.publish new ConnectionEvent(endpoint: e, incoming: true, leaf: null, status: ConnectionAttemptStatus.FAILED)
}
@@ -208,7 +210,9 @@ class ConnectionAcceptor {
os.writeShort(json.bytes.length)
os.write(json.bytes)
}
e.outputStream.close()
try {
e.outputStream.close()
} catch (Exception ignored) {}
e.close()
eventBus.publish(new ConnectionEvent(endpoint: e, incoming: true, leaf: leaf, status: ConnectionAttemptStatus.REJECTED))
}
@@ -379,10 +383,10 @@ class ConnectionAcceptor {
dis.readFully(RUST)
if (RUST != "RUST\r\n".getBytes(StandardCharsets.US_ASCII))
throw new IOException("Invalid TRUST connection")
String header
while ((header = DataUtil.readTillRN(dis)) != ""); // ignore headers for now
OutputStream os = e.getOutputStream()
Map<String,String> headers = DataUtil.readAllHeaders(dis)
OutputStream os = e.getOutputStream()
if (!settings.allowTrustLists) {
os.write("403 Not Allowed\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
os.flush()
@@ -390,22 +394,53 @@ class ConnectionAcceptor {
return
}
os.write("200 OK\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
List<Persona> good = new ArrayList<>(trustService.good.values())
int size = Math.min(Short.MAX_VALUE * 2, good.size())
good = good.subList(0, size)
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
boolean json = headers.containsKey('Json') && Boolean.parseBoolean(headers['Json'])
List<TrustService.TrustEntry> good = new ArrayList<>(trustService.good.values())
List<TrustService.TrustEntry> bad = new ArrayList<>(trustService.bad.values())
DataOutputStream dos = new DataOutputStream(os)
dos.writeShort(size)
good.each {
it.write(dos)
}
List<Persona> bad = new ArrayList<>(trustService.bad.values())
size = Math.min(Short.MAX_VALUE * 2, bad.size())
bad = bad.subList(0, size)
dos.writeShort(size)
bad.each {
it.write(dos)
if (!json) {
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
int size = Math.min(Short.MAX_VALUE * 2, good.size())
good = good.subList(0, size)
dos.writeShort(size)
good.each {
it.persona.write(dos)
}
size = Math.min(Short.MAX_VALUE * 2, bad.size())
bad = bad.subList(0, size)
dos.writeShort(size)
bad.each {
it.persona.write(dos)
}
} else {
dos.write("Json: true\r\n".getBytes(StandardCharsets.US_ASCII))
dos.write("Good:${good.size()}\r\n".getBytes(StandardCharsets.US_ASCII))
dos.write("Bad:${bad.size()}\r\n".getBytes(StandardCharsets.US_ASCII))
dos.write("\r\n".getBytes(StandardCharsets.US_ASCII))
good.each {
def obj = [:]
obj.persona = it.persona.toBase64()
obj.reason = it.reason
String toJson = JsonOutput.toJson(obj)
byte [] payload = toJson.getBytes(StandardCharsets.US_ASCII)
dos.writeShort(payload.length)
dos.write(payload)
}
bad.each {
def obj = [:]
obj.persona = it.persona.toBase64()
obj.reason = it.reason
String toJson = JsonOutput.toJson(obj)
byte [] payload = toJson.getBytes(StandardCharsets.US_ASCII)
dos.writeShort(payload.length)
dos.write(payload)
}
}
dos.flush()

View File

@@ -143,6 +143,7 @@ class FileManager {
String comment = sf.getComment()
if (comment != null) {
comment = DataUtil.readi18nString(Base64.decode(comment))
Set<File> existingComment = commentToFile.get(comment)
if (existingComment != null) {
existingComment.remove(sf.getFile())
@@ -229,7 +230,7 @@ class FileManager {
return files
Set<SharedFile> rv = new HashSet<>()
files.each {
if (it.getPieceSize() != 0)
if (it != null && it.getPieceSize() != 0)
rv.add(it)
}
rv

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,4 +13,6 @@ public class Constants {
public static final int MAX_RESULTS = 0x1 << 16;
public static final int MAX_COMMENT_LENGTH = 0x1 << 15;
public static final long MAX_QUERY_AGE = 5 * 60 * 1000L;
}

View File

@@ -109,6 +109,10 @@ public class SharedFile {
return downloaders;
}
public Set<SearchEntry> getSearches() {
return searches;
}
public void addDownloader(String name) {
downloaders.add(name);
}

View File

@@ -15,11 +15,15 @@ import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import com.muwire.core.Constants;
import net.i2p.crypto.DSAEngine;
import net.i2p.data.Base64;
import net.i2p.data.Signature;
import net.i2p.data.SigningPrivateKey;
import net.i2p.util.ConcurrentHashSet;
public class DataUtil {
@@ -203,4 +207,10 @@ public class DataUtil {
.collect(Collectors.joining(","));
props.setProperty(property, encoded);
}
public static byte[] signUUID(UUID uuid, long timestamp, SigningPrivateKey spk) {
byte [] payload = (uuid.toString() + String.valueOf(timestamp)).getBytes(StandardCharsets.US_ASCII);
Signature sig = DSAEngine.getInstance().sign(payload, spk);
return sig.getData();
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,10 +7,13 @@ import griffon.core.mvc.MVCGroup
import griffon.core.mvc.MVCGroupConfiguration
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import groovy.json.StringEscapeUtils
import net.i2p.crypto.DSAEngine
import net.i2p.data.Base64
import net.i2p.data.Signature
import net.i2p.data.SigningPrivateKey
import java.awt.Desktop
import java.awt.event.ActionEvent
import java.nio.charset.StandardCharsets
@@ -42,6 +45,7 @@ import com.muwire.core.trust.TrustLevel
import com.muwire.core.trust.TrustSubscriptionEvent
import com.muwire.core.upload.HashListUploader
import com.muwire.core.upload.Uploader
import com.muwire.core.util.DataUtil
@ArtifactProviderFor(GriffonController)
class MainFrameController {
@@ -90,6 +94,7 @@ class MainFrameController {
params["search-terms"] = search
params["uuid"] = uuid.toString()
params["core"] = core
params["settings"] = view.settings
def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params)
model.results[uuid.toString()] = group
@@ -119,9 +124,10 @@ class MainFrameController {
Signature sig = DSAEngine.getInstance().sign(payload, core.spk)
long timestamp = System.currentTimeMillis()
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : firstHop,
replyTo: core.me.destination, receivedOn: core.me.destination,
originator : core.me, sig : sig.data))
originator : core.me, sig : sig.data, queryTime : timestamp, sig2 : DataUtil.signUUID(uuid, timestamp, core.spk)))
}
@@ -138,14 +144,16 @@ class MainFrameController {
byte [] infoHashBytes = Base64.decode(infoHash)
Signature sig = DSAEngine.getInstance().sign(infoHashBytes, core.spk)
long timestamp = System.currentTimeMillis()
byte [] sig2 = DataUtil.signUUID(uuid, timestamp, core.spk)
def searchEvent = new SearchEvent(searchHash : Base64.decode(infoHash), uuid:uuid,
oobInfohash: true, persona : core.me)
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true,
replyTo: core.me.destination, receivedOn: core.me.destination,
originator : core.me, sig : sig.data))
originator : core.me, sig : sig.data, queryTime : timestamp, sig2 : sig2))
}
private int selectedDownload() {
def downloadsTable = builder.getVariable("downloads-table")
def selected = downloadsTable.getSelectedRow()
@@ -160,8 +168,9 @@ class MainFrameController {
int selected = builder.getVariable("searches-table").getSelectedRow()
if (selected < 0)
return
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
Persona p = model.searches[selected].originator
core.eventBus.publish( new TrustEvent(persona : p, level : TrustLevel.TRUSTED) )
core.eventBus.publish( new TrustEvent(persona : p, level : TrustLevel.TRUSTED, reason : reason) )
}
@ControllerAction
@@ -169,8 +178,9 @@ class MainFrameController {
int selected = builder.getVariable("searches-table").getSelectedRow()
if (selected < 0)
return
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
Persona p = model.searches[selected].originator
core.eventBus.publish( new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED) )
core.eventBus.publish( new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED, reason : reason) )
}
@ControllerAction
@@ -217,7 +227,7 @@ class MainFrameController {
if (row < 0)
return
builder.getVariable(tableName).model.fireTableDataChanged()
core.eventBus.publish(new TrustEvent(persona : list[row], level : level))
core.eventBus.publish(new TrustEvent(persona : list[row].persona, level : level))
}
@ControllerAction
@@ -255,7 +265,7 @@ class MainFrameController {
int row = view.getSelectedTrustTablesRow("trusted-table")
if (row < 0)
return
Persona p = model.trusted[row]
Persona p = model.trusted[row].persona
core.muOptions.trustSubscriptions.add(p)
saveMuWireSettings()
core.eventBus.publish(new TrustSubscriptionEvent(persona : p, subscribe : true))
@@ -380,7 +390,7 @@ class MainFrameController {
@ControllerAction
void showFileDetails() {
def selected = view.selectedSharedFiles()
if (selected.size() != 1) {
if (selected == null || selected.size() != 1) {
JOptionPane.showMessageDialog(null, "Please select only one file to view it's details")
return
}
@@ -389,6 +399,19 @@ class MainFrameController {
params['core'] = core
mvcGroup.createMVCGroup("shared-file", params)
}
@ControllerAction
void openContainingFolder() {
def selected = view.selectedSharedFiles()
if (selected == null || selected.size() != 1) {
JOptionPane.showMessageDialog(null, "Please select only one file to open it's containing folder")
return
}
try {
Desktop.getDesktop().open(selected[0].file.getParentFile())
} catch (Exception ignored) {}
}
void saveMuWireSettings() {
core.saveMuSettings()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,9 +16,11 @@ class SharedFileModel {
def downloaders = []
def certificates = []
@Observable boolean showCommentActionEnabled
public void mvcGroupInit(Map<String,String> args) {
searchers.addAll(sf.searches)
downloaders.addAll(sf.downloaders)
certificates.addAll(core.certificateManager.byInfoHash.get(sf.infoHash))
searchers.addAll(sf.getSearches())
downloaders.addAll(sf.getDownloaders())
certificates.addAll(core.certificateManager.byInfoHash.getOrDefault(sf.infoHash,[]))
}
}

View File

@@ -65,10 +65,7 @@ class CertificateControlView {
closureColumn(header : "File Name", type : String, read : {it.name.name})
closureColumn(header : "Hash", type : String, read : {Base64.encode(it.infoHash.getRoot())})
closureColumn(header : "Comment", preferredWidth : 20, type : Boolean, read : {it.comment != null})
closureColumn(header : "Timestamp", type : String, read : {
def date = new Date(it.timestamp)
date.toString()
})
closureColumn(header : "Timestamp", type : Long, read : { it.timestamp })
}
}
}
@@ -113,6 +110,8 @@ class CertificateControlView {
}
})
certsTable.setDefaultRenderer(Long.class, new DateRenderer())
dialog.getContentPane().add(panel)
dialog.pack()
dialog.setLocationRelativeTo(mainFrame)

View File

@@ -422,7 +422,8 @@ class MainFrameView {
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "trusted-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.trusted) {
closureColumn(header : "Trusted Users", type : String, read : { it.getHumanReadableName() } )
closureColumn(header : "Trusted Users", type : String, read : { it.persona.getHumanReadableName() } )
closureColumn(header : "Reason", type : String, read : {it.reason})
}
}
}
@@ -438,7 +439,8 @@ class MainFrameView {
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "distrusted-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.distrusted) {
closureColumn(header: "Distrusted Users", type : String, read : { it.getHumanReadableName() } )
closureColumn(header: "Distrusted Users", type : String, read : { it.persona.getHumanReadableName() } )
closureColumn(header: "Reason", type : String, read : {it.reason})
}
}
}
@@ -461,12 +463,7 @@ class MainFrameView {
closureColumn(header : "Trusted", preferredWidth : 20, type: Integer, read : {it.good.size()})
closureColumn(header : "Distrusted", preferredWidth: 20, type: Integer, read : {it.bad.size()})
closureColumn(header : "Status", preferredWidth: 30, type: String, read : {it.status.toString()})
closureColumn(header : "Last Updated", preferredWidth: 200, type : String, read : {
if (it.timestamp == 0)
return "Never"
else
return String.valueOf(new Date(it.timestamp))
})
closureColumn(header : "Last Updated", preferredWidth: 200, type : Long, read : { it.timestamp })
}
}
}
@@ -611,6 +608,9 @@ class MainFrameView {
JMenuItem certifySelectedFiles = new JMenuItem("Certify selected files")
certifySelectedFiles.addActionListener({mvcGroup.controller.issueCertificate()})
sharedFilesMenu.add(certifySelectedFiles)
JMenuItem openContainingFolder = new JMenuItem("Open containing folder")
openContainingFolder.addActionListener({mvcGroup.controller.openContainingFolder()})
sharedFilesMenu.add(openContainingFolder)
JMenuItem showFileDetails = new JMenuItem("Show file details")
showFileDetails.addActionListener({mvcGroup.controller.showFileDetails()})
sharedFilesMenu.add(showFileDetails)
@@ -743,6 +743,8 @@ class MainFrameView {
break
}
})
subscriptionTable.setDefaultRenderer(Long.class, new DateRenderer())
// trusted table
def trustedTable = builder.getVariable("trusted-table")

View File

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

View File

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

View File

@@ -5,10 +5,17 @@ import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.swing.JDialog
import javax.swing.JMenuItem
import javax.swing.JPopupMenu
import javax.swing.JTabbedPane
import javax.swing.ListSelectionModel
import javax.swing.SwingConstants
import com.muwire.core.filecert.Certificate
import java.awt.BorderLayout
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
@@ -20,14 +27,18 @@ class SharedFileView {
FactoryBuilderSupport builder
@MVCMember @Nonnull
SharedFileModel model
@MVCMember @Nonnull
SharedFileController controller
def mainFrame
def dialog
def panel
def searchersPanel
def searchersTable
def downloadersPanel
def certificatesTable
def certificatesPanel
def lastCertificateSortEvent
void initUI() {
mainFrame = application.windowManager.findWindow("main-frame")
@@ -38,14 +49,11 @@ class SharedFileView {
searchersPanel = builder.panel {
borderLayout()
scrollPane(constraints : BorderLayout.CENTER) {
table(autoCreateRowSorter : true, rowHeight : rowHeight) {
searchersTable = table(autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.searchers) {
closureColumn(header : "Searcher", type : String, read : {it.searcher.getHumanReadableName()})
closureColumn(header : "Searcher", type : String, read : {it.searcher?.getHumanReadableName()})
closureColumn(header : "Query", type : String, read : {it.query})
closureColumn(header : "Timestamp", type : String, read : {
Date d = new Date(it.timestamp)
d.toString()
})
closureColumn(header : "Timestamp", type : Long, read : {it.timestamp})
}
}
}
@@ -70,17 +78,42 @@ class SharedFileView {
closureColumn(header : "Issuer", type:String, read : {it.issuer.getHumanReadableName()})
closureColumn(header : "File Name", type : String, read : {it.name.name})
closureColumn(header : "Comment", type : Boolean, read : {it.comment != null})
closureColumn(header : "Timestamp", type : String, read : {
Date d = new Date(it.timestamp)
d.toString()
})
closureColumn(header : "Timestamp", type : Long, read : {it.timestamp})
}
}
}
panel(constraints : BorderLayout.SOUTH) {
button(text : "Show Comment", enabled : bind {model.showCommentActionEnabled}, showCommentAction)
}
}
}
void mvcGroupInit(Map<String,String> args) {
certificatesTable.rowSorter.addRowSorterListener({evt -> lastCertificateSortEvent = evt})
def selectionModel = certificatesTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
selectionModel.addListSelectionListener({
Certificate c = getSelectedCertificate()
model.showCommentActionEnabled = c != null && c.comment != null
})
certificatesTable.addMouseListener(new MouseAdapter() {
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger())
showMenu(e)
}
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger())
showMenu(e)
}
})
certificatesTable.setDefaultRenderer(Long.class, new DateRenderer())
searchersTable.setDefaultRenderer(Long.class, new DateRenderer())
def tabbedPane = new JTabbedPane()
tabbedPane.addTab("Search Hits", searchersPanel)
tabbedPane.addTab("Downloaders", downloadersPanel)
@@ -99,4 +132,23 @@ class SharedFileView {
show()
}
}
Certificate getSelectedCertificate() {
int selectedRow = certificatesTable.getSelectedRow()
if (selectedRow < 0)
return null
if (lastCertificateSortEvent != null)
selectedRow = certificatesTable.rowSorter.convertRowIndexToModel(selectedRow)
model.certificates[selectedRow]
}
private void showMenu(MouseEvent e) {
if (!model.showCommentActionEnabled)
return
JPopupMenu menu = new JPopupMenu()
JMenuItem showComment = new JMenuItem("Show Comment")
showComment.addActionListener({controller.showComment()})
menu.add(showComment)
menu.show(e.getComponent(), e.getX(), e.getY())
}
}

View File

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

View File

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

View File

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

View File

@@ -18,6 +18,7 @@ class UISettings {
boolean exitOnClose
boolean clearUploads
boolean storeSearchHistory
boolean groupByFile
Set<String> searchHistory
Set<String> openTabs
@@ -36,6 +37,7 @@ class UISettings {
exitOnClose = Boolean.parseBoolean(props.getProperty("exitOnClose","false"))
clearUploads = Boolean.parseBoolean(props.getProperty("clearUploads","false"))
storeSearchHistory = Boolean.parseBoolean(props.getProperty("storeSearchHistory","true"))
groupByFile = Boolean.parseBoolean(props.getProperty("groupByFile","false"))
searchHistory = DataUtil.readEncodedSet(props, "searchHistory")
openTabs = DataUtil.readEncodedSet(props, "openTabs")
@@ -56,6 +58,7 @@ class UISettings {
props.setProperty("exitOnClose", String.valueOf(exitOnClose))
props.setProperty("clearUploads", String.valueOf(clearUploads))
props.setProperty("storeSearchHistory", String.valueOf(storeSearchHistory))
props.setProperty("groupByFile", String.valueOf(groupByFile))
if (font != null)
props.setProperty("font", font)