Compare commits

..

22 Commits

Author SHA1 Message Date
Zlatin Balevsky
16b475bd9a 0.0.7 for multi-source downloads 2019-06-04 04:17:29 +01:00
Zlatin Balevsky
3cea1870cd multisource downloads, untested 2019-06-04 03:30:55 +01:00
Zlatin Balevsky
e7240dcb6f keep track of claimed pieces in preparation for multi-source downloads 2019-06-04 02:18:30 +01:00
Zlatin Balevsky
c91440cbfc config option for update check interval 2019-06-03 23:30:39 +01:00
Zlatin Balevsky
294605f5c7 basic update notification 2019-06-03 23:23:07 +01:00
Zlatin Balevsky
986caf3a75 backend for checking updates 2019-06-03 23:11:03 +01:00
Zlatin Balevsky
8524d5309f typo 2019-06-03 21:53:51 +01:00
Zlatin Balevsky
48b3ac2b4a wip on update server 2019-06-03 21:50:46 +01:00
Zlatin Balevsky
18f21dc247 update server 2019-06-03 21:47:31 +01:00
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
34 changed files with 789 additions and 111 deletions

View File

@@ -32,6 +32,7 @@ import com.muwire.core.search.SearchEvent
import com.muwire.core.search.SearchManager import com.muwire.core.search.SearchManager
import com.muwire.core.trust.TrustEvent import com.muwire.core.trust.TrustEvent
import com.muwire.core.trust.TrustService import com.muwire.core.trust.TrustService
import com.muwire.core.update.UpdateClient
import com.muwire.core.upload.UploadManager import com.muwire.core.upload.UploadManager
import com.muwire.core.util.MuWireLogManager import com.muwire.core.util.MuWireLogManager
@@ -61,11 +62,12 @@ public class Core {
private final HostCache hostCache private final HostCache hostCache
private final ConnectionManager connectionManager private final ConnectionManager connectionManager
private final CacheClient cacheClient private final CacheClient cacheClient
private final UpdateClient updateClient
private final ConnectionAcceptor connectionAcceptor private final ConnectionAcceptor connectionAcceptor
private final ConnectionEstablisher connectionEstablisher private final ConnectionEstablisher connectionEstablisher
private final HasherService hasherService private final HasherService hasherService
public Core(MuWireSettings props, File home) { public Core(MuWireSettings props, File home, String myVersion) {
this.home = home this.home = home
log.info "Initializing I2P context" log.info "Initializing I2P context"
I2PAppContext.getGlobalContext().logManager() I2PAppContext.getGlobalContext().logManager()
@@ -154,6 +156,9 @@ public class Core {
log.info("initializing cache client") log.info("initializing cache client")
cacheClient = new CacheClient(eventBus,hostCache, connectionManager, i2pSession, props, 10000) cacheClient = new CacheClient(eventBus,hostCache, connectionManager, i2pSession, props, 10000)
log.info("initializing update client")
updateClient = new UpdateClient(eventBus, i2pSession, myVersion, props)
log.info("initializing connector") log.info("initializing connector")
I2PConnector i2pConnector = new I2PConnector(socketManager) I2PConnector i2pConnector = new I2PConnector(socketManager)
@@ -197,6 +202,7 @@ public class Core {
connectionAcceptor.start() connectionAcceptor.start()
connectionEstablisher.start() connectionEstablisher.start()
hostCache.waitForLoad() hostCache.waitForLoad()
updateClient.start()
} }
public void shutdown() { public void shutdown() {
@@ -227,7 +233,7 @@ public class Core {
} }
} }
Core core = new Core(props, home) Core core = new Core(props, home, "0.0.7")
core.startServices() core.startServices()
// ... at the end, sleep or execute script // ... at the end, sleep or execute script

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ package com.muwire.core.download
import com.muwire.core.connection.I2PConnector import com.muwire.core.connection.I2PConnector
import net.i2p.data.Base64 import net.i2p.data.Base64
import net.i2p.data.Destination
import com.muwire.core.EventBus import com.muwire.core.EventBus
import com.muwire.core.Persona import com.muwire.core.Persona
@@ -16,16 +17,13 @@ public class DownloadManager {
private final I2PConnector connector private final I2PConnector connector
private final Executor executor private final Executor executor
private final File incompletes private final File incompletes
private final String meB64 private final Persona me
public DownloadManager(EventBus eventBus, I2PConnector connector, File incompletes, Persona me) { public DownloadManager(EventBus eventBus, I2PConnector connector, File incompletes, Persona me) {
this.eventBus = eventBus this.eventBus = eventBus
this.connector = connector this.connector = connector
this.incompletes = incompletes this.incompletes = incompletes
this.me = me
def baos = new ByteArrayOutputStream()
me.write(baos)
this.meB64 = Base64.encode(baos.toByteArray())
incompletes.mkdir() incompletes.mkdir()
@@ -39,8 +37,18 @@ public class DownloadManager {
public void onUIDownloadEvent(UIDownloadEvent e) { public void onUIDownloadEvent(UIDownloadEvent e) {
def downloader = new Downloader(this, meB64, e.target, e.result.size,
e.result.infohash, e.result.pieceSize, connector, e.result.sender.destination, def size = e.result[0].size
def infohash = e.result[0].infohash
def pieceSize = e.result[0].pieceSize
Set<Destination> destinations = new HashSet<>()
e.result.each {
destinations.add(it.sender.destination)
}
def downloader = new Downloader(this, me, e.target, size,
infohash, pieceSize, connector, destinations,
incompletes) incompletes)
executor.execute({downloader.download()} as Runnable) executor.execute({downloader.download()} as Runnable)
eventBus.publish(new DownloadStartedEvent(downloader : downloader)) eventBus.publish(new DownloadStartedEvent(downloader : downloader))

View File

@@ -20,8 +20,10 @@ import java.security.NoSuchAlgorithmException
@Log @Log
class DownloadSession { class DownloadSession {
private static int SAMPLES = 10
private final String meB64 private final String meB64
private final Pieces pieces private final Pieces downloaded, claimed
private final InfoHash infoHash private final InfoHash infoHash
private final Endpoint endpoint private final Endpoint endpoint
private final File file private final File file
@@ -29,12 +31,16 @@ class DownloadSession {
private final long fileLength private final long fileLength
private final MessageDigest digest private final MessageDigest digest
private final ArrayDeque<Long> timestamps = new ArrayDeque<>(SAMPLES)
private final ArrayDeque<Integer> reads = new ArrayDeque<>(SAMPLES)
private ByteBuffer mapped private ByteBuffer mapped
DownloadSession(String meB64, Pieces pieces, InfoHash infoHash, Endpoint endpoint, File file, DownloadSession(String meB64, Pieces downloaded, Pieces claimed, InfoHash infoHash, Endpoint endpoint, File file,
int pieceSize, long fileLength) { int pieceSize, long fileLength) {
this.meB64 = meB64 this.meB64 = meB64
this.pieces = pieces this.downloaded = downloaded
this.claimed = claimed
this.endpoint = endpoint this.endpoint = endpoint
this.infoHash = infoHash this.infoHash = infoHash
this.file = file this.file = file
@@ -48,11 +54,31 @@ class DownloadSession {
} }
} }
public void request() throws IOException { /**
* @return if the request will proceed. The only time it may not
* is if all the pieces have been claimed by other sessions.
* @throws IOException
*/
public boolean request() throws IOException {
OutputStream os = endpoint.getOutputStream() OutputStream os = endpoint.getOutputStream()
InputStream is = endpoint.getInputStream() InputStream is = endpoint.getInputStream()
int piece = pieces.getRandomPiece() int piece
while(true) {
piece = downloaded.getRandomPiece()
if (claimed.isMarked(piece)) {
if (downloaded.donePieces() + claimed.donePieces() == downloaded.nPieces) {
log.info("all pieces claimed")
return false
}
continue
}
break
}
claimed.markDownloaded(piece)
log.info("will download piece $piece")
long start = piece * pieceSize long start = piece * pieceSize
long end = Math.min(fileLength, start + pieceSize) - 1 long end = Math.min(fileLength, start + pieceSize) - 1
long length = end - start + 1 long length = end - start + 1
@@ -122,6 +148,13 @@ class DownloadSession {
throw new IOException() throw new IOException()
synchronized(this) { synchronized(this) {
mapped.put(tmp, 0, read) mapped.put(tmp, 0, read)
if (timestamps.size() == SAMPLES) {
timestamps.removeFirst()
reads.removeFirst()
}
timestamps.addLast(System.currentTimeMillis())
reads.addLast(read)
} }
} }
@@ -133,10 +166,12 @@ class DownloadSession {
if (hash != expected) if (hash != expected)
throw new BadHashException() throw new BadHashException()
pieces.markDownloaded(piece) downloaded.markDownloaded(piece)
} finally { } finally {
claimed.clear(piece)
try { channel?.close() } catch (IOException ignore) {} try { channel?.close() } catch (IOException ignore) {}
} }
return true
} }
synchronized int positionInPiece() { synchronized int positionInPiece() {
@@ -144,4 +179,13 @@ class DownloadSession {
return 0 return 0
mapped.position() 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

@@ -1,8 +1,12 @@
package com.muwire.core.download package com.muwire.core.download
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
import com.muwire.core.Persona
import com.muwire.core.connection.Endpoint import com.muwire.core.connection.Endpoint
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.logging.Level import java.util.logging.Level
import com.muwire.core.Constants import com.muwire.core.Constants
@@ -14,35 +18,41 @@ import net.i2p.data.Destination
@Log @Log
public class Downloader { public class Downloader {
public enum DownloadState { CONNECTING, DOWNLOADING, FAILED, CANCELLED, FINISHED } public enum DownloadState { CONNECTING, DOWNLOADING, FAILED, CANCELLED, FINISHED }
private enum WorkerState { CONNECTING, DOWNLOADING, FINISHED}
private static final ExecutorService executorService = Executors.newCachedThreadPool({r ->
Thread rv = new Thread(r)
rv.setName("download worker")
rv.setDaemon(true)
rv
})
private final DownloadManager downloadManager private final DownloadManager downloadManager
private final String meB64 private final Persona me
private final File file private final File file
private final Pieces pieces private final Pieces downloaded, claimed
private final long length private final long length
private final InfoHash infoHash private final InfoHash infoHash
private final int pieceSize private final int pieceSize
private final I2PConnector connector private final I2PConnector connector
private final Destination destination private final Set<Destination> destinations
private final int nPieces private final int nPieces
private final File piecesFile private final File piecesFile
private final Map<Destination, DownloadWorker> activeWorkers = new ConcurrentHashMap<>()
private Endpoint endpoint
private volatile DownloadSession currentSession
private volatile DownloadState currentState
private volatile boolean cancelled private volatile boolean cancelled
private volatile Thread downloadThread
public Downloader(DownloadManager downloadManager, String meB64, File file, long length, InfoHash infoHash, public Downloader(DownloadManager downloadManager, Persona me, File file, long length, InfoHash infoHash,
int pieceSizePow2, I2PConnector connector, Destination destination, int pieceSizePow2, I2PConnector connector, Set<Destination> destinations,
File incompletes) { File incompletes) {
this.meB64 = meB64 this.me = me
this.downloadManager = downloadManager this.downloadManager = downloadManager
this.file = file this.file = file
this.infoHash = infoHash this.infoHash = infoHash
this.length = length this.length = length
this.connector = connector this.connector = connector
this.destination = destination this.destinations = destinations
this.piecesFile = new File(incompletes, file.getName()+".pieces") this.piecesFile = new File(incompletes, file.getName()+".pieces")
this.pieceSize = 1 << pieceSizePow2 this.pieceSize = 1 << pieceSizePow2
@@ -53,32 +63,18 @@ public class Downloader {
nPieces = length / pieceSize + 1 nPieces = length / pieceSize + 1
this.nPieces = nPieces this.nPieces = nPieces
pieces = new Pieces(nPieces, Constants.DOWNLOAD_SEQUENTIAL_RATIO) downloaded = new Pieces(nPieces, Constants.DOWNLOAD_SEQUENTIAL_RATIO)
currentState = DownloadState.CONNECTING claimed = new Pieces(nPieces)
} }
void download() { void download() {
readPieces() readPieces()
downloadThread = Thread.currentThread() destinations.each {
Endpoint endpoint = null if (it != me.destination) {
try { def worker = new DownloadWorker(it)
endpoint = connector.connect(destination) activeWorkers.put(it, worker)
currentState = DownloadState.DOWNLOADING executorService.submit(worker)
while(!pieces.isComplete()) {
currentSession = new DownloadSession(meB64, pieces, infoHash, endpoint, file, pieceSize, length)
currentSession.request()
writePieces()
} }
currentState = DownloadState.FINISHED
piecesFile.delete()
} catch (Exception bad) {
log.log(Level.WARNING,"Exception while downloading",bad)
if (cancelled)
currentState = DownloadState.CANCELLED
else if (currentState != DownloadState.FINISHED)
currentState = DownloadState.FAILED
} finally {
endpoint?.close()
} }
} }
@@ -87,38 +83,112 @@ public class Downloader {
return return
piecesFile.withReader { piecesFile.withReader {
int piece = Integer.parseInt(it.readLine()) int piece = Integer.parseInt(it.readLine())
pieces.markDownloaded(piece) downloaded.markDownloaded(piece)
} }
} }
void writePieces() { void writePieces() {
piecesFile.withPrintWriter { writer -> piecesFile.withPrintWriter { writer ->
pieces.getDownloaded().each { piece -> downloaded.getDownloaded().each { piece ->
writer.println(piece) writer.println(piece)
} }
} }
} }
public long donePieces() { public long donePieces() {
pieces.donePieces() downloaded.donePieces()
} }
public int positionInPiece() {
if (currentSession == null) public int speed() {
return 0 int total = 0
currentSession.positionInPiece() activeWorkers.values().each {
total += it.speed()
}
total
} }
public DownloadState getCurrentState() { public DownloadState getCurrentState() {
currentState if (cancelled)
return DownloadState.CANCELLED
boolean allFinished = true
activeWorkers.values().each {
allFinished &= it.currentState == WorkerState.FINISHED
}
if (allFinished) {
if (downloaded.isComplete())
return DownloadState.FINISHED
return DownloadState.FAILED
}
// if at least one is downloading...
boolean oneDownloading = false
activeWorkers.values().each {
if (it.currentState == WorkerState.DOWNLOADING) {
oneDownloading = true
return
}
}
if (oneDownloading)
return DownloadState.DOWNLOADING
return DownloadState.CONNECTING
} }
public void cancel() { public void cancel() {
cancelled = true cancelled = true
downloadThread?.interrupt() activeWorkers.values().each {
it.cancel()
}
} }
public void resume() { public void resume() {
downloadManager.resume(this) downloadManager.resume(this)
} }
class DownloadWorker implements Runnable {
private final Destination destination
private volatile WorkerState currentState
private volatile Thread downloadThread
private Endpoint endpoint
private volatile DownloadSession currentSession
DownloadWorker(Destination destination) {
this.destination = destination
}
public void run() {
downloadThread = Thread.currentThread()
currentState = WorkerState.CONNECTING
Endpoint endpoint = null
try {
endpoint = connector.connect(destination)
currentState = WorkerState.DOWNLOADING
boolean requestPerformed
while(!downloaded.isComplete()) {
currentSession = new DownloadSession(me.toBase64(), downloaded, claimed, infoHash, endpoint, file, pieceSize, length)
requestPerformed = currentSession.request()
if (!requestPerformed)
break
writePieces()
}
} catch (Exception bad) {
log.log(Level.WARNING,"Exception while downloading",bad)
} finally {
currentState = WorkerState.FINISHED
endpoint?.close()
}
}
int speed() {
if (currentSession == null)
return 0
currentSession.speed()
}
void cancel() {
downloadThread?.interrupt()
}
}
} }

View File

@@ -28,7 +28,8 @@ class Pieces {
while(true) { while(true) {
int start = random.nextInt(nPieces) int start = random.nextInt(nPieces)
while(bitSet.get(start) && ++start < nPieces); if (bitSet.get(start))
continue
return start return start
} }
} }
@@ -45,10 +46,18 @@ class Pieces {
bitSet.set(piece) bitSet.set(piece)
} }
synchronized void clear(int piece) {
bitSet.clear(piece)
}
synchronized boolean isComplete() { synchronized boolean isComplete() {
bitSet.cardinality() == nPieces bitSet.cardinality() == nPieces
} }
synchronized boolean isMarked(int piece) {
bitSet.get(piece)
}
synchronized int donePieces() { synchronized int donePieces() {
bitSet.cardinality() bitSet.cardinality()
} }

View File

@@ -5,6 +5,6 @@ import com.muwire.core.search.UIResultEvent
class UIDownloadEvent extends Event { class UIDownloadEvent extends Event {
UIResultEvent result UIResultEvent[] result
File target File target
} }

View File

@@ -65,7 +65,7 @@ class CacheClient {
options.setSendLeaseSet(true) options.setSendLeaseSet(true)
CacheServers.getCacheServers().each { CacheServers.getCacheServers().each {
log.info "Querying hostcache ${it.toBase32()}" log.info "Querying hostcache ${it.toBase32()}"
session.sendMessage(it, ping, 0, ping.length, I2PSession.PROTO_DATAGRAM, 0, 0, options) session.sendMessage(it, ping, 0, ping.length, I2PSession.PROTO_DATAGRAM, 1, 0, options)
} }
} }

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
package com.muwire.core.update
import com.muwire.core.Event
class UpdateAvailableEvent extends Event {
String version
String signer
}

View File

@@ -0,0 +1,132 @@
package com.muwire.core.update
import java.util.logging.Level
import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import groovy.util.logging.Log
import net.i2p.client.I2PSession
import net.i2p.client.I2PSessionMuxedListener
import net.i2p.client.SendMessageOptions
import net.i2p.client.datagram.I2PDatagramDissector
import net.i2p.client.datagram.I2PDatagramMaker
import net.i2p.util.VersionComparator
@Log
class UpdateClient {
final EventBus eventBus
final I2PSession session
final String myVersion
final MuWireSettings settings
private final Timer timer
private long lastUpdateCheckTime
UpdateClient(EventBus eventBus, I2PSession session, String myVersion, MuWireSettings settings) {
this.eventBus = eventBus
this.session = session
this.myVersion = myVersion
this.settings = settings
timer = new Timer("update-client",true)
}
void start() {
session.addMuxedSessionListener(new Listener(), I2PSession.PROTO_DATAGRAM, 2)
timer.schedule({checkUpdate()} as TimerTask, 30000, 60 * 60 * 1000)
}
void stop() {
timer.cancel()
}
private void checkUpdate() {
final long now = System.currentTimeMillis()
if (lastUpdateCheckTime > 0) {
if (now - lastUpdateCheckTime < settings.updateCheckInterval * 60 * 60 * 1000)
return
}
lastUpdateCheckTime = now
log.info("checking for update")
def ping = [version : 1, myVersion : myVersion]
ping = JsonOutput.toJson(ping)
def maker = new I2PDatagramMaker(session)
ping = maker.makeI2PDatagram(ping.bytes)
def options = new SendMessageOptions()
options.setSendLeaseSet(true)
session.sendMessage(UpdateServers.UPDATE_SERVER, ping, 0, ping.length, I2PSession.PROTO_DATAGRAM, 2, 0, options)
}
class Listener implements I2PSessionMuxedListener {
final JsonSlurper slurper = new JsonSlurper()
@Override
public void messageAvailable(I2PSession session, int msgId, long size) {
}
@Override
public void messageAvailable(I2PSession session, int msgId, long size, int proto, int fromport, int toport) {
if (proto != I2PSession.PROTO_DATAGRAM) {
log.warning "Received unexpected protocol $proto"
return
}
def payload = session.receiveMessage(msgId)
def dissector = new I2PDatagramDissector()
try {
dissector.loadI2PDatagram(payload)
def sender = dissector.getSender()
if (sender != UpdateServers.UPDATE_SERVER) {
log.warning("received something not from update server " + sender.toBase32())
return
}
log.info("Received something from update server")
payload = dissector.getPayload()
payload = slurper.parse(payload)
if (payload.version == null) {
log.warning("version missing")
return
}
if (payload.signer == null) {
log.warning("signer missing")
}
if (VersionComparator.comp(myVersion, payload.version) >= 0) {
log.info("no new version available")
return
}
log.info("new version $payload.version available, publishing event")
eventBus.publish(new UpdateAvailableEvent(version : payload.version, signer : payload.signer))
} catch (Exception e) {
log.log(Level.WARNING,"Invalid datagram",e)
}
}
@Override
public void reportAbuse(I2PSession session, int severity) {
}
@Override
public void disconnected(I2PSession session) {
log.severe("I2P session disconnected")
}
@Override
public void errorOccurred(I2PSession session, String message, Throwable error) {
log.log(Level.SEVERE, message, error)
}
}
}

View File

@@ -0,0 +1,7 @@
package com.muwire.core.update
import net.i2p.data.Destination
class UpdateServers {
static final Destination UPDATE_SERVER = new Destination("pSWieSRB3czCl3Zz4WpKp4Z8tjv-05zbogRDS7SEnKcSdWOupVwjzQ92GsgQh1VqgoSRk1F8dpZOnHxxz5HFy9D7ri0uFdkMyXdSKoB7IgkkvCfTAyEmeaPwSYnurF3Zk7u286E7YG2rZkQZgJ77tow7ZS0mxFB7Z0Ti-VkZ9~GeGePW~howwNm4iSQACZA0DyTpI8iv5j4I0itPCQRgaGziob~Vfvjk49nd8N4jtaDGo9cEcafikVzQ2OgBgYWL6LRbrrItwuGqsDvITUHWaElUYIDhRQYUq8gYiUA6rwAJputfhFU0J7lIxFR9vVY7YzRvcFckfr0DNI4VQVVlPnRPkUxQa--BlldMaCIppWugjgKLwqiSiHywKpSMlBWgY2z1ry4ueEBo1WEP-mEf88wRk4cFQBCKtctCQnIG2GsnATqTl-VGUAsuzeNWZiFSwXiTy~gQ094yWx-K06fFZUDt4CMiLZVhGlixiInD~34FCRC9LVMtFcqiFB2M-Ql2AAAA")
}

View File

@@ -62,6 +62,11 @@ public class UploadManager {
} }
Request request = Request.parse(new InfoHash(infoHashRoot), e.getInputStream()) 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) Uploader uploader = new Uploader(sharedFiles.iterator().next().file, request, e)
eventBus.publish(new UploadEvent(uploader : uploader)) eventBus.publish(new UploadEvent(uploader : uploader))
try { 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

@@ -15,7 +15,7 @@ class DownloadSessionTest {
private File source, target private File source, target
private InfoHash infoHash private InfoHash infoHash
private Endpoint endpoint private Endpoint endpoint
private Pieces pieces private Pieces pieces, claimed
private String rootBase64 private String rootBase64
private DownloadSession session private DownloadSession session
@@ -24,7 +24,7 @@ class DownloadSessionTest {
private InputStream fromDownloader, fromUploader private InputStream fromDownloader, fromUploader
private OutputStream toDownloader, toUploader private OutputStream toDownloader, toUploader
private void initSession(int size) { private void initSession(int size, def claimedPieces = []) {
Random r = new Random() Random r = new Random()
byte [] content = new byte[size] byte [] content = new byte[size]
r.nextBytes(content) r.nextBytes(content)
@@ -48,6 +48,8 @@ class DownloadSessionTest {
else else
nPieces = size / pieceSize + 1 nPieces = size / pieceSize + 1
pieces = new Pieces(nPieces) pieces = new Pieces(nPieces)
claimed = new Pieces(nPieces)
claimedPieces.each {claimed.markDownloaded(it)}
fromDownloader = new PipedInputStream() fromDownloader = new PipedInputStream()
fromUploader = new PipedInputStream() fromUploader = new PipedInputStream()
@@ -55,7 +57,7 @@ class DownloadSessionTest {
toUploader = new PipedOutputStream(fromDownloader) toUploader = new PipedOutputStream(fromDownloader)
endpoint = new Endpoint(null, fromUploader, toUploader, null) endpoint = new Endpoint(null, fromUploader, toUploader, null)
session = new DownloadSession("",pieces, infoHash, endpoint, target, pieceSize, size) session = new DownloadSession("",pieces, claimed, infoHash, endpoint, target, pieceSize, size)
downloadThread = new Thread( { session.request() } as Runnable) downloadThread = new Thread( { session.request() } as Runnable)
downloadThread.setDaemon(true) downloadThread.setDaemon(true)
downloadThread.start() downloadThread.start()
@@ -138,4 +140,29 @@ class DownloadSessionTest {
assert !pieces.isComplete() assert !pieces.isComplete()
assert 1 == pieces.donePieces() assert 1 == pieces.donePieces()
} }
@Test
public void testSmallFileClaimed() {
initSession(20, [0])
long now = System.currentTimeMillis()
downloadThread.join(100)
assert 100 > (System.currentTimeMillis() - now)
}
@Test
public void testClaimedPiecesAvoided() {
int pieceSize = FileHasher.getPieceSize(1)
int size = (1 << pieceSize) * 10
initSession(size, [1,2,3,4,5,6,7,8,9])
assert !claimed.isMarked(0)
assert "GET $rootBase64" == readTillRN(fromDownloader)
String range = readTillRN(fromDownloader)
def matcher = (range =~ /^Range: (\d+)-(\d+)$/)
int start = Integer.parseInt(matcher[0][1])
int end = Integer.parseInt(matcher[0][2])
assert claimed.isMarked(0)
assert start == 0 && end == (1 << pieceSize) - 1
}
} }

View File

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

View File

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

View File

@@ -47,7 +47,8 @@ class MainFrameController {
def terms = search.toLowerCase().trim().split(Constants.SPLIT_PATTERN) def terms = search.toLowerCase().trim().split(Constants.SPLIT_PATTERN)
def searchEvent = new SearchEvent(searchTerms : terms, uuid : uuid) def searchEvent = new SearchEvent(searchTerms : terms, uuid : uuid)
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true, core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true,
replyTo: core.me.destination, receivedOn: core.me.destination)) replyTo: core.me.destination, receivedOn: core.me.destination,
originator : core.me))
} }
private def selectedResult() { private def selectedResult() {
@@ -70,8 +71,15 @@ class MainFrameController {
def result = selectedResult() def result = selectedResult()
if (result == null) if (result == null)
return // TODO disable button return // TODO disable button
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name) def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
core.eventBus.publish(new UIDownloadEvent(result : result, target : file))
def selected = builder.getVariable("result-tabs").getSelectedComponent()
def group = selected.getClientProperty("mvc-group")
def resultsBucket = group.model.hashBucket[result.infohash]
core.eventBus.publish(new UIDownloadEvent(result : resultsBucket, target : file))
} }
@ControllerAction @ControllerAction

View File

@@ -0,0 +1,41 @@
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)
text = view.updateField.text
model.updateCheckInterval = text
settings.updateCheckInterval = 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

@@ -1,4 +1,5 @@
import griffon.core.GriffonApplication import griffon.core.GriffonApplication
import griffon.core.env.Metadata
import groovy.util.logging.Log import groovy.util.logging.Log
import org.codehaus.griffon.runtime.core.AbstractLifecycleHandler import org.codehaus.griffon.runtime.core.AbstractLifecycleHandler
@@ -20,6 +21,9 @@ import java.util.logging.Level
@Log @Log
class Ready extends AbstractLifecycleHandler { class Ready extends AbstractLifecycleHandler {
@Inject Metadata metadata
@Inject @Inject
Ready(@Nonnull GriffonApplication application) { Ready(@Nonnull GriffonApplication application) {
super(application) super(application)
@@ -90,9 +94,10 @@ class Ready extends AbstractLifecycleHandler {
props.write(it) props.write(it)
} }
} }
Core core Core core
try { try {
core = new Core(props, home) core = new Core(props, home, metadata["application.version"])
} catch (Exception bad) { } catch (Exception bad) {
log.log(Level.SEVERE,"couldn't initialize core",bad) log.log(Level.SEVERE,"couldn't initialize core",bad)
JOptionPane.showMessageDialog(null, "Couldn't connect to I2P router. Make sure I2P is running and restart MuWire", JOptionPane.showMessageDialog(null, "Couldn't connect to I2P router. Make sure I2P is running and restart MuWire",

View File

@@ -4,10 +4,12 @@ import java.util.concurrent.ConcurrentHashMap
import javax.annotation.Nonnull import javax.annotation.Nonnull
import javax.inject.Inject import javax.inject.Inject
import javax.swing.JOptionPane
import javax.swing.JTable import javax.swing.JTable
import com.muwire.core.Core import com.muwire.core.Core
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
import com.muwire.core.Persona
import com.muwire.core.connection.ConnectionAttemptStatus import com.muwire.core.connection.ConnectionAttemptStatus
import com.muwire.core.connection.ConnectionEvent import com.muwire.core.connection.ConnectionEvent
import com.muwire.core.connection.DisconnectionEvent import com.muwire.core.connection.DisconnectionEvent
@@ -20,6 +22,7 @@ import com.muwire.core.search.QueryEvent
import com.muwire.core.search.UIResultEvent import com.muwire.core.search.UIResultEvent
import com.muwire.core.trust.TrustEvent import com.muwire.core.trust.TrustEvent
import com.muwire.core.trust.TrustService import com.muwire.core.trust.TrustService
import com.muwire.core.update.UpdateAvailableEvent
import com.muwire.core.upload.UploadEvent import com.muwire.core.upload.UploadEvent
import com.muwire.core.upload.UploadFinishedEvent import com.muwire.core.upload.UploadFinishedEvent
@@ -58,6 +61,8 @@ class MainFrameModel {
volatile Core core volatile Core core
private long lastRetryTime = System.currentTimeMillis()
void updateTablePreservingSelection(String tableName) { void updateTablePreservingSelection(String tableName) {
def downloadTable = builder.getVariable(tableName) def downloadTable = builder.getVariable(tableName)
int selectedRow = downloadTable.getSelectedRow() int selectedRow = downloadTable.getSelectedRow()
@@ -94,19 +99,26 @@ class MainFrameModel {
core.eventBus.register(UploadFinishedEvent.class, this) core.eventBus.register(UploadFinishedEvent.class, this)
core.eventBus.register(TrustEvent.class, this) core.eventBus.register(TrustEvent.class, this)
core.eventBus.register(QueryEvent.class, this) core.eventBus.register(QueryEvent.class, this)
core.eventBus.register(UpdateAvailableEvent.class, this)
timer.schedule({
int retryInterval = application.context.get("muwire-settings").downloadRetryInterval int retryInterval = application.context.get("muwire-settings").downloadRetryInterval
if (retryInterval > 0) { if (retryInterval > 0) {
retryInterval *= 60000 retryInterval *= 60000
timer.schedule({ long now = System.currentTimeMillis()
if (now - lastRetryTime > retryInterval) {
lastRetryTime = now
runInsideUIAsync { runInsideUIAsync {
downloads.each { downloads.each {
if (it.downloader.currentState == Downloader.DownloadState.FAILED) if (it.downloader.currentState == Downloader.DownloadState.FAILED)
it.downloader.resume() it.downloader.resume()
updateTablePreservingSelection("downloads-table")
} }
} }
}, retryInterval, retryInterval)
} }
}
}, 60000, 60000)
runInsideUIAsync { runInsideUIAsync {
trusted.addAll(core.trustService.good.values()) trusted.addAll(core.trustService.good.values())
@@ -228,11 +240,23 @@ class MainFrameModel {
if (search.trim().size() == 0) if (search.trim().size() == 0)
return return
runInsideUIAsync { runInsideUIAsync {
searches.addFirst(search) searches.addFirst(new IncomingSearch(search : search, replyTo : e.replyTo, originator : e.originator))
while(searches.size() > 200) while(searches.size() > 200)
searches.removeLast() searches.removeLast()
JTable table = builder.getVariable("searches-table") JTable table = builder.getVariable("searches-table")
table.model.fireTableDataChanged() table.model.fireTableDataChanged()
} }
} }
class IncomingSearch {
String search
Destination replyTo
Persona originator
}
void onUpdateAvailableEvent(UpdateAvailableEvent e) {
runInsideUIAsync {
JOptionPane.showMessageDialog(null, "A new version of MuWire is available from $e.signer. Please update to $e.version")
}
}
} }

View File

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

View File

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

View File

@@ -44,6 +44,11 @@ class MainFrameView {
imageIcon('/griffon-icon-16x16.png').image], imageIcon('/griffon-icon-16x16.png').image],
pack : false, pack : false,
visible : bind { model.coreInitialized }) { visible : bind { model.coreInitialized }) {
menuBar {
menu (text : "Options") {
menuItem("Configuration", actionPerformed : {mvcGroup.createMVCGroup("Options")})
}
}
borderLayout() borderLayout()
panel (border: etchedBorder(), constraints : BorderLayout.NORTH) { panel (border: etchedBorder(), constraints : BorderLayout.NORTH) {
borderLayout() borderLayout()
@@ -99,11 +104,8 @@ class MainFrameView {
int done = row.downloader.donePieces() int done = row.downloader.donePieces()
"$done/$pieces pieces" "$done/$pieces pieces"
}) })
closureColumn(header: "Piece", type: String, read: { row -> closureColumn(header: "Sources", type: Integer, read : {row -> row.downloader.activeWorkers.size()})
int position = row.downloader.positionInPiece() closureColumn(header: "Speed (bytes/second)", type:Integer, read :{row -> row.downloader.speed()})
int pieceSize = row.downloader.pieceSize // TODO: fix for last piece
"$position/$pieceSize bytes"
})
} }
} }
} }
@@ -177,7 +179,14 @@ class MainFrameView {
scrollPane(constraints : BorderLayout.CENTER) { scrollPane(constraints : BorderLayout.CENTER) {
table(id : "searches-table") { table(id : "searches-table") {
tableModel(list : model.searches) { 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()
}
})
} }
} }
} }

View File

@@ -0,0 +1,59 @@
package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.swing.JDialog
import javax.swing.SwingConstants
import java.awt.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 updateField
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))
label(text : "Check for updates every", constraints : gbc(gridx : 0, gridy: 1))
updateField = textField(text : bind {model.updateCheckInterval }, columns : 2, constraints : gbc(gridx : 1, gridy: 1))
label(text : "hours", constraints : gbc(gridx: 2, gridy : 1))
button(text : "Save", constraints : gbc(gridx : 1, gridy: 2), saveAction)
button(text : "Cancel", constraints : gbc(gridx : 2, gridy: 2), 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) { tableModel(list: model.results) {
closureColumn(header: "Name", type: String, read : {row -> row.name}) closureColumn(header: "Name", type: String, read : {row -> row.name})
closureColumn(header: "Size", preferredWidth: 150, type: Long, read : {row -> row.size}) closureColumn(header: "Size", preferredWidth: 150, type: Long, read : {row -> row.size})
closureColumn(header: "Sources", type : Integer, read : { row -> model.hashBucket[row.infohash].size()})
closureColumn(header: "Sender", type: String, read : {row -> row.sender.getHumanReadableName()}) closureColumn(header: "Sender", type: String, read : {row -> row.sender.getHumanReadableName()})
closureColumn(header: "Trust", type: String, read : {row -> closureColumn(header: "Trust", type: String, read : {row ->
model.core.trustService.getLevel(row.sender.destination) model.core.trustService.getLevel(row.sender.destination)

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!')
}
}

View File

@@ -1,4 +1,5 @@
include 'pinger' include 'pinger'
include 'host-cache' include 'host-cache'
include 'update-server'
include 'core' include 'core'
include 'gui' include 'gui'

View File

@@ -0,0 +1,2 @@
apply plugin : 'application'
mainClassName = 'com.muwire.update.UpdateServer'

View File

@@ -0,0 +1,105 @@
package com.muwire.update
import java.util.logging.Level
import groovy.util.logging.Log
import net.i2p.client.I2PClientFactory
import net.i2p.client.I2PSession
import net.i2p.client.I2PSessionMuxedListener
import net.i2p.client.datagram.I2PDatagramDissector
import net.i2p.client.datagram.I2PDatagramMaker
@Log
class UpdateServer {
public static void main(String[] args) {
def home = System.getProperty("user.home") + "/.MuWireUpdateServer"
home = new File(home)
if (!home.exists())
home.mkdirs()
def keyFile = new File(home, "key.dat")
def i2pClientFactory = new I2PClientFactory()
def i2pClient = i2pClientFactory.createClient()
def myDest
def session
if (!keyFile.exists()) {
def os = new FileOutputStream(keyFile);
myDest = i2pClient.createDestination(os)
os.close()
log.info "No key.dat file was found, so creating a new destination."
log.info "This is the destination you want to give out for your new UpdateServer"
log.info myDest.toBase64()
}
def update = new File(home, "update.json")
if (!update.exists()) {
log.warning("update file doesn't exist, exiting")
System.exit(1)
}
def props = System.getProperties().clone()
props.putAt("inbound.nickname", "MuWire UpdateServer")
session = i2pClient.createSession(new FileInputStream(keyFile), props)
myDest = session.getMyDestination()
session.addMuxedSessionListener(new Listener(update), I2PSession.PROTO_DATAGRAM, I2PSession.PORT_ANY)
session.connect()
log.info("Connected, going to sleep")
Thread.sleep(Integer.MAX_VALUE)
}
static class Listener implements I2PSessionMuxedListener {
private final File json
Listener(File json) {
this.json = json
}
@Override
public void messageAvailable(I2PSession session, int msgId, long size) {
}
@Override
public void messageAvailable(I2PSession session, int msgId, long size, int proto, int fromport, int toport) {
if (proto != I2PSession.PROTO_DATAGRAM) {
log.warning("received uknown protocol $proto")
return
}
def payload = session.receiveMessage(msgId)
def dissector = new I2PDatagramDissector()
try {
dissector.loadI2PDatagram(payload)
def sender = dissector.getSender()
log.info("Got an update ping from "+sender.toBase32())
// I don't think we care about the payload at this point
def maker = new I2PDatagramMaker(session)
def response = maker.makeI2PDatagram(json.bytes)
session.sendMessage(sender, response, I2PSession.PROTO_DATAGRAM, 0, 2)
} catch (Exception e) {
log.log(Level.WARNING, "exception responding to update request",e)
}
}
@Override
public void reportAbuse(I2PSession session, int severity) {
}
@Override
public void disconnected(I2PSession session) {
Log.severe("Disconnected from I2P router")
System.exit(1)
}
@Override
public void errorOccurred(I2PSession session, String message, Throwable error) {
log.log(Level.SEVERE, message, error)
}
}
}