Compare commits

..

31 Commits

Author SHA1 Message Date
Zlatin Balevsky
e69a5eac18 0.0.6 2019-06-03 18:30:27 +01:00
Zlatin Balevsky
6e0f1778b7 rudimentary speed gauge 2019-06-03 18:02:10 +01:00
Zlatin Balevsky
abbb741d73 show the number of sources for a result, counted by infohash 2019-06-03 17:21:08 +01:00
Zlatin Balevsky
07dfc0a1d1 destroy mvc group on options window close 2019-06-03 15:33:16 +01:00
Zlatin Balevsky
00c12cfd49 hook up download retry logic 2019-06-03 15:02:04 +01:00
Zlatin Balevsky
1ee389ff91 options dialog 2019-06-03 14:40:32 +01:00
Zlatin Balevsky
3642736cfe options dialog, wip 2019-06-03 11:32:34 +01:00
Zlatin Balevsky
b6f7f51476 verify X-Persona header if present 2019-06-03 08:12:33 +01:00
Zlatin Balevsky
4c21f2d5ae show full persona in searches 2019-06-03 08:06:51 +01:00
Zlatin Balevsky
9e0d52d548 show source in incoming searches 2019-06-03 07:43:28 +01:00
Zlatin Balevsky
fad01603de fix replyTo field 2019-06-03 07:35:09 +01:00
Zlatin Balevsky
da007795fb learn about new hosts from incoming connections too 2019-06-03 07:27:12 +01:00
Zlatin Balevsky
881d755dd3 update test work with personas 2019-06-02 22:47:43 +01:00
Zlatin Balevsky
bc3b6f500f 0.0.5 for trust panel 2019-06-02 12:18:44 +01:00
Zlatin Balevsky
8f8710801c update any result tabs on trust events 2019-06-02 12:16:28 +01:00
Zlatin Balevsky
43f3cf9b7a small ui tweak 2019-06-02 12:00:14 +01:00
Zlatin Balevsky
6fe4155678 delete accidental commit 2019-06-02 11:57:15 +01:00
Zlatin Balevsky
32f944a089 trust panel ui 2019-06-02 11:56:19 +01:00
Zlatin Balevsky
b19b5ef315 Fix for java 9+ #1 2019-06-02 10:04:27 +01:00
Zlatin Balevsky
5138935c20 add options for portable installation, issue #2 2019-06-02 09:33:28 +01:00
Zlatin Balevsky
ba596af778 Trust panel, wip 2019-06-02 05:40:44 +01:00
Zlatin Balevsky
0f4533c867 persist personas in trust files instead of destinations 2019-06-02 05:12:14 +01:00
Zlatin Balevsky
727834390c slightly better looking message 2019-06-02 04:18:15 +01:00
Zlatin Balevsky
c51e3874da show a message instead of search bar while disconnected 2019-06-02 04:12:11 +01:00
Zlatin Balevsky
d18a618575 focus on the tab of the new search 2019-06-02 03:54:34 +01:00
Zlatin Balevsky
15508f417d hack to add some horizontal space 2019-06-02 01:33:53 +01:00
Zlatin Balevsky
44dad55178 update test 2019-06-02 01:28:00 +01:00
Zlatin Balevsky
5c17e77190 change groovy version to match griffon 2019-06-02 01:20:55 +01:00
Zlatin Balevsky
de856cd085 canonize search terms 2019-06-02 00:42:18 +01:00
Zlatin Balevsky
d2533cc4d6 retry failed downloads, every 15 minutes by default 2019-06-02 00:22:33 +01:00
Zlatin Balevsky
f41cc39659 show who is downloading 2019-06-01 21:53:14 +01:00
34 changed files with 541 additions and 90 deletions

View File

@@ -3,7 +3,7 @@ subprojects {
dependencies {
compile 'net.i2p:i2p:0.9.40'
compile 'org.codehaus.groovy:groovy-all:2.5.7'
compile 'org.codehaus.groovy:groovy-all:2.4.15'
}
compileGroovy {

View File

@@ -10,4 +10,6 @@ class Constants {
public static final int MAX_HEADERS = 16
public static final float DOWNLOAD_SEQUENTIAL_RATIO = 0.8f
public static final String SPLIT_PATTERN = "[\\.,_-]"
}

View File

@@ -120,8 +120,8 @@ public class Core {
eventBus = new EventBus()
log.info("initializing trust service")
File goodTrust = new File(home, "trust.good")
File badTrust = new File(home, "trust.bad")
File goodTrust = new File(home, "trusted")
File badTrust = new File(home, "distrusted")
trustService = new TrustService(goodTrust, badTrust, 5000)
eventBus.register(TrustEvent.class, trustService)
@@ -166,7 +166,7 @@ public class Core {
eventBus.register(ResultsEvent.class, searchManager)
log.info("initializing download manager")
DownloadManager downloadManager = new DownloadManager(eventBus, i2pConnector, new File(home, "incompletes"))
DownloadManager downloadManager = new DownloadManager(eventBus, i2pConnector, new File(home, "incompletes"), me)
eventBus.register(UIDownloadEvent.class, downloadManager)
log.info("initializing upload manager")

View File

@@ -6,6 +6,7 @@ class MuWireSettings {
final boolean isLeaf
boolean allowUntrusted
int downloadRetryInterval
String nickname
File downloadLocation
String sharedFiles
@@ -23,6 +24,7 @@ class MuWireSettings {
downloadLocation = new File((String)props.getProperty("downloadLocation",
System.getProperty("user.home")))
sharedFiles = props.getProperty("sharedFiles")
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","15"))
}
void write(OutputStream out) throws IOException {
@@ -32,6 +34,7 @@ class MuWireSettings {
props.setProperty("crawlerResponse", crawlerResponse.toString())
props.setProperty("nickname", nickname)
props.setProperty("downloadLocation", downloadLocation.getAbsolutePath())
props.setProperty("downloadRetryInterval", String.valueOf(downloadRetryInterval))
if (sharedFiles != null)
props.setProperty("sharedFiles", sharedFiles)
props.store(out, "")

View File

@@ -2,6 +2,7 @@ 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
@@ -14,6 +15,7 @@ public class Persona {
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 {
@@ -59,6 +61,15 @@ public class Persona {
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()

View File

@@ -6,6 +6,7 @@ import java.util.concurrent.atomic.AtomicBoolean
import java.util.logging.Level
import com.muwire.core.EventBus
import com.muwire.core.Persona
import com.muwire.core.hostcache.HostCache
import com.muwire.core.hostcache.HostDiscoveredEvent
import com.muwire.core.search.QueryEvent
@@ -14,6 +15,7 @@ import com.muwire.core.trust.TrustLevel
import com.muwire.core.trust.TrustService
import groovy.util.logging.Log
import net.i2p.data.Base64
import net.i2p.data.Destination
@Log
@@ -120,9 +122,10 @@ abstract class Connection implements Closeable {
query.version = 1
query.uuid = e.searchEvent.getUuid()
query.firstHop = e.firstHop
// TODO: first hop figure out
query.keywords = e.searchEvent.getSearchTerms()
query.replyTo = e.getReceivedOn().toBase64()
query.replyTo = e.replyTo.toBase64()
if (e.originator != null)
query.originator = e.originator.toBase64()
messages.put(query)
}
@@ -158,11 +161,22 @@ abstract class Connection implements Closeable {
}
// TODO: add option to respond only to trusted peers
Persona originator = null
if (search.originator != null) {
originator = new Persona(new ByteArrayInputStream(Base64.decode(search.originator)))
if (originator.destination != replyTo) {
log.info("originator doesn't match destination")
return
}
}
SearchEvent searchEvent = new SearchEvent(searchTerms : search.keywords,
searchHash : search.infohash,
uuid : uuid)
QueryEvent event = new QueryEvent ( searchEvent : searchEvent,
replyTo : replyTo,
originator : originator,
receivedOn : endpoint.destination,
firstHop : search.firstHop )
eventBus.publish(event)

View File

@@ -40,7 +40,7 @@ abstract class ConnectionManager {
void onTrustEvent(TrustEvent e) {
if (e.level == TrustLevel.DISTRUSTED)
drop(e.destination)
drop(e.persona.destination)
}
abstract void drop(Destination d)

View File

@@ -1,7 +1,11 @@
package com.muwire.core.download
import com.muwire.core.connection.I2PConnector
import net.i2p.data.Base64
import com.muwire.core.EventBus
import com.muwire.core.Persona
import java.util.concurrent.Executor
import java.util.concurrent.Executors
@@ -12,12 +16,19 @@ public class DownloadManager {
private final I2PConnector connector
private final Executor executor
private final File incompletes
private final String meB64
public DownloadManager(EventBus eventBus, I2PConnector connector, File incompletes) {
public DownloadManager(EventBus eventBus, I2PConnector connector, File incompletes, Persona me) {
this.eventBus = eventBus
this.connector = connector
this.incompletes = incompletes
def baos = new ByteArrayOutputStream()
me.write(baos)
this.meB64 = Base64.encode(baos.toByteArray())
incompletes.mkdir()
this.executor = Executors.newCachedThreadPool({ r ->
Thread rv = new Thread(r)
rv.setName("download-worker")
@@ -28,7 +39,7 @@ public class DownloadManager {
public void onUIDownloadEvent(UIDownloadEvent e) {
def downloader = new Downloader(this, e.target, e.result.size,
def downloader = new Downloader(this, meB64, e.target, e.result.size,
e.result.infohash, e.result.pieceSize, connector, e.result.sender.destination,
incompletes)
executor.execute({downloader.download()} as Runnable)

View File

@@ -20,6 +20,9 @@ import java.security.NoSuchAlgorithmException
@Log
class DownloadSession {
private static int SAMPLES = 10
private final String meB64
private final Pieces pieces
private final InfoHash infoHash
private final Endpoint endpoint
@@ -28,10 +31,14 @@ class DownloadSession {
private final long fileLength
private final MessageDigest digest
private final ArrayDeque<Long> timestamps = new ArrayDeque<>(SAMPLES)
private final ArrayDeque<Integer> reads = new ArrayDeque<>(SAMPLES)
private ByteBuffer mapped
DownloadSession(Pieces pieces, InfoHash infoHash, Endpoint endpoint, File file,
DownloadSession(String meB64, Pieces pieces, InfoHash infoHash, Endpoint endpoint, File file,
int pieceSize, long fileLength) {
this.meB64 = meB64
this.pieces = pieces
this.endpoint = endpoint
this.infoHash = infoHash
@@ -60,7 +67,8 @@ class DownloadSession {
FileChannel channel
try {
os.write("GET $root\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("Range: $start-$end\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("Range: $start-$end\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("X-Persona: $meB64\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
os.flush()
String code = readTillRN(is)
if (code.startsWith("404 ")) {
@@ -119,6 +127,13 @@ class DownloadSession {
throw new IOException()
synchronized(this) {
mapped.put(tmp, 0, read)
if (timestamps.size() == SAMPLES) {
timestamps.removeFirst()
reads.removeFirst()
}
timestamps.addLast(System.currentTimeMillis())
reads.addLast(read)
}
}
@@ -141,4 +156,13 @@ class DownloadSession {
return 0
mapped.position()
}
synchronized int speed() {
if (timestamps.size() < SAMPLES)
return 0
long interval = timestamps.last - timestamps.first
int totalRead = 0
reads.each { totalRead += it }
(int)(totalRead * 1000.0 / interval)
}
}

View File

@@ -15,7 +15,8 @@ import net.i2p.data.Destination
public class Downloader {
public enum DownloadState { CONNECTING, DOWNLOADING, FAILED, CANCELLED, FINISHED }
private final DownloadManager downloadManager
private final DownloadManager downloadManager
private final String meB64
private final File file
private final Pieces pieces
private final long length
@@ -32,9 +33,10 @@ public class Downloader {
private volatile boolean cancelled
private volatile Thread downloadThread
public Downloader(DownloadManager downloadManager, File file, long length, InfoHash infoHash,
public Downloader(DownloadManager downloadManager, String meB64, File file, long length, InfoHash infoHash,
int pieceSizePow2, I2PConnector connector, Destination destination,
File incompletes) {
this.meB64 = meB64
this.downloadManager = downloadManager
this.file = file
this.infoHash = infoHash
@@ -63,7 +65,7 @@ public class Downloader {
endpoint = connector.connect(destination)
currentState = DownloadState.DOWNLOADING
while(!pieces.isComplete()) {
currentSession = new DownloadSession(pieces, infoHash, endpoint, file, pieceSize, length)
currentSession = new DownloadSession(meB64, pieces, infoHash, endpoint, file, pieceSize, length)
currentSession.request()
writePieces()
}
@@ -107,6 +109,12 @@ public class Downloader {
currentSession.positionInPiece()
}
public int speed() {
if (currentSession == null)
return 0
currentSession.speed()
}
public DownloadState getCurrentState() {
currentState
}
@@ -117,6 +125,7 @@ public class Downloader {
}
public void resume() {
currentState = DownloadState.CONNECTING
downloadManager.resume(this)
}
}

View File

@@ -55,7 +55,7 @@ class HostCache extends Service {
}
void onConnectionEvent(ConnectionEvent e) {
if (e.incoming || e.leaf)
if (e.leaf)
return
Destination dest = e.endpoint.destination
Host host = hosts.get(dest)

View File

@@ -1,6 +1,7 @@
package com.muwire.core.search
import com.muwire.core.Event
import com.muwire.core.Persona
import net.i2p.data.Destination
@@ -9,6 +10,7 @@ class QueryEvent extends Event {
SearchEvent searchEvent
boolean firstHop
Destination replyTo
Persona originator
Destination receivedOn
}

View File

@@ -1,5 +1,6 @@
package com.muwire.core.search
import com.muwire.core.Constants
class SearchIndex {
@@ -31,7 +32,7 @@ class SearchIndex {
}
private static String[] split(String source) {
source = source.replaceAll("[\\.,_-]", " ")
source = source.replaceAll(Constants.SPLIT_PATTERN, " ").toLowerCase()
source.split(" ")
}

View File

@@ -1,11 +1,10 @@
package com.muwire.core.trust
import com.muwire.core.Event
import net.i2p.data.Destination
import com.muwire.core.Persona
class TrustEvent extends Event {
Destination destination
Persona persona
TrustLevel level
}

View File

@@ -1,7 +1,11 @@
package com.muwire.core.trust
import java.util.concurrent.ConcurrentHashMap
import com.muwire.core.Persona
import com.muwire.core.Service
import net.i2p.data.Base64
import net.i2p.data.Destination
import net.i2p.util.ConcurrentHashSet
@@ -10,8 +14,8 @@ class TrustService extends Service {
final File persistGood, persistBad
final long persistInterval
final Set<Destination> good = new ConcurrentHashSet<>()
final Set<Destination> bad = new ConcurrentHashSet<>()
final Map<Destination, Persona> good = new ConcurrentHashMap<>()
final Map<Destination, Persona> bad = new ConcurrentHashMap<>()
final Timer timer
@@ -35,12 +39,16 @@ class TrustService extends Service {
void load() {
if (persistGood.exists()) {
persistGood.eachLine {
good.add(new Destination(it))
byte [] decoded = Base64.decode(it)
Persona persona = new Persona(new ByteArrayInputStream(decoded))
good.put(persona.destination, persona)
}
}
if (persistBad.exists()) {
persistBad.eachLine {
bad.add(new Destination(it))
byte [] decoded = Base64.decode(it)
Persona persona = new Persona(new ByteArrayInputStream(decoded))
bad.put(persona.destination, persona)
}
}
timer.schedule({persist()} as TimerTask, persistInterval, persistInterval)
@@ -50,22 +58,22 @@ class TrustService extends Service {
private void persist() {
persistGood.delete()
persistGood.withPrintWriter { writer ->
good.each {
writer.println it.toBase64()
good.each {k,v ->
writer.println v.toBase64()
}
}
persistBad.delete()
persistBad.withPrintWriter { writer ->
bad.each {
writer.println it.toBase64()
bad.each { k,v ->
writer.println v.toBase64()
}
}
}
TrustLevel getLevel(Destination dest) {
if (good.contains(dest))
if (good.containsKey(dest))
return TrustLevel.TRUSTED
else if (bad.contains(dest))
else if (bad.containsKey(dest))
return TrustLevel.DISTRUSTED
TrustLevel.NEUTRAL
}
@@ -73,16 +81,16 @@ class TrustService extends Service {
void onTrustEvent(TrustEvent e) {
switch(e.level) {
case TrustLevel.TRUSTED:
bad.remove(e.destination)
good.add(e.destination)
bad.remove(e.persona.destination)
good.put(e.persona.destination, e.persona)
break
case TrustLevel.DISTRUSTED:
good.remove(e.destination)
bad.add(e.destination)
good.remove(e.persona.destination)
bad.put(e.persona.destination, e.persona)
break
case TrustLevel.NEUTRAL:
good.remove(e.destination)
bad.remove(e.destination)
good.remove(e.persona.destination)
bad.remove(e.persona.destination)
break
}
}

View File

@@ -4,6 +4,7 @@ import java.nio.charset.StandardCharsets
import com.muwire.core.Constants
import com.muwire.core.InfoHash
import com.muwire.core.Persona
import groovy.util.logging.Log
import net.i2p.data.Base64
@@ -16,6 +17,7 @@ class Request {
InfoHash infoHash
Range range
Persona downloader
Map<String, String> headers
static Request parse(InfoHash infoHash, InputStream is) throws IOException {
@@ -85,7 +87,13 @@ class Request {
if (start < 0 || end < start)
throw new IOException("Invalid range $start - $end")
new Request( infoHash : infoHash, range : new Range(start, end), headers : headers)
Persona downloader = null
if (headers.containsKey("X-Persona")) {
def encoded = headers["X-Persona"].trim()
def decoded = Base64.decode(encoded)
downloader = new Persona(new ByteArrayInputStream(decoded))
}
new Request( infoHash : infoHash, range : new Range(start, end), headers : headers, downloader : downloader)
}
}

View File

@@ -62,6 +62,11 @@ public class UploadManager {
}
Request request = Request.parse(new InfoHash(infoHashRoot), e.getInputStream())
if (request.downloader != null && request.downloader.destination != e.destination) {
log.info("Downloader persona doesn't match their destination")
e.close()
return
}
Uploader uploader = new Uploader(sharedFiles.iterator().next().file, request, e)
eventBus.publish(new UploadEvent(uploader : uploader))
try {

View File

@@ -0,0 +1,11 @@
package com.muwire.core
import net.i2p.data.Base64
class Personas {
private final String encoded1 = "AQADemFiO~pgSoEo8wQfwncYMvBQWkvPY9I7DYUllHp289UE~zBaLdbl~wbliktAUsW-S70f3UeYgHq34~c7zVuUQjgHZ506iG9hX8B9S3a9gQ3CSG0GuDpeNyiXmZkpHp5m8vT9PZ1zMWzxvzZY~fP9yKFKgO4yrso5I9~DGOPeyJZJ4BFsTJDERv41aZqjFLYUBDmeHGgg9RjYy~93h-nQMVYj9JSO3AgowW-ix49rtiKYIXHMa2PxWHUXkUHWJZtIZntNIDEFeMnPdzLxjAl8so2G6pDcTMZPLLwyb73Ee5ZVfxUynPqyp~fIGVP8Rl4rlaGFli2~ATGBz3XY54aObC~0p7us2JnWaTC~oQT5DVDM7gaOO885o-m8BB8b0duzMBelbdnMZFQJ5jIHVKxkC6Niw4fxTOoXTyOqQmVhtK-9xcwxMuN5DF9IewkR5bhpq5rgnfBP5zvyBaAHMq-d3TCOjTsZ-d3liB98xX5p8G5zmS7gfKArQtM5~CcK~AlX-lGLBQAEAAcAAN5MW1Tq983szfZgY1l8tQFqy8I9tdMf7vc1Ktj~TCIvXYw6AYMbMGy3S67FSPLZVmfHEMQKj2KLAdaRKQkHPAY"
private final String encoded2 = "AQAHemxhdGluYiN~3G-hPoBfJ04mhcC52lC6TYSwWxH-WNWno9Y35JS-WrXlnPsodZtwy96ttEaiKTg-hkRqMsaYKpWar1FwayR6qlo0pZCo5pQOLfR7GIM3~wde0JIBEp8BUpgzF1-QXLhuRG1t7tBbenW2tSgp5jQH61RI-c9flyUlOvf6nrhQMZ3aoviZ4aZW23Fx-ajYQBDk7PIxuyn8qYNwWy3kWOhGan05c54NnumS3XCzQWFDDPlADmco1WROeY9qrwwtmLM8lzDCEtJQXJlk~K5yLbyB63hmAeTK7J4iS6f9nnWv7TbB5r-Z3kC6D9TLYrQbu3h4AAxrqso45P8yHQtKUA4QJicS-6NJoBOnlCCU887wx2k9YSxxwNydlIxb1mZsX65Ke4uY0HDFokZHTzUcxvfLB6G~5JkSPDCyZz~2fREgW2-VXu7gokEdEugkuZRrsiQzyfAOOkv53ti5MzTbMOXinBskSb1vZyN2-XcZNaDJvEqUNj~qpfhe-ov2F7FuwQUABAAHAAAfqq-MneIqWBQY92-sy9Z0s~iQsq6lUFa~sYMdY-5o-94fF8a140dm-emF3rO8vuidUIPNaS-37Rl05mAKUCcB"
Persona persona1 = new Persona(new ByteArrayInputStream(Base64.decode(encoded1)))
Persona persona2 = new Persona(new ByteArrayInputStream(Base64.decode(encoded2)))
}

View File

@@ -55,7 +55,7 @@ class DownloadSessionTest {
toUploader = new PipedOutputStream(fromDownloader)
endpoint = new Endpoint(null, fromUploader, toUploader, null)
session = new DownloadSession(pieces, infoHash, endpoint, target, pieceSize, size)
session = new DownloadSession("",pieces, infoHash, endpoint, target, pieceSize, size)
downloadThread = new Thread( { session.request() } as Runnable)
downloadThread.setDaemon(true)
downloadThread.start()
@@ -74,6 +74,7 @@ class DownloadSessionTest {
initSession(20)
assert "GET $rootBase64" == readTillRN(fromDownloader)
assert "Range: 0-19" == readTillRN(fromDownloader)
readTillRN(fromDownloader)
assert "" == readTillRN(fromDownloader)
toDownloader.write("200 OK\r\n".bytes)
@@ -95,6 +96,7 @@ class DownloadSessionTest {
assert "GET $rootBase64" == readTillRN(fromDownloader)
readTillRN(fromDownloader)
readTillRN(fromDownloader)
assert "" == readTillRN(fromDownloader)
toDownloader.write("200 OK\r\n".bytes)
@@ -122,6 +124,7 @@ class DownloadSessionTest {
assert (start == 0 && end == ((1 << pieceSize) - 1)) ||
(start == (1 << pieceSize) && end == (1 << pieceSize))
readTillRN(fromDownloader)
assert "" == readTillRN(fromDownloader)
toDownloader.write("200 OK\r\n".bytes)

View File

@@ -5,14 +5,17 @@ import org.junit.Before
import org.junit.Test
import com.muwire.core.Destinations
import com.muwire.core.Persona
import com.muwire.core.Personas
import net.i2p.data.Base64
import net.i2p.data.Destination
class TrustServiceTest {
TrustService service
File persistGood, persistBad
Destinations dests = new Destinations()
Personas personas = new Personas()
@Before
void before() {
@@ -33,51 +36,50 @@ class TrustServiceTest {
@Test
void testEmpty() {
assert TrustLevel.NEUTRAL == service.getLevel(dests.dest1)
assert TrustLevel.NEUTRAL == service.getLevel(dests.dest2)
assert TrustLevel.NEUTRAL == service.getLevel(personas.persona1.destination)
assert TrustLevel.NEUTRAL == service.getLevel(personas.persona2.destination)
}
@Test
void testOnEvent() {
service.onTrustEvent new TrustEvent(level: TrustLevel.TRUSTED, destination: dests.dest1)
service.onTrustEvent new TrustEvent(level: TrustLevel.DISTRUSTED, destination: dests.dest2)
service.onTrustEvent new TrustEvent(level: TrustLevel.TRUSTED, persona: personas.persona1)
service.onTrustEvent new TrustEvent(level: TrustLevel.DISTRUSTED, persona: personas.persona2)
assert TrustLevel.TRUSTED == service.getLevel(dests.dest1)
assert TrustLevel.DISTRUSTED == service.getLevel(dests.dest2)
assert TrustLevel.TRUSTED == service.getLevel(personas.persona1.destination)
assert TrustLevel.DISTRUSTED == service.getLevel(personas.persona2.destination)
}
@Test
void testPersist() {
service.onTrustEvent new TrustEvent(level: TrustLevel.TRUSTED, destination: dests.dest1)
service.onTrustEvent new TrustEvent(level: TrustLevel.DISTRUSTED, destination: dests.dest2)
service.onTrustEvent new TrustEvent(level: TrustLevel.TRUSTED, persona: personas.persona1)
service.onTrustEvent new TrustEvent(level: TrustLevel.DISTRUSTED, persona: personas.persona2)
Thread.sleep(250)
def trusted = new HashSet<>()
persistGood.eachLine {
trusted.add(new Destination(it))
trusted.add(new Persona(new ByteArrayInputStream(Base64.decode(it))))
}
def distrusted = new HashSet<>()
persistBad.eachLine {
distrusted.add(new Destination(it))
distrusted.add(new Persona(new ByteArrayInputStream(Base64.decode(it))))
}
assert trusted.size() == 1
assert trusted.contains(dests.dest1)
assert trusted.contains(personas.persona1)
assert distrusted.size() == 1
assert distrusted.contains(dests.dest2)
assert distrusted.contains(personas.persona2)
}
@Test
void testLoad() {
service.stop()
persistGood.append("${dests.dest1.toBase64()}\n")
persistBad.append("${dests.dest2.toBase64()}\n")
persistGood.append("${personas.persona1.toBase64()}\n")
persistBad.append("${personas.persona2.toBase64()}\n")
service = new TrustService(persistGood, persistBad, 100)
service.start()
Thread.sleep(10)
Thread.sleep(50)
assert TrustLevel.TRUSTED == service.getLevel(dests.dest1)
assert TrustLevel.DISTRUSTED == service.getLevel(dests.dest2)
assert TrustLevel.TRUSTED == service.getLevel(personas.persona1.destination)
assert TrustLevel.DISTRUSTED == service.getLevel(personas.persona2.destination)
}
}

View File

@@ -1,5 +1,5 @@
group = com.muwire
version = 0.0.4
version = 0.0.6
groovyVersion = 2.4.15
slf4jVersion = 1.7.25
spockVersion = 1.1-groovy-2.4

View File

@@ -58,6 +58,7 @@ dependencies {
compile "org.codehaus.griffon:griffon-guice:${griffon.version}"
runtime "org.slf4j:slf4j-simple:${slf4jVersion}"
runtime "javax.annotation:javax.annotation-api:1.3.2"
testCompile "org.codehaus.griffon:griffon-fest-test:${griffon.version}"
testCompile "org.spockframework:spock-core:${spockVersion}"

View File

@@ -21,4 +21,9 @@ mvcGroups {
view = 'com.muwire.gui.SearchTabView'
controller = 'com.muwire.gui.SearchTabController'
}
}
'Options' {
model = 'com.muwire.gui.OptionsModel'
view = 'com.muwire.gui.OptionsView'
controller = 'com.muwire.gui.OptionsController'
}
}

View File

@@ -10,6 +10,7 @@ import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
import javax.inject.Inject
import com.muwire.core.Constants
import com.muwire.core.Core
import com.muwire.core.download.DownloadStartedEvent
import com.muwire.core.download.UIDownloadEvent
@@ -31,6 +32,9 @@ class MainFrameController {
@ControllerAction
void search() {
def cardsPanel = builder.getVariable("cards-panel")
cardsPanel.getLayout().show(cardsPanel, "search window")
def search = builder.getVariable("search-field").text
def uuid = UUID.randomUUID()
Map<String, Object> params = new HashMap<>()
@@ -39,9 +43,12 @@ class MainFrameController {
def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params)
model.results[uuid.toString()] = group
def searchEvent = new SearchEvent(searchTerms : [search], uuid : uuid)
// this can be improved a lot
def terms = search.toLowerCase().trim().split(Constants.SPLIT_PATTERN)
def searchEvent = new SearchEvent(searchTerms : terms, uuid : uuid)
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true,
replyTo: core.me.destination, receivedOn: core.me.destination))
replyTo: core.me.destination, receivedOn: core.me.destination,
originator : core.me))
}
private def selectedResult() {
@@ -73,7 +80,7 @@ class MainFrameController {
def result = selectedResult()
if (result == null)
return // TODO disable button
core.eventBus.publish( new TrustEvent(destination : result.sender.destination, level : TrustLevel.TRUSTED))
core.eventBus.publish( new TrustEvent(persona : result.sender, level : TrustLevel.TRUSTED))
}
@ControllerAction
@@ -81,7 +88,7 @@ class MainFrameController {
def result = selectedResult()
if (result == null)
return // TODO disable button
core.eventBus.publish( new TrustEvent(destination : result.sender.destination, level : TrustLevel.DISTRUSTED))
core.eventBus.publish( new TrustEvent(persona : result.sender, level : TrustLevel.DISTRUSTED))
}
@ControllerAction
@@ -95,6 +102,33 @@ class MainFrameController {
def downloader = selectedDownload()
downloader.resume()
}
private void markTrust(String tableName, TrustLevel level, def list) {
int row = builder.getVariable(tableName).getSelectedRow()
if (row < 0)
return
core.eventBus.publish(new TrustEvent(persona : list[row], level : level))
}
@ControllerAction
void markTrusted() {
markTrust("distrusted-table", TrustLevel.TRUSTED, model.distrusted)
}
@ControllerAction
void markNeutralFromDistrusted() {
markTrust("distrusted-table", TrustLevel.NEUTRAL, model.distrusted)
}
@ControllerAction
void markDistrusted() {
markTrust("trusted-table", TrustLevel.DISTRUSTED, model.trusted)
}
@ControllerAction
void markNeutralFromTrusted() {
markTrust("trusted-table", TrustLevel.NEUTRAL, model.trusted)
}
void mvcGroupInit(Map<String, String> args) {
application.addPropertyChangeListener("core", {e->

View File

@@ -0,0 +1,37 @@
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 OptionsController {
@MVCMember @Nonnull
OptionsModel model
@MVCMember @Nonnull
OptionsView view
@ControllerAction
void save() {
String text = view.retryField.text
model.downloadRetryInterval = text
def settings = application.context.get("muwire-settings")
settings.downloadRetryInterval = Integer.valueOf(text)
File settingsFile = new File(application.context.get("core").home, "MuWire.properties")
settingsFile.withOutputStream {
settings.write(it)
}
cancel()
}
@ControllerAction
void cancel() {
view.d.setVisible(false)
mvcGroup.destroy()
}
}

View File

@@ -28,7 +28,11 @@ class Ready extends AbstractLifecycleHandler {
@Override
void execute() {
log.info "starting core services"
def home = System.getProperty("user.home") + File.separator + ".MuWire"
def portableHome = System.getProperty("portable.home")
def home = portableHome == null ?
System.getProperty("user.home") + File.separator + ".MuWire" :
portableHome
home = new File(home)
if (!home.exists()) {
log.info("creating home dir")
@@ -64,8 +68,13 @@ class Ready extends AbstractLifecycleHandler {
nickname = nickname.trim()
break
}
props.setNickname(nickname)
while(true) {
def portableDownloads = System.getProperty("portable.downloads")
if (portableDownloads != null) {
props.downloadLocation = new File(portableDownloads)
} else {
def chooser = new JFileChooser()
chooser.setDialogTitle("Select a directory where downloads will be saved")
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY)
@@ -75,9 +84,8 @@ class Ready extends AbstractLifecycleHandler {
System.exit(0)
}
props.downloadLocation = chooser.getSelectedFile()
break
}
props.setNickname(nickname)
propsFile.withOutputStream {
props.write(it)
}

View File

@@ -8,10 +8,12 @@ import javax.swing.JTable
import com.muwire.core.Core
import com.muwire.core.InfoHash
import com.muwire.core.Persona
import com.muwire.core.connection.ConnectionAttemptStatus
import com.muwire.core.connection.ConnectionEvent
import com.muwire.core.connection.DisconnectionEvent
import com.muwire.core.download.DownloadStartedEvent
import com.muwire.core.download.Downloader
import com.muwire.core.files.FileHashedEvent
import com.muwire.core.files.FileLoadedEvent
import com.muwire.core.files.FileSharedEvent
@@ -44,6 +46,8 @@ class MainFrameModel {
def shared = []
def connectionList = []
def searches = new LinkedList()
def trusted = []
def distrusted = []
@Observable int connections
@Observable String me
@@ -54,8 +58,31 @@ class MainFrameModel {
private final Set<InfoHash> infoHashes = new HashSet<>()
volatile Core core
private long lastRetryTime = System.currentTimeMillis()
void updateTablePreservingSelection(String tableName) {
def downloadTable = builder.getVariable(tableName)
int selectedRow = downloadTable.getSelectedRow()
downloadTable.model.fireTableDataChanged()
downloadTable.selectionModel.setSelectionInterval(selectedRow,selectedRow)
}
void mvcGroupInit(Map<String, Object> args) {
Timer timer = new Timer("download-pumper", true)
timer.schedule({
runInsideUIAsync {
if (!mvcGroup.alive)
return
builder.getVariable("uploads-table")?.model.fireTableDataChanged()
updateTablePreservingSelection("downloads-table")
updateTablePreservingSelection("trusted-table")
updateTablePreservingSelection("distrusted-table")
}
}, 1000, 1000)
application.addPropertyChangeListener("core", {e ->
coreInitialized = (e.getNewValue() != null)
core = e.getNewValue()
@@ -70,20 +97,32 @@ class MainFrameModel {
core.eventBus.register(UploadFinishedEvent.class, this)
core.eventBus.register(TrustEvent.class, this)
core.eventBus.register(QueryEvent.class, this)
})
Timer timer = new Timer("download-pumper", true)
timer.schedule({
timer.schedule({
int retryInterval = application.context.get("muwire-settings").downloadRetryInterval
if (retryInterval > 0) {
retryInterval *= 60000
long now = System.currentTimeMillis()
if (now - lastRetryTime > retryInterval) {
lastRetryTime = now
runInsideUIAsync {
downloads.each {
if (it.downloader.currentState == Downloader.DownloadState.FAILED)
it.downloader.resume()
updateTablePreservingSelection("downloads-table")
}
}
}
}
}, 60000, 60000)
runInsideUIAsync {
if (!mvcGroup.alive)
return
builder.getVariable("uploads-table")?.model.fireTableDataChanged()
def downloadTable = builder.getVariable("downloads-table")
int selectedRow = downloadTable.getSelectedRow()
downloadTable.model.fireTableDataChanged()
downloadTable.selectionModel.setSelectionInterval(selectedRow,selectedRow)
trusted.addAll(core.trustService.good.values())
distrusted.addAll(core.trustService.bad.values())
}
}, 1000, 1000)
})
}
void onUIResultEvent(UIResultEvent e) {
@@ -103,6 +142,11 @@ class MainFrameModel {
runInsideUIAsync {
connections = core.connectionManager.getConnections().size()
if (connections > 0) {
def topPanel = builder.getVariable("top-panel")
topPanel.getLayout().show(topPanel, "top-search-panel")
}
connectionList.add(e.endpoint.destination)
JTable table = builder.getVariable("connections-table")
table.model.fireTableDataChanged()
@@ -112,6 +156,12 @@ class MainFrameModel {
void onDisconnectionEvent(DisconnectionEvent e) {
runInsideUIAsync {
connections = core.connectionManager.getConnections().size()
if (connections == 0) {
def topPanel = builder.getVariable("top-panel")
topPanel.getLayout().show(topPanel, "top-connect-panel")
}
connectionList.remove(e.destination)
JTable table = builder.getVariable("connections-table")
table.model.fireTableDataChanged()
@@ -160,8 +210,18 @@ class MainFrameModel {
void onTrustEvent(TrustEvent e) {
runInsideUIAsync {
JTable table = builder.getVariable("results-table")
table.model.fireTableDataChanged()
trusted.clear()
trusted.addAll(core.trustService.good.values())
distrusted.clear()
distrusted.addAll(core.trustService.bad.values())
updateTablePreservingSelection("trusted-table")
updateTablePreservingSelection("distrusted-table")
results.values().each {
it.view.pane.getClientProperty("results-table")?.model.fireTableDataChanged()
}
}
}
@@ -177,11 +237,17 @@ class MainFrameModel {
if (search.trim().size() == 0)
return
runInsideUIAsync {
searches.addFirst(search)
searches.addFirst(new IncomingSearch(search : search, replyTo : e.replyTo, originator : e.originator))
while(searches.size() > 200)
searches.removeLast()
JTable table = builder.getVariable("searches-table")
table.model.fireTableDataChanged()
}
}
class IncomingSearch {
String search
Destination replyTo
Persona originator
}
}

View File

@@ -0,0 +1,14 @@
package com.muwire.gui
import griffon.core.artifact.GriffonModel
import griffon.transform.Observable
import griffon.metadata.ArtifactProviderFor
@ArtifactProviderFor(GriffonModel)
class OptionsModel {
@Observable String downloadRetryInterval
void mvcGroupInit(Map<String, String> args) {
downloadRetryInterval = application.context.get("muwire-settings").downloadRetryInterval
}
}

View File

@@ -21,6 +21,7 @@ class SearchTabModel {
Core core
String uuid
def results = []
def hashCount = [:]
void mvcGroupInit(Map<String, String> args) {
@@ -34,6 +35,12 @@ class SearchTabModel {
void handleResult(UIResultEvent e) {
runInsideUIAsync {
Integer count = hashCount.get(e.infohash)
if (count == null)
count = 0
count++
hashCount[e.infohash] = count
results << e
JTable table = builder.getVariable("results-table")
table.model.fireTableDataChanged()

View File

@@ -22,6 +22,7 @@ import java.awt.FlowLayout
import java.awt.GridBagConstraints
import java.awt.GridBagLayout
import java.awt.Insets
import java.nio.charset.StandardCharsets
import javax.annotation.Nonnull
@@ -43,6 +44,11 @@ class MainFrameView {
imageIcon('/griffon-icon-16x16.png').image],
pack : false,
visible : bind { model.coreInitialized }) {
menuBar {
menu (text : "Options") {
menuItem("Configuration", actionPerformed : {mvcGroup.createMVCGroup("Options")})
}
}
borderLayout()
panel (border: etchedBorder(), constraints : BorderLayout.NORTH) {
borderLayout()
@@ -51,15 +57,24 @@ class MainFrameView {
button(text: "Searches", actionPerformed : showSearchWindow)
button(text: "Uploads", actionPerformed : showUploadsWindow)
button(text: "Monitor", actionPerformed : showMonitorWindow)
button(text: "Trust", actionPerformed : showTrustWindow)
}
panel(constraints: BorderLayout.CENTER) {
borderLayout()
label("Enter search here:", constraints: BorderLayout.WEST)
textField(id: "search-field", constraints: BorderLayout.CENTER, action : searchAction)
}
panel( constraints: BorderLayout.EAST) {
button(text: "Search", searchAction)
panel(id: "top-panel", constraints: BorderLayout.CENTER) {
cardLayout()
label(constraints : "top-connect-panel",
text : " MuWire is connecting, please wait. You will be able to search soon.") // TODO: real padding
panel(constraints : "top-search-panel") {
borderLayout()
panel(constraints: BorderLayout.CENTER) {
borderLayout()
label(" Enter search here:", constraints: BorderLayout.WEST) // TODO: fix this
textField(id: "search-field", constraints: BorderLayout.CENTER, action : searchAction)
}
panel( constraints: BorderLayout.EAST) {
button(text: "Search", searchAction)
}
}
}
}
panel (id: "cards-panel", constraints : BorderLayout.CENTER) {
@@ -94,6 +109,7 @@ class MainFrameView {
int pieceSize = row.downloader.pieceSize // TODO: fix for last piece
"$position/$pieceSize bytes"
})
closureColumn(header: "Speed (bytes/second)", type:Integer, read :{row -> row.downloader.speed()})
}
}
}
@@ -136,6 +152,9 @@ class MainFrameView {
int percent = (int)((position * 100.0) / total)
"$percent%"
})
closureColumn(header : "Downloader", type : String, read : { row ->
row.request.downloader?.getHumanReadableName()
})
}
}
}
@@ -164,12 +183,52 @@ class MainFrameView {
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "searches-table") {
tableModel(list : model.searches) {
closureColumn(header : "Keywords", type : String, read : { it })
closureColumn(header : "Keywords", type : String, read : { it.search })
closureColumn(header : "From", type : String, read : {
if (it.originator != null) {
return it.originator.getHumanReadableName()
} else {
return it.replyTo.toBase32()
}
})
}
}
}
}
}
panel(constraints : "trust window") {
gridLayout(rows: 1, cols :2)
panel (border : etchedBorder()){
borderLayout()
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "trusted-table") {
tableModel(list : model.trusted) {
closureColumn(header : "Trusted Users", type : String, read : { it.getHumanReadableName() } )
}
}
}
panel (constraints : BorderLayout.EAST) {
gridBagLayout()
button(text : "Mark Neutral", constraints : gbc(gridx: 0, gridy: 0), markNeutralFromTrustedAction)
button(text : "Mark Distrusted", constraints : gbc(gridx: 0, gridy:1), markDistrustedAction)
}
}
panel (border : etchedBorder()){
borderLayout()
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "distrusted-table") {
tableModel(list : model.distrusted) {
closureColumn(header: "Distrusted Users", type : String, read : { it.getHumanReadableName() } )
}
}
}
panel(constraints : BorderLayout.WEST) {
gridBagLayout()
button(text: "Mark Neutral", constraints: gbc(gridx: 0, gridy: 0), markNeutralFromDistrustedAction)
button(text: "Mark Trusted", constraints : gbc(gridx: 0, gridy : 1), markTrustedAction)
}
}
}
}
panel (border: etchedBorder(), constraints : BorderLayout.SOUTH) {
borderLayout()
@@ -223,6 +282,11 @@ class MainFrameView {
cardsPanel.getLayout().show(cardsPanel,"monitor window")
}
def showTrustWindow = {
def cardsPanel = builder.getVariable("cards-panel")
cardsPanel.getLayout().show(cardsPanel,"trust window")
}
def shareFiles = {
def chooser = new JFileChooser()
chooser.setDialogTitle("Select file or directory to share")

View File

@@ -0,0 +1,54 @@
package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.swing.JDialog
import javax.swing.SwingConstants
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class OptionsView {
@MVCMember @Nonnull
FactoryBuilderSupport builder
@MVCMember @Nonnull
OptionsModel model
def d
def p
def retryField
def mainFrame
void initUI() {
mainFrame = application.windowManager.findWindow("main-frame")
d = new JDialog(mainFrame, "Options", true)
d.setResizable(false)
p = builder.panel {
gridBagLayout()
label(text : "Retry failed downloads every", constraints : gbc(gridx: 0, gridy: 0))
retryField = textField(text : bind { model.downloadRetryInterval }, columns : 2, constraints : gbc(gridx: 1, gridy: 0))
label(text : "minutes", constraints : gbc(gridx : 2, gridy: 0))
button(text : "Save", constraints : gbc(gridx : 1, gridy: 1), saveAction)
button(text : "Cancel", constraints : gbc(gridx : 2, gridy: 1), cancelAction)
}
}
void mvcGroupInit(Map<String,String> args) {
d.getContentPane().add(p)
d.pack()
d.setLocationRelativeTo(mainFrame)
d.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
d.addWindowListener(new WindowAdapter() {
public void windowClosed(WindowEvent e) {
mvcGroup.destroy()
}
})
d.show()
}
}

View File

@@ -31,6 +31,7 @@ class SearchTabView {
tableModel(list: model.results) {
closureColumn(header: "Name", type: String, read : {row -> row.name})
closureColumn(header: "Size", preferredWidth: 150, type: Long, read : {row -> row.size})
closureColumn(header: "Sources", type : Integer, read : { row -> model.hashCount[row.infohash]})
closureColumn(header: "Sender", type: String, read : {row -> row.sender.getHumanReadableName()})
closureColumn(header: "Trust", type: String, read : {row ->
model.core.trustService.getLevel(row.sender.destination)
@@ -55,6 +56,7 @@ class SearchTabView {
parent = mvcGroup.parentGroup.view.builder.getVariable("result-tabs")
parent.addTab(searchTerms, pane)
int index = parent.indexOfTab(searchTerms)
parent.setSelectedIndex(index)
def tabPanel
builder.with {

View File

@@ -0,0 +1,25 @@
package com.muwire.gui
import griffon.core.test.GriffonFestRule
import org.fest.swing.fixture.FrameFixture
import org.junit.Rule
import org.junit.Test
import static org.junit.Assert.fail
class OptionsIntegrationTest {
static {
System.setProperty('griffon.swing.edt.violations.check', 'true')
System.setProperty('griffon.swing.edt.hang.monitor', 'true')
}
@Rule
public final GriffonFestRule fest = new GriffonFestRule()
private FrameFixture window
@Test
void smokeTest() {
fail('Not implemented yet!')
}
}

View File

@@ -0,0 +1,21 @@
package com.muwire.gui
import griffon.core.test.GriffonUnitRule
import griffon.core.test.TestFor
import org.junit.Rule
import org.junit.Test
import static org.junit.Assert.fail
@TestFor(OptionsController)
class OptionsControllerTest {
private OptionsController controller
@Rule
public final GriffonUnitRule griffon = new GriffonUnitRule()
@Test
void smokeTest() {
fail('Not yet implemented!')
}
}