Compare commits

...

132 Commits

Author SHA1 Message Date
Zlatin Balevsky
a5d442d320 Release 0.0.13 for keyword search fix 2019-06-07 06:37:23 +01:00
Zlatin Balevsky
3f9ee887d6 prevent NPE in toString 2019-06-07 06:31:29 +01:00
Zlatin Balevsky
4a9e6d3b6b prevent npe in keyword searches 2019-06-07 06:14:40 +01:00
Zlatin Balevsky
80f2cc5f99 logging and toString() 2019-06-07 06:07:02 +01:00
Zlatin Balevsky
12283dba9d Release 0.0.12 for search by hash 2019-06-06 22:22:43 +01:00
Zlatin Balevsky
5c959bc8b7 name update search tab 2019-06-06 22:07:20 +01:00
Zlatin Balevsky
f3712fe7af delay initial update check a minute 2019-06-06 21:52:35 +01:00
Zlatin Balevsky
3e49b0ec66 infohash may be null 2019-06-06 21:40:44 +01:00
Zlatin Balevsky
f90beb8e3d encode infohash 2019-06-06 21:31:00 +01:00
Zlatin Balevsky
fbad7b6c7e searchHash 2019-06-06 21:27:07 +01:00
Zlatin Balevsky
ec2d89c18c serialize infohash 2019-06-06 21:21:40 +01:00
Zlatin Balevsky
c27fc0a515 update from infohash 2019-06-06 21:08:58 +01:00
Zlatin Balevsky
14681c2060 search by hash ui 2019-06-06 20:30:15 +01:00
Zlatin Balevsky
1aeb230ea8 catch exceptions in event dispatch thread 2019-06-06 19:31:10 +01:00
Zlatin Balevsky
d1dfc73f5a decode infohash 2019-06-06 19:28:29 +01:00
Zlatin Balevsky
0cebe4119c update list of limitations 2019-06-06 14:19:43 +01:00
Zlatin Balevsky
9f21120ec8 print periodic stats 2019-06-06 13:59:05 +01:00
Zlatin Balevsky
7eea8be67d Release 0.0.11 for file loading bug 2019-06-06 09:22:16 +01:00
Zlatin Balevsky
f114302bdb hopefully fix the shared file loss 2019-06-06 09:19:00 +01:00
Zlatin Balevsky
05b9b37488 emit an event when all files are loaded 2019-06-06 09:10:09 +01:00
Zlatin Balevsky
52f317a5b7 prevent division by zero 2019-06-06 07:09:54 +01:00
Zlatin Balevsky
fb8227a1f3 prevent division by zero 2019-06-06 07:09:05 +01:00
Zlatin Balevsky
5677d9f46a release 0.0.10 2019-06-06 00:23:59 +01:00
Zlatin Balevsky
c5192e3845 update readme for fix 2019-06-06 00:21:41 +01:00
Zlatin Balevsky
43c2a55cb8 0 not null 2019-06-06 00:03:22 +01:00
Zlatin Balevsky
94f6de6bea do not create new objects because that clears the successes 2019-06-05 21:07:23 +01:00
Zlatin Balevsky
6782849a12 retry hosts received from hostcache even if marked as failed 2019-06-05 20:58:28 +01:00
Zlatin Balevsky
c07d351c5d switch to jul, reduce aging interval 2019-06-05 20:14:38 +01:00
Zlatin Balevsky
dc2f675dd3 delete pieces file when download finishes 2019-06-05 19:52:50 +01:00
Zlatin Balevsky
a8e795ec51 do not accept connections if already try to connect to them 2019-06-05 19:07:36 +01:00
Zlatin Balevsky
33c5b3b18e option to disable sharing of downloaded files 2019-06-05 17:46:55 +01:00
Zlatin Balevsky
581fce4643 share downloaded files 2019-06-05 17:33:34 +01:00
Zlatin Balevsky
7fe78a0719 more clear name 2019-06-05 16:47:10 +01:00
Zlatin Balevsky
cdb6e22522 ui option for allowing untrusted connections 2019-06-05 15:47:44 +01:00
Zlatin Balevsky
2edeb046be drop neutral queries if configured 2019-06-05 15:38:39 +01:00
Zlatin Balevsky
4021f3c244 fix jullog 2019-06-05 13:04:46 +01:00
Zlatin Balevsky
9008fac24d shutdown cleanly on exit 2019-06-05 12:38:56 +01:00
Zlatin Balevsky
e2f92c5c5e print reported version 2019-06-05 10:07:04 +01:00
Zlatin Balevsky
7b33a16fd8 update list of known issues 2019-06-05 09:22:56 +01:00
Zlatin Balevsky
9a2531b264 release 0.0.9 2019-06-05 09:04:52 +01:00
Zlatin Balevsky
9a8dadff57 center the sources column 2019-06-05 08:43:58 +01:00
Zlatin Balevsky
4a274010f9 fix close tab button not appearing on duplicate searches 2019-06-05 08:34:09 +01:00
Zlatin Balevsky
1eb930435b fix hashing errors in large files 2019-06-05 00:34:38 +01:00
Zlatin Balevsky
9df28552ad try to load persisted files before hashing new ones 2019-06-05 00:22:36 +01:00
Zlatin Balevsky
ac0204dffc hopefully more accurate bandwidth gauge 2019-06-04 23:50:36 +01:00
Zlatin Balevsky
e5c402a400 retry download workers on resume 2019-06-04 23:36:57 +01:00
Zlatin Balevsky
7704c73b68 pass logging.properties to cli 2019-06-04 22:19:19 +01:00
Zlatin Balevsky
a9aa8dd840 do not count finished downloaders towards bandwidth 2019-06-04 21:55:59 +01:00
Zlatin Balevsky
de682a802a options panel for i2p tunnel options 2019-06-04 21:14:23 +01:00
Zlatin Balevsky
5435518212 core-side i2cp options 2019-06-04 20:20:25 +01:00
Zlatin Balevsky
bd01f983c9 break html in search results 2019-06-04 19:27:22 +01:00
Zlatin Balevsky
8b63864b90 utility to share files in headless mode 2019-06-04 18:58:02 +01:00
Zlatin Balevsky
ed3943c1af 0.0.8 for UI tweaks and sanitization 2019-06-04 18:01:08 +01:00
Zlatin Balevsky
e195141a27 simpler sanitization 2019-06-04 17:58:19 +01:00
Zlatin Balevsky
bb02fdbee9 do not use regex in sanitization 2019-06-04 17:46:41 +01:00
Zlatin Balevsky
6e3a2c0d08 update split pattern 2019-06-04 17:30:55 +01:00
Zlatin Balevsky
bd5fecc19d fix 2019-06-04 17:04:24 +01:00
Zlatin Balevsky
d5db49fa79 initialize core 2019-06-04 16:56:58 +01:00
Zlatin Balevsky
f2ea8619bb CLI project 2019-06-04 16:46:32 +01:00
Zlatin Balevsky
b129e79196 do not count finished workers in total count 2019-06-04 16:22:48 +01:00
Zlatin Balevsky
404d5b60bc format length in shared file stable an resize columns 2019-06-04 14:05:33 +01:00
Zlatin Balevsky
de2753ac50 preferred sizes for download table columns 2019-06-04 13:35:18 +01:00
Zlatin Balevsky
2d53999c8e only show download speed if downloading 2019-06-04 13:23:48 +01:00
Zlatin Balevsky
5aecf72d6f format download speed 2019-06-04 13:19:14 +01:00
Zlatin Balevsky
a574a67ec6 format file size 2019-06-04 13:15:24 +01:00
Zlatin Balevsky
6b5ad969b7 pass logging properties 2019-06-04 13:00:10 +01:00
Zlatin Balevsky
617209c4e4 column widths tweaks 2019-06-04 12:46:48 +01:00
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
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
Zlatin Balevsky
656b62fc2e 0.0.4 with download retry 2019-06-01 18:31:36 +01:00
Zlatin Balevsky
13b3f0f63b retry implemented 2019-06-01 18:30:30 +01:00
Zlatin Balevsky
98ea8154a5 store done pieces on disk to enable resume 2019-06-01 18:09:14 +01:00
Zlatin Balevsky
82377aa9df hook up cancel button 2019-06-01 17:44:52 +01:00
Zlatin Balevsky
bd2368e23a cancelled downloader state 2019-06-01 17:31:18 +01:00
Zlatin Balevsky
70078c309b add cancel and retry buttons, not hooked up yet 2019-06-01 17:30:29 +01:00
Zlatin Balevsky
15a0eda713 preserve selection in downloads table 2019-06-01 17:09:23 +01:00
Zlatin Balevsky
9645716e18 prevent rare stacktraces on shutdown 2019-06-01 16:55:37 +01:00
Zlatin Balevsky
03d6af39ed icon for closing tabs 2019-06-01 16:43:05 +01:00
Zlatin Balevsky
9435cb003b Show warning if cannot find I2P router 2019-06-01 16:36:23 +01:00
Zlatin Balevsky
63399803d5 ui tweaks 2019-06-01 15:59:55 +01:00
Zlatin Balevsky
4d6541030f disable system l&f on osx 2019-06-01 14:55:17 +01:00
Zlatin Balevsky
16c51e7cd6 add a failed download state 2019-06-01 14:14:20 +01:00
Zlatin Balevsky
9d75550b6f do not show local searches in monitor 2019-06-01 13:48:12 +01:00
Zlatin Balevsky
1996681677 incoming searches monitor 2019-06-01 13:44:46 +01:00
Zlatin Balevsky
9dac1891b2 connection monitor 2019-06-01 13:32:40 +01:00
Zlatin Balevsky
1255ac936b close connections on shutdown 2019-06-01 13:04:22 +01:00
Zlatin Balevsky
2db3276b07 fix rare NPE on shutdown 2019-06-01 13:03:42 +01:00
Zlatin Balevsky
7e3b0795af disable buttons if no row is selected 2019-06-01 12:23:20 +01:00
Zlatin Balevsky
4bb27b84de release 0.0.3 for fixed first hop and tabbed search ui 2019-06-01 11:11:22 +01:00
Zlatin Balevsky
d5b8c0c694 fix parsing of first hop 2019-06-01 11:03:27 +01:00
Zlatin Balevsky
1e314b3cca serialize firstHop 2019-06-01 10:44:24 +01:00
Zlatin Balevsky
6f02e3e9c0 hook up buttons to tabbed results 2019-06-01 10:04:10 +01:00
Zlatin Balevsky
9f5f21376a one tab per search 2019-06-01 09:29:03 +01:00
Zlatin Balevsky
d2231b8e38 attach uuid to search results 2019-06-01 07:20:39 +01:00
75 changed files with 2087 additions and 242 deletions

View File

@@ -32,8 +32,6 @@ At the moment there are very few nodes on the network, so you will see very few
### Known bugs and limitations ### Known bugs and limitations
* Any shared files get re-hashed on startup
* Sometimes the list of shared files gets lost
* Many UI features you would expect are not there yet * Many UI features you would expect are not there yet

View File

@@ -3,7 +3,7 @@ subprojects {
dependencies { dependencies {
compile 'net.i2p:i2p:0.9.40' 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 { compileGroovy {

8
cli/build.gradle Normal file
View File

@@ -0,0 +1,8 @@
apply plugin : 'application'
mainClassName = 'com.muwire.cli.Cli'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
dependencies {
compile project(":core")
}

View File

@@ -0,0 +1,142 @@
package com.muwire.cli
import java.util.concurrent.CountDownLatch
import com.muwire.core.Core
import com.muwire.core.MuWireSettings
import com.muwire.core.connection.ConnectionAttemptStatus
import com.muwire.core.connection.ConnectionEvent
import com.muwire.core.connection.DisconnectionEvent
import com.muwire.core.files.AllFilesLoadedEvent
import com.muwire.core.files.FileHashedEvent
import com.muwire.core.files.FileLoadedEvent
import com.muwire.core.files.FileSharedEvent
import com.muwire.core.upload.UploadEvent
import com.muwire.core.upload.UploadFinishedEvent
class Cli {
public static void main(String[] args) {
def home = System.getProperty("user.home") + File.separator + ".MuWire"
home = new File(home)
if (!home.exists())
home.mkdirs()
def propsFile = new File(home,"MuWire.properties")
if (!propsFile.exists()) {
println "create props file ${propsFile.getAbsoluteFile()} before launching MuWire"
System.exit(1)
}
def props = new Properties()
propsFile.withInputStream { props.load(it) }
props = new MuWireSettings(props)
Core core
try {
core = new Core(props, home, "0.0.13")
} catch (Exception bad) {
bad.printStackTrace(System.out)
println "Failed to initialize core, exiting"
System.exit(1)
}
def filesList
if (args.length == 0) {
println "Enter a file containing list of files to share"
def reader = new BufferedReader(new InputStreamReader(System.in))
filesList = reader.readLine()
} else
filesList = args[0]
Thread.sleep(1000)
println "loading shared files from $filesList"
// listener for shared files
def sharedListener = new SharedListener()
core.eventBus.register(FileHashedEvent.class, sharedListener)
core.eventBus.register(FileLoadedEvent.class, sharedListener)
// for connections
def connectionsListener = new ConnectionListener()
core.eventBus.register(ConnectionEvent.class, connectionsListener)
core.eventBus.register(DisconnectionEvent.class, connectionsListener)
// for uploads
def uploadsListener = new UploadsListener()
core.eventBus.register(UploadEvent.class, uploadsListener)
core.eventBus.register(UploadFinishedEvent.class, uploadsListener)
Timer timer = new Timer("status-printer", true)
timer.schedule({
println "Connections $connectionsListener.connections Uploads $uploadsListener.uploads Shared $sharedListener.shared"
} as TimerTask, 60000, 60000)
def latch = new CountDownLatch(1)
def fileLoader = new Object() {
public void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
latch.countDown()
}
}
core.eventBus.register(AllFilesLoadedEvent.class, fileLoader)
core.startServices()
println "waiting for files to load"
latch.await()
// now we begin
println "MuWire is ready"
filesList = new File(filesList)
filesList.withReader {
def toShare = it.readLine()
core.eventBus.publish(new FileSharedEvent(file : new File(toShare)))
}
Runtime.getRuntime().addShutdownHook({
println "shutting down.."
core.shutdown()
println "shutdown."
})
Thread.sleep(Integer.MAX_VALUE)
}
static class ConnectionListener {
volatile int connections
public void onConnectionEvent(ConnectionEvent e) {
if (e.status == ConnectionAttemptStatus.SUCCESSFUL)
connections++
}
public void onDisconnectionEvent(DisconnectionEvent e) {
connections--
}
}
static class UploadsListener {
volatile int uploads
public void onUploadEvent(UploadEvent e) {
uploads++
println "Starting upload of ${e.uploader.file.getName()} to ${e.uploader.request.downloader.getHumanReadableName()}"
}
public void onUploadFinishedEvent(UploadFinishedEvent e) {
uploads--
println "Finished upload of ${e.uploader.file.getName()} to ${e.uploader.request.downloader.getHumanReadableName()}"
}
}
static class SharedListener {
volatile int shared
void onFileHashedEvent(FileHashedEvent e) {
if (e.error != null)
println "ERROR $e.error"
else {
println "Shared file : $e.sharedFile.file"
shared++
}
}
void onFileLoadedEvent(FileLoadedEvent e) {
shared++
}
}
}

View File

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

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
@@ -54,17 +55,23 @@ public class Core {
final EventBus eventBus final EventBus eventBus
final Persona me final Persona me
final File home
final Properties i2pOptions
final MuWireSettings muOptions
private final TrustService trustService private final TrustService trustService
private final PersisterService persisterService private final PersisterService persisterService
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.muOptions = props
log.info "Initializing I2P context" log.info "Initializing I2P context"
I2PAppContext.getGlobalContext().logManager() I2PAppContext.getGlobalContext().logManager()
I2PAppContext.getGlobalContext()._logManager = new MuWireLogManager() I2PAppContext.getGlobalContext()._logManager = new MuWireLogManager()
@@ -79,12 +86,23 @@ public class Core {
} }
} }
def sysProps = System.getProperties().clone() i2pOptions = new Properties()
sysProps["inbound.nickname"] = "MuWire" def i2pOptionsFile = new File(home,"i2p.properties")
if (i2pOptionsFile.exists()) {
i2pOptionsFile.withInputStream { i2pOptions.load(it) }
} else {
i2pOptions["inbound.nickname"] = "MuWire"
i2pOptions["inbound.length"] = "3"
i2pOptions["inbound.quantity"] = "2"
i2pOptions["outbound.length"] = "3"
i2pOptions["outbound.quantity"] = "2"
}
// options like tunnel length and quantity
I2PSession i2pSession I2PSession i2pSession
I2PSocketManager socketManager I2PSocketManager socketManager
keyDat.withInputStream { keyDat.withInputStream {
socketManager = new I2PSocketManagerFactory().createManager(it, sysProps) socketManager = new I2PSocketManagerFactory().createManager(it, i2pOptions)
} }
socketManager.getDefaultOptions().setReadTimeout(60000) socketManager.getDefaultOptions().setReadTimeout(60000)
socketManager.getDefaultOptions().setConnectTimeout(30000) socketManager.getDefaultOptions().setConnectTimeout(30000)
@@ -118,14 +136,14 @@ public class Core {
eventBus = new EventBus() eventBus = new EventBus()
log.info("initializing trust service") log.info("initializing trust service")
File goodTrust = new File(home, "trust.good") File goodTrust = new File(home, "trusted")
File badTrust = new File(home, "trust.bad") File badTrust = new File(home, "distrusted")
trustService = new TrustService(goodTrust, badTrust, 5000) trustService = new TrustService(goodTrust, badTrust, 5000)
eventBus.register(TrustEvent.class, trustService) eventBus.register(TrustEvent.class, trustService)
log.info "initializing file manager" log.info "initializing file manager"
FileManager fileManager = new FileManager(eventBus) FileManager fileManager = new FileManager(eventBus, props)
eventBus.register(FileHashedEvent.class, fileManager) eventBus.register(FileHashedEvent.class, fileManager)
eventBus.register(FileLoadedEvent.class, fileManager) eventBus.register(FileLoadedEvent.class, fileManager)
eventBus.register(FileDownloadedEvent.class, fileManager) eventBus.register(FileDownloadedEvent.class, fileManager)
@@ -143,7 +161,8 @@ public class Core {
log.info("initializing connection manager") log.info("initializing connection manager")
connectionManager = props.isLeaf() ? connectionManager = props.isLeaf() ?
new LeafConnectionManager(eventBus, me, 3, hostCache) : new UltrapeerConnectionManager(eventBus, me, 512, 512, hostCache, trustService) new LeafConnectionManager(eventBus, me, 3, hostCache, props) :
new UltrapeerConnectionManager(eventBus, me, 512, 512, hostCache, trustService, props)
eventBus.register(TrustEvent.class, connectionManager) eventBus.register(TrustEvent.class, connectionManager)
eventBus.register(ConnectionEvent.class, connectionManager) eventBus.register(ConnectionEvent.class, connectionManager)
eventBus.register(DisconnectionEvent.class, connectionManager) eventBus.register(DisconnectionEvent.class, connectionManager)
@@ -152,6 +171,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)
@@ -164,23 +186,23 @@ public class Core {
eventBus.register(ResultsEvent.class, searchManager) eventBus.register(ResultsEvent.class, searchManager)
log.info("initializing download manager") log.info("initializing download manager")
DownloadManager downloadManager = new DownloadManager(eventBus, i2pConnector) DownloadManager downloadManager = new DownloadManager(eventBus, i2pConnector, new File(home, "incompletes"), me)
eventBus.register(UIDownloadEvent.class, downloadManager) eventBus.register(UIDownloadEvent.class, downloadManager)
log.info("initializing upload manager") log.info("initializing upload manager")
UploadManager uploadManager = new UploadManager(eventBus, fileManager) UploadManager uploadManager = new UploadManager(eventBus, fileManager)
log.info("initializing connection establisher")
connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache)
log.info("initializing acceptor") log.info("initializing acceptor")
I2PAcceptor i2pAcceptor = new I2PAcceptor(socketManager) I2PAcceptor i2pAcceptor = new I2PAcceptor(socketManager)
connectionAcceptor = new ConnectionAcceptor(eventBus, connectionManager, props, connectionAcceptor = new ConnectionAcceptor(eventBus, connectionManager, props,
i2pAcceptor, hostCache, trustService, searchManager, uploadManager) i2pAcceptor, hostCache, trustService, searchManager, uploadManager, connectionEstablisher)
connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache)
log.info("initializing hasher service") log.info("initializing hasher service")
hasherService = new HasherService(new FileHasher(), eventBus) hasherService = new HasherService(new FileHasher(), eventBus, fileManager)
eventBus.register(FileSharedEvent.class, hasherService) eventBus.register(FileSharedEvent.class, hasherService)
} }
@@ -195,6 +217,11 @@ public class Core {
connectionAcceptor.start() connectionAcceptor.start()
connectionEstablisher.start() connectionEstablisher.start()
hostCache.waitForLoad() hostCache.waitForLoad()
updateClient.start()
}
public void shutdown() {
connectionManager.shutdown()
} }
static main(args) { static main(args) {
@@ -221,7 +248,7 @@ public class Core {
} }
} }
Core core = new Core(props, home) Core core = new Core(props, home, "0.0.13")
core.startServices() core.startServices()
// ... at the end, sleep or execute script // ... at the end, sleep or execute script

View File

@@ -3,6 +3,7 @@ package com.muwire.core
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.Executor import java.util.concurrent.Executor
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.logging.Level
import com.muwire.core.files.FileSharedEvent import com.muwire.core.files.FileSharedEvent
@@ -23,14 +24,18 @@ class EventBus {
} }
private void publishInternal(Event e) { private void publishInternal(Event e) {
log.fine "publishing event $e of type ${e.getClass().getSimpleName()}" log.fine "publishing event $e of type ${e.getClass().getSimpleName()} event $e"
def currentHandlers def currentHandlers
final def clazz = e.getClass() final def clazz = e.getClass()
synchronized(this) { synchronized(this) {
currentHandlers = handlers.getOrDefault(clazz, []) currentHandlers = handlers.getOrDefault(clazz, [])
} }
currentHandlers.each { currentHandlers.each {
it."on${clazz.getSimpleName()}"(e) try {
it."on${clazz.getSimpleName()}"(e)
} catch (Exception bad) {
log.log(Level.SEVERE, "exception dispatching event",bad)
}
} }
} }

View File

@@ -6,10 +6,13 @@ class MuWireSettings {
final boolean isLeaf final boolean isLeaf
boolean allowUntrusted boolean allowUntrusted
int downloadRetryInterval
int updateCheckInterval
String nickname String nickname
File downloadLocation File downloadLocation
String sharedFiles String sharedFiles
CrawlerResponse crawlerResponse CrawlerResponse crawlerResponse
boolean shareDownloadedFiles
MuWireSettings() { MuWireSettings() {
this(new Properties()) this(new Properties())
@@ -23,6 +26,9 @@ class MuWireSettings {
downloadLocation = new File((String)props.getProperty("downloadLocation", downloadLocation = new File((String)props.getProperty("downloadLocation",
System.getProperty("user.home"))) System.getProperty("user.home")))
sharedFiles = props.getProperty("sharedFiles") sharedFiles = props.getProperty("sharedFiles")
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","15"))
updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","36"))
shareDownloadedFiles = Boolean.parseBoolean(props.getProperty("shareDownloadedFiles","true"))
} }
void write(OutputStream out) throws IOException { void write(OutputStream out) throws IOException {
@@ -32,6 +38,9 @@ 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", String.valueOf(downloadRetryInterval))
props.setProperty("updateCheckInterval", String.valueOf(updateCheckInterval))
props.setProperty("shareDownloadedFiles", String.valueOf(shareDownloadedFiles))
if (sharedFiles != null) if (sharedFiles != null)
props.setProperty("sharedFiles", sharedFiles) props.setProperty("sharedFiles", sharedFiles)
props.store(out, "") props.store(out, "")

View File

@@ -2,6 +2,7 @@ package com.muwire.core
import net.i2p.crypto.DSAEngine import net.i2p.crypto.DSAEngine
import net.i2p.crypto.SigType import net.i2p.crypto.SigType
import net.i2p.data.Base64
import net.i2p.data.Destination import net.i2p.data.Destination
import net.i2p.data.Signature import net.i2p.data.Signature
import net.i2p.data.SigningPublicKey import net.i2p.data.SigningPublicKey
@@ -14,6 +15,7 @@ public class Persona {
private final Destination destination private final Destination destination
private final byte[] sig private final byte[] sig
private volatile String humanReadableName private volatile String humanReadableName
private volatile String base64
private volatile byte[] payload private volatile byte[] payload
public Persona(InputStream personaStream) throws IOException, InvalidSignatureException { public Persona(InputStream personaStream) throws IOException, InvalidSignatureException {
@@ -59,6 +61,15 @@ public class Persona {
humanReadableName humanReadableName
} }
public String toBase64() {
if (base64 == null) {
def baos = new ByteArrayOutputStream()
write(baos)
base64 = Base64.encode(baos.toByteArray())
}
base64
}
@Override @Override
public int hashCode() { public int hashCode() {
name.hashCode() ^ destination.hashCode() name.hashCode() ^ destination.hashCode()

View File

@@ -6,6 +6,8 @@ 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.MuWireSettings
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 +16,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
@@ -24,7 +27,8 @@ abstract class Connection implements Closeable {
final boolean incoming final boolean incoming
final HostCache hostCache final HostCache hostCache
final TrustService trustService final TrustService trustService
final MuWireSettings settings
private final AtomicBoolean running = new AtomicBoolean() private final AtomicBoolean running = new AtomicBoolean()
private final BlockingQueue messages = new LinkedBlockingQueue() private final BlockingQueue messages = new LinkedBlockingQueue()
private final Thread reader, writer private final Thread reader, writer
@@ -33,12 +37,14 @@ abstract class Connection implements Closeable {
long lastPingSentTime, lastPongReceivedTime long lastPingSentTime, lastPongReceivedTime
Connection(EventBus eventBus, Endpoint endpoint, boolean incoming, HostCache hostCache, TrustService trustService) { Connection(EventBus eventBus, Endpoint endpoint, boolean incoming,
HostCache hostCache, TrustService trustService, MuWireSettings settings) {
this.eventBus = eventBus this.eventBus = eventBus
this.incoming = incoming this.incoming = incoming
this.endpoint = endpoint this.endpoint = endpoint
this.hostCache = hostCache this.hostCache = hostCache
this.trustService = trustService this.trustService = trustService
this.settings = settings
this.name = endpoint.destination.toBase32().substring(0,8) this.name = endpoint.destination.toBase32().substring(0,8)
@@ -82,7 +88,6 @@ abstract class Connection implements Closeable {
read() read()
} }
} catch (SocketTimeoutException e) { } catch (SocketTimeoutException e) {
close()
} catch (Exception e) { } catch (Exception e) {
log.log(Level.WARNING,"unhandled exception in reader",e) log.log(Level.WARNING,"unhandled exception in reader",e)
} finally { } finally {
@@ -120,9 +125,13 @@ abstract class Connection implements Closeable {
query.type = "Search" query.type = "Search"
query.version = 1 query.version = 1
query.uuid = e.searchEvent.getUuid() query.uuid = e.searchEvent.getUuid()
// TODO: first hop figure out query.firstHop = e.firstHop
query.keywords = e.searchEvent.getSearchTerms() query.keywords = e.searchEvent.getSearchTerms()
query.replyTo = e.getReceivedOn().toBase64() if (e.searchEvent.searchHash != null)
query.infohash = Base64.encode(e.searchEvent.searchHash)
query.replyTo = e.replyTo.toBase64()
if (e.originator != null)
query.originator = e.originator.toBase64()
messages.put(query) messages.put(query)
} }
@@ -148,23 +157,41 @@ abstract class Connection implements Closeable {
protected void handleSearch(def search) { protected void handleSearch(def search) {
UUID uuid = UUID.fromString(search.uuid) UUID uuid = UUID.fromString(search.uuid)
if (search.infohash != null) byte [] infohash = null
if (search.infohash != null) {
search.keywords = null search.keywords = null
infohash = Base64.decode(search.infohash)
}
Destination replyTo = new Destination(search.replyTo) Destination replyTo = new Destination(search.replyTo)
if (trustService.getLevel(replyTo) == TrustLevel.DISTRUSTED) { TrustLevel trustLevel = trustService.getLevel(replyTo)
if (trustLevel == TrustLevel.DISTRUSTED) {
log.info "dropping search from distrusted peer" log.info "dropping search from distrusted peer"
return return
} }
// TODO: add option to respond only to trusted peers if (trustLevel == TrustLevel.NEUTRAL && !settings.allowUntrusted()) {
log.info("dropping search from neutral peer")
return
}
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 : 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 : Boolean.parseBoolean(search.firstHop) ) firstHop : search.firstHop )
eventBus.publish(event) eventBus.publish(event)
} }

View File

@@ -34,13 +34,15 @@ class ConnectionAcceptor {
final TrustService trustService final TrustService trustService
final SearchManager searchManager final SearchManager searchManager
final UploadManager uploadManager final UploadManager uploadManager
final ConnectionEstablisher establisher
final ExecutorService acceptorThread final ExecutorService acceptorThread
final ExecutorService handshakerThreads final ExecutorService handshakerThreads
ConnectionAcceptor(EventBus eventBus, UltrapeerConnectionManager manager, ConnectionAcceptor(EventBus eventBus, UltrapeerConnectionManager manager,
MuWireSettings settings, I2PAcceptor acceptor, HostCache hostCache, MuWireSettings settings, I2PAcceptor acceptor, HostCache hostCache,
TrustService trustService, SearchManager searchManager, UploadManager uploadManager) { TrustService trustService, SearchManager searchManager, UploadManager uploadManager,
ConnectionEstablisher establisher) {
this.eventBus = eventBus this.eventBus = eventBus
this.manager = manager this.manager = manager
this.settings = settings this.settings = settings
@@ -49,7 +51,8 @@ class ConnectionAcceptor {
this.trustService = trustService this.trustService = trustService
this.searchManager = searchManager this.searchManager = searchManager
this.uploadManager = uploadManager this.uploadManager = uploadManager
this.establisher = establisher
acceptorThread = Executors.newSingleThreadExecutor { r -> acceptorThread = Executors.newSingleThreadExecutor { r ->
def rv = new Thread(r) def rv = new Thread(r)
rv.setDaemon(true) rv.setDaemon(true)
@@ -140,7 +143,9 @@ class ConnectionAcceptor {
} }
private void handleIncoming(Endpoint e, boolean leaf) { private void handleIncoming(Endpoint e, boolean leaf) {
boolean accept = !manager.isConnected(e.destination) && (leaf ? manager.hasLeafSlots() : manager.hasPeerSlots()) boolean accept = !manager.isConnected(e.destination) &&
!establisher.inProgress.contains(e.destination) &&
(leaf ? manager.hasLeafSlots() : manager.hasPeerSlots())
if (accept) { if (accept) {
log.info("accepting connection, leaf:$leaf") log.info("accepting connection, leaf:$leaf")
e.outputStream.write("OK".bytes) e.outputStream.write("OK".bytes)
@@ -204,7 +209,7 @@ class ConnectionAcceptor {
byte [] payload = new byte[jsonSize] byte [] payload = new byte[jsonSize]
dis.readFully(payload) dis.readFully(payload)
def json = slurper.parse(payload) def json = slurper.parse(payload)
eventBus.publish(ResultsParser.parse(sender, json)) eventBus.publish(ResultsParser.parse(sender, resultsUUID, json))
} }
} catch (IOException | UnexpectedResultsException | InvalidSearchResultException bad) { } catch (IOException | UnexpectedResultsException | InvalidSearchResultException bad) {
log.log(Level.WARNING, "failed to process POST", bad) log.log(Level.WARNING, "failed to process POST", bad)

View File

@@ -1,6 +1,7 @@
package com.muwire.core.connection package com.muwire.core.connection
import com.muwire.core.EventBus import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import com.muwire.core.Persona import com.muwire.core.Persona
import com.muwire.core.hostcache.HostCache import com.muwire.core.hostcache.HostCache
import com.muwire.core.search.QueryEvent import com.muwire.core.search.QueryEvent
@@ -19,13 +20,15 @@ abstract class ConnectionManager {
protected final HostCache hostCache protected final HostCache hostCache
protected final Persona me protected final Persona me
protected final MuWireSettings settings
ConnectionManager() {} ConnectionManager() {}
ConnectionManager(EventBus eventBus, Persona me, HostCache hostCache) { ConnectionManager(EventBus eventBus, Persona me, HostCache hostCache, MuWireSettings settings) {
this.eventBus = eventBus this.eventBus = eventBus
this.me = me this.me = me
this.hostCache = hostCache this.hostCache = hostCache
this.settings = settings
this.timer = new Timer("connections-pinger",true) this.timer = new Timer("connections-pinger",true)
} }
@@ -40,7 +43,7 @@ abstract class ConnectionManager {
void onTrustEvent(TrustEvent e) { void onTrustEvent(TrustEvent e) {
if (e.level == TrustLevel.DISTRUSTED) if (e.level == TrustLevel.DISTRUSTED)
drop(e.destination) drop(e.persona.destination)
} }
abstract void drop(Destination d) abstract void drop(Destination d)
@@ -58,6 +61,8 @@ abstract class ConnectionManager {
abstract void onConnectionEvent(ConnectionEvent e) abstract void onConnectionEvent(ConnectionEvent e)
abstract void onDisconnectionEvent(DisconnectionEvent e) abstract void onDisconnectionEvent(DisconnectionEvent e)
abstract void shutdown()
protected void sendPings() { protected void sendPings() {
final long now = System.currentTimeMillis() final long now = System.currentTimeMillis()

View File

@@ -4,6 +4,7 @@ import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import com.muwire.core.EventBus import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import com.muwire.core.hostcache.HostCache import com.muwire.core.hostcache.HostCache
import com.muwire.core.trust.TrustService import com.muwire.core.trust.TrustService
@@ -16,8 +17,9 @@ import net.i2p.data.Destination
*/ */
class LeafConnection extends Connection { class LeafConnection extends Connection {
public LeafConnection(EventBus eventBus, Endpoint endpoint, HostCache hostCache, TrustService trustService) { public LeafConnection(EventBus eventBus, Endpoint endpoint, HostCache hostCache,
super(eventBus, endpoint, true, hostCache, trustService); TrustService trustService, MuWireSettings settings) {
super(eventBus, endpoint, true, hostCache, trustService, settings);
} }
@Override @Override

View File

@@ -3,6 +3,7 @@ package com.muwire.core.connection
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import com.muwire.core.EventBus import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import com.muwire.core.Persona import com.muwire.core.Persona
import com.muwire.core.hostcache.HostCache import com.muwire.core.hostcache.HostCache
import com.muwire.core.search.QueryEvent import com.muwire.core.search.QueryEvent
@@ -17,8 +18,9 @@ class LeafConnectionManager extends ConnectionManager {
final Map<Destination, UltrapeerConnection> connections = new ConcurrentHashMap() final Map<Destination, UltrapeerConnection> connections = new ConcurrentHashMap()
public LeafConnectionManager(EventBus eventBus, Persona me, int maxConnections, HostCache hostCache) { public LeafConnectionManager(EventBus eventBus, Persona me, int maxConnections,
super(eventBus, me, hostCache) HostCache hostCache, MuWireSettings settings) {
super(eventBus, me, hostCache, settings)
this.maxConnections = maxConnections this.maxConnections = maxConnections
} }
@@ -71,4 +73,8 @@ class LeafConnectionManager extends ConnectionManager {
log.severe("removed destination not present in connection manager ${e.destination.toBase32()}") log.severe("removed destination not present in connection manager ${e.destination.toBase32()}")
} }
@Override
void shutdown() {
}
} }

View File

@@ -4,6 +4,7 @@ import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import com.muwire.core.EventBus import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import com.muwire.core.hostcache.HostCache import com.muwire.core.hostcache.HostCache
import com.muwire.core.trust.TrustService import com.muwire.core.trust.TrustService
import com.muwire.core.util.DataUtil import com.muwire.core.util.DataUtil
@@ -29,8 +30,9 @@ class PeerConnection extends Connection {
private final JsonSlurper slurper = new JsonSlurper() private final JsonSlurper slurper = new JsonSlurper()
public PeerConnection(EventBus eventBus, Endpoint endpoint, public PeerConnection(EventBus eventBus, Endpoint endpoint,
boolean incoming, HostCache hostCache, TrustService trustService) { boolean incoming, HostCache hostCache, TrustService trustService,
super(eventBus, endpoint, incoming, hostCache, trustService) MuWireSettings settings) {
super(eventBus, endpoint, incoming, hostCache, trustService, settings)
this.dis = new DataInputStream(endpoint.inputStream) this.dis = new DataInputStream(endpoint.inputStream)
this.dos = new DataOutputStream(endpoint.outputStream) this.dos = new DataOutputStream(endpoint.outputStream)
} }

View File

@@ -4,6 +4,7 @@ import java.util.Collection
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import com.muwire.core.EventBus import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import com.muwire.core.Persona import com.muwire.core.Persona
import com.muwire.core.hostcache.HostCache import com.muwire.core.hostcache.HostCache
import com.muwire.core.search.QueryEvent import com.muwire.core.search.QueryEvent
@@ -17,15 +18,15 @@ class UltrapeerConnectionManager extends ConnectionManager {
final int maxPeers, maxLeafs final int maxPeers, maxLeafs
final TrustService trustService final TrustService trustService
final Map<Destination, PeerConnection> peerConnections = new ConcurrentHashMap() final Map<Destination, PeerConnection> peerConnections = new ConcurrentHashMap()
final Map<Destination, LeafConnection> leafConnections = new ConcurrentHashMap() final Map<Destination, LeafConnection> leafConnections = new ConcurrentHashMap()
UltrapeerConnectionManager() {} UltrapeerConnectionManager() {}
public UltrapeerConnectionManager(EventBus eventBus, Persona me, int maxPeers, int maxLeafs, public UltrapeerConnectionManager(EventBus eventBus, Persona me, int maxPeers, int maxLeafs,
HostCache hostCache, TrustService trustService) { HostCache hostCache, TrustService trustService, MuWireSettings settings) {
super(eventBus, me, hostCache) super(eventBus, me, hostCache, settings)
this.maxPeers = maxPeers this.maxPeers = maxPeers
this.maxLeafs = maxLeafs this.maxLeafs = maxLeafs
this.trustService = trustService this.trustService = trustService
@@ -85,8 +86,8 @@ class UltrapeerConnectionManager extends ConnectionManager {
return return
Connection c = e.leaf ? Connection c = e.leaf ?
new LeafConnection(eventBus, e.endpoint, hostCache, trustService) : new LeafConnection(eventBus, e.endpoint, hostCache, trustService, settings) :
new PeerConnection(eventBus, e.endpoint, e.incoming, hostCache, trustService) new PeerConnection(eventBus, e.endpoint, e.incoming, hostCache, trustService, settings)
def map = e.leaf ? leafConnections : peerConnections def map = e.leaf ? leafConnections : peerConnections
map.put(e.endpoint.destination, c) map.put(e.endpoint.destination, c)
c.start() c.start()
@@ -100,6 +101,14 @@ class UltrapeerConnectionManager extends ConnectionManager {
if (removed == null) if (removed == null)
log.severe("Removed connection not present in either leaf or peer map ${e.destination.toBase32()}") log.severe("Removed connection not present in either leaf or peer map ${e.destination.toBase32()}")
} }
@Override
void shutdown() {
peerConnections.each {k,v -> v.close() }
leafConnections.each {k,v -> v.close() }
peerConnections.clear()
leafConnections.clear()
}
void forwardQueryToLeafs(QueryEvent e) { void forwardQueryToLeafs(QueryEvent e) {

View File

@@ -0,0 +1,25 @@
package com.muwire.core.download
class BadHashException extends Exception {
public BadHashException() {
super();
}
public BadHashException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
public BadHashException(String message, Throwable cause) {
super(message, cause);
}
public BadHashException(String message) {
super(message);
}
public BadHashException(Throwable cause) {
super(cause);
}
}

View File

@@ -1,7 +1,12 @@
package com.muwire.core.download 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.Destination
import com.muwire.core.EventBus import com.muwire.core.EventBus
import com.muwire.core.Persona
import java.util.concurrent.Executor import java.util.concurrent.Executor
import java.util.concurrent.Executors import java.util.concurrent.Executors
@@ -11,10 +16,17 @@ public class DownloadManager {
private final EventBus eventBus private final EventBus eventBus
private final I2PConnector connector private final I2PConnector connector
private final Executor executor private final Executor executor
private final File incompletes
private final Persona me
public DownloadManager(EventBus eventBus, I2PConnector connector) { 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.me = me
incompletes.mkdir()
this.executor = Executors.newCachedThreadPool({ r -> this.executor = Executors.newCachedThreadPool({ r ->
Thread rv = new Thread(r) Thread rv = new Thread(r)
rv.setName("download-worker") rv.setName("download-worker")
@@ -25,9 +37,24 @@ public class DownloadManager {
public void onUIDownloadEvent(UIDownloadEvent e) { public void onUIDownloadEvent(UIDownloadEvent e) {
def downloader = new Downloader(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(eventBus, this, me, e.target, size,
infohash, pieceSize, connector, destinations,
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))
} }
void resume(Downloader downloader) {
executor.execute({downloader.download() as Runnable})
}
} }

View File

@@ -20,7 +20,10 @@ import java.security.NoSuchAlgorithmException
@Log @Log
class DownloadSession { class DownloadSession {
private final Pieces pieces private static int SAMPLES = 10
private final String meB64
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
@@ -28,11 +31,16 @@ class DownloadSession {
private final long fileLength private final long fileLength
private final MessageDigest digest private final MessageDigest digest
private final LinkedList<Long> timestamps = new LinkedList<>()
private final LinkedList<Integer> reads = new LinkedList<>()
private ByteBuffer mapped private ByteBuffer mapped
DownloadSession(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.pieces = pieces this.meB64 = meB64
this.downloaded = downloaded
this.claimed = claimed
this.endpoint = endpoint this.endpoint = endpoint
this.infoHash = infoHash this.infoHash = infoHash
this.file = file this.file = file
@@ -46,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
@@ -60,7 +88,8 @@ class DownloadSession {
FileChannel channel FileChannel channel
try { try {
os.write("GET $root\r\n".getBytes(StandardCharsets.US_ASCII)) 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() os.flush()
String code = readTillRN(is) String code = readTillRN(is)
if (code.startsWith("404 ")) { if (code.startsWith("404 ")) {
@@ -119,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)
} }
} }
@@ -127,16 +163,15 @@ class DownloadSession {
byte [] hash = digest.digest() byte [] hash = digest.digest()
byte [] expected = new byte[32] byte [] expected = new byte[32]
System.arraycopy(infoHash.getHashList(), piece * 32, expected, 0, 32) System.arraycopy(infoHash.getHashList(), piece * 32, expected, 0, 32)
if (hash != expected) { if (hash != expected)
log.warning("hash mismatch") throw new BadHashException()
endpoint.close()
return
}
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,26 @@ class DownloadSession {
return 0 return 0
mapped.position() mapped.position()
} }
synchronized int speed() {
if (timestamps.size() < SAMPLES)
return 0
int totalRead = 0
int idx = 0
final long now = System.currentTimeMillis()
while(idx < SAMPLES && timestamps.get(idx) < now - 1000)
idx++
if (idx == SAMPLES)
return 0
if (idx == SAMPLES - 1)
return reads[idx]
long interval = timestamps.last - timestamps[idx]
if (interval == 0)
interval = 1
for (int i = idx; i < SAMPLES; i++)
totalRead += reads[idx]
(int)(totalRead * 1000.0 / interval)
}
} }

View File

@@ -1,34 +1,66 @@
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 com.muwire.core.Constants
import com.muwire.core.connection.I2PConnector
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.logging.Level
import com.muwire.core.Constants
import com.muwire.core.DownloadedFile
import com.muwire.core.EventBus
import com.muwire.core.connection.I2PConnector
import com.muwire.core.files.FileDownloadedEvent
import groovy.util.logging.Log
import net.i2p.data.Destination import net.i2p.data.Destination
@Log
public class Downloader { public class Downloader {
public enum DownloadState { CONNECTING, DOWNLOADING, 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 EventBus eventBus
private final DownloadManager downloadManager
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 Map<Destination, DownloadWorker> activeWorkers = new ConcurrentHashMap<>()
private Endpoint endpoint
private volatile DownloadSession currentSession
private volatile DownloadState currentState
public Downloader(File file, long length, InfoHash infoHash, int pieceSizePow2, I2PConnector connector, Destination destination) { private volatile boolean cancelled
private volatile boolean eventFired
public Downloader(EventBus eventBus, DownloadManager downloadManager,
Persona me, File file, long length, InfoHash infoHash,
int pieceSizePow2, I2PConnector connector, Set<Destination> destinations,
File incompletes) {
this.eventBus = eventBus
this.me = me
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.pieceSize = 1 << pieceSizePow2 this.pieceSize = 1 << pieceSizePow2
int nPieces int nPieces
@@ -38,32 +70,155 @@ 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() {
Endpoint endpoint = connector.connect(destination) readPieces()
currentState = DownloadState.DOWNLOADING destinations.each {
while(!pieces.isComplete()) { if (it != me.destination) {
currentSession = new DownloadSession(pieces, infoHash, endpoint, file, pieceSize, length) def worker = new DownloadWorker(it)
currentSession.request() activeWorkers.put(it, worker)
executorService.submit(worker)
}
}
}
void readPieces() {
if (!piecesFile.exists())
return
piecesFile.withReader {
int piece = Integer.parseInt(it.readLine())
downloaded.markDownloaded(piece)
}
}
void writePieces() {
piecesFile.withPrintWriter { writer ->
downloaded.getDownloaded().each { piece ->
writer.println(piece)
}
} }
currentState = DownloadState.FINISHED
endpoint.close()
} }
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() if (getCurrentState() == DownloadState.DOWNLOADING) {
activeWorkers.values().each {
if (it.currentState == WorkerState.DOWNLOADING)
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() {
cancelled = true
activeWorkers.values().each {
it.cancel()
}
}
public int activeWorkers() {
int active = 0
activeWorkers.values().each {
if (it.currentState != WorkerState.FINISHED)
active++
}
active
}
public void resume() {
activeWorkers.each { destination, worker ->
if (worker.currentState == WorkerState.FINISHED) {
def newWorker = new DownloadWorker(destination)
activeWorkers.put(destination, newWorker)
executorService.submit(newWorker)
}
}
}
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
if (downloaded.isComplete() && !eventFired) {
piecesFile.delete()
eventFired = true
eventBus.publish(new FileDownloadedEvent(downloadedFile : new DownloadedFile(file, infoHash, Collections.emptySet())))
}
endpoint?.close()
}
}
int speed() {
if (currentSession == null)
return 0
currentSession.speed()
}
void cancel() {
downloadThread?.interrupt()
}
} }
} }

View File

@@ -28,19 +28,36 @@ 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
} }
} }
def getDownloaded() {
def rv = []
for (int i = bitSet.nextSetBit(0); i >= 0; i = bitSet.nextSetBit(i+1)) {
rv << i
}
rv
}
synchronized void markDownloaded(int piece) { synchronized void markDownloaded(int piece) {
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

@@ -0,0 +1,6 @@
package com.muwire.core.files
import com.muwire.core.Event
class AllFilesLoadedEvent extends Event {
}

View File

@@ -52,11 +52,11 @@ class FileHasher {
try { try {
MappedByteBuffer buf MappedByteBuffer buf
for (int i = 0; i < numPieces - 1; i++) { for (int i = 0; i < numPieces - 1; i++) {
buf = raf.getChannel().map(MapMode.READ_ONLY, size * i, size) buf = raf.getChannel().map(MapMode.READ_ONLY, ((long)size) * i, size)
digest.update buf digest.update buf
output.write(digest.digest(), 0, 32) output.write(digest.digest(), 0, 32)
} }
def lastPieceLength = length - (numPieces - 1) * size def lastPieceLength = length - (numPieces - 1) * ((long)size)
buf = raf.getChannel().map(MapMode.READ_ONLY, length - lastPieceLength, lastPieceLength) buf = raf.getChannel().map(MapMode.READ_ONLY, length - lastPieceLength, lastPieceLength)
digest.update buf digest.update buf
output.write(digest.digest(), 0, 32) output.write(digest.digest(), 0, 32)

View File

@@ -2,6 +2,7 @@ package com.muwire.core.files
import com.muwire.core.EventBus import com.muwire.core.EventBus
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
import com.muwire.core.MuWireSettings
import com.muwire.core.SharedFile import com.muwire.core.SharedFile
import com.muwire.core.search.ResultsEvent import com.muwire.core.search.ResultsEvent
import com.muwire.core.search.SearchEvent import com.muwire.core.search.SearchEvent
@@ -14,18 +15,22 @@ class FileManager {
final EventBus eventBus final EventBus eventBus
final MuWireSettings settings
final Map<InfoHash, Set<SharedFile>> rootToFiles = Collections.synchronizedMap(new HashMap<>()) final Map<InfoHash, Set<SharedFile>> rootToFiles = Collections.synchronizedMap(new HashMap<>())
final Map<File, SharedFile> fileToSharedFile = Collections.synchronizedMap(new HashMap<>()) final Map<File, SharedFile> fileToSharedFile = Collections.synchronizedMap(new HashMap<>())
final Map<String, Set<File>> nameToFiles = new HashMap<>() final Map<String, Set<File>> nameToFiles = new HashMap<>()
final SearchIndex index = new SearchIndex() final SearchIndex index = new SearchIndex()
FileManager(EventBus eventBus) { FileManager(EventBus eventBus, MuWireSettings settings) {
this.settings = settings
this.eventBus = eventBus this.eventBus = eventBus
} }
void onFileHashedEvent(FileHashedEvent e) { void onFileHashedEvent(FileHashedEvent e) {
if (e.sharedFile != null) if (settings.shareDownloadedFiles) {
addToIndex(e.sharedFile) if (e.sharedFile != null)
addToIndex(e.sharedFile)
}
} }
void onFileLoadedEvent(FileLoadedEvent e) { void onFileLoadedEvent(FileLoadedEvent e) {

View File

@@ -10,11 +10,13 @@ class HasherService {
final FileHasher hasher final FileHasher hasher
final EventBus eventBus final EventBus eventBus
final FileManager fileManager
Executor executor Executor executor
HasherService(FileHasher hasher, EventBus eventBus) { HasherService(FileHasher hasher, EventBus eventBus, FileManager fileManager) {
this.hasher = hasher this.hasher = hasher
this.eventBus = eventBus this.eventBus = eventBus
this.fileManager = fileManager
} }
void start() { void start() {
@@ -22,6 +24,8 @@ class HasherService {
} }
void onFileSharedEvent(FileSharedEvent evt) { void onFileSharedEvent(FileSharedEvent evt) {
if (fileManager.fileToSharedFile.containsKey(evt.file))
return
executor.execute( { -> process(evt.file) } as Runnable) executor.execute( { -> process(evt.file) } as Runnable)
} }

View File

@@ -1,5 +1,8 @@
package com.muwire.core.files package com.muwire.core.files
import java.nio.file.CopyOption
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.util.logging.Level import java.util.logging.Level
import java.util.stream.Collectors import java.util.stream.Collectors
@@ -34,7 +37,7 @@ class PersisterService extends Service {
} }
void start() { void start() {
timer.schedule({load()} as TimerTask, 1000) timer.schedule({load()} as TimerTask, 1)
} }
void stop() { void stop() {
@@ -55,6 +58,7 @@ class PersisterService extends Service {
} }
} }
} }
listener.publish(new AllFilesLoadedEvent())
} catch (IllegalArgumentException|NumberFormatException e) { } catch (IllegalArgumentException|NumberFormatException e) {
log.log(Level.WARNING, "couldn't load files",e) log.log(Level.WARNING, "couldn't load files",e)
} }
@@ -107,15 +111,19 @@ class PersisterService extends Service {
} }
private void persistFiles() { private void persistFiles() {
location.delete()
def sharedFiles = fileManager.getSharedFiles() def sharedFiles = fileManager.getSharedFiles()
location.withPrintWriter { writer ->
File tmp = File.createTempFile("muwire-files", "tmp")
tmp.deleteOnExit()
tmp.withPrintWriter { writer ->
sharedFiles.each { k, v -> sharedFiles.each { k, v ->
def json = toJson(k,v) def json = toJson(k,v)
json = JsonOutput.toJson(json) json = JsonOutput.toJson(json)
writer.println json writer.println json
} }
} }
Files.copy(tmp.toPath(), location.toPath(), StandardCopyOption.REPLACE_EXISTING)
tmp.delete()
} }
private def toJson(File f, SharedFile sf) { private def toJson(File f, SharedFile sf) {

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)
} }
} }
@@ -140,7 +140,7 @@ class CacheClient {
pong.pongs.asList().each { pong.pongs.asList().each {
Destination dest = new Destination(it) Destination dest = new Destination(it)
if (!session.getMyDestination().equals(dest)) if (!session.getMyDestination().equals(dest))
eventBus.publish(new HostDiscoveredEvent(destination: dest)) eventBus.publish(new HostDiscoveredEvent(destination: dest, fromHostcache : true))
} }
} }

View File

@@ -30,4 +30,8 @@ class Host {
synchronized boolean hasSucceeded() { synchronized boolean hasSucceeded() {
successes > 0 successes > 0
} }
synchronized void clearFailures() {
failures = 0
}
} }

View File

@@ -46,8 +46,12 @@ class HostCache extends Service {
void onHostDiscoveredEvent(HostDiscoveredEvent e) { void onHostDiscoveredEvent(HostDiscoveredEvent e) {
if (myself == e.destination) if (myself == e.destination)
return return
if (hosts.containsKey(e.destination)) if (hosts.containsKey(e.destination)) {
return if (!e.fromHostcache)
return
hosts.get(e.destination).clearFailures()
return
}
Host host = new Host(e.destination) Host host = new Host(e.destination)
if (allowHost(host)) { if (allowHost(host)) {
hosts.put(e.destination, host) hosts.put(e.destination, host)
@@ -55,7 +59,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

@@ -7,9 +7,10 @@ import net.i2p.data.Destination
class HostDiscoveredEvent extends Event { class HostDiscoveredEvent extends Event {
Destination destination Destination destination
boolean fromHostcache
@Override @Override
public String toString() { public String toString() {
"HostDiscoveredEvent ${super.toString()} destination:${destination.toBase32()}" "HostDiscoveredEvent ${super.toString()} destination:${destination.toBase32()} from hostcache $fromHostcache"
} }
} }

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,11 @@ class QueryEvent extends Event {
SearchEvent searchEvent SearchEvent searchEvent
boolean firstHop boolean firstHop
Destination replyTo Destination replyTo
Persona originator
Destination receivedOn Destination receivedOn
String toString() {
"searchEvent: $searchEvent firstHop:$firstHop, replyTo:${replyTo.toBase32()}" +
"originator: ${originator.getHumanReadableName()} receivedOn: ${receivedOn.toBase32()}"
}
} }

View File

@@ -9,7 +9,7 @@ import com.muwire.core.util.DataUtil
import net.i2p.data.Base64 import net.i2p.data.Base64
class ResultsParser { class ResultsParser {
public static UIResultEvent parse(Persona p, def json) throws InvalidSearchResultException { public static UIResultEvent parse(Persona p, UUID uuid, def json) throws InvalidSearchResultException {
if (json.type != "Result") if (json.type != "Result")
throw new InvalidSearchResultException("not a result json") throw new InvalidSearchResultException("not a result json")
if (json.version != 1) if (json.version != 1)
@@ -46,7 +46,8 @@ class ResultsParser {
name : name, name : name,
size : size, size : size,
infohash : parsedIH, infohash : parsedIH,
pieceSize : pieceSize) pieceSize : pieceSize,
uuid : uuid)
} catch (Exception e) { } catch (Exception e) {
throw new InvalidSearchResultException("parsing search result failed",e) throw new InvalidSearchResultException("parsing search result failed",e)
} }

View File

@@ -55,7 +55,8 @@ class ResultsSender {
name : it.getFile().getName(), name : it.getFile().getName(),
size : length, size : length,
infohash : it.getInfoHash(), infohash : it.getInfoHash(),
pieceSize : FileHasher.getPieceSize(length) pieceSize : FileHasher.getPieceSize(length),
uuid : uuid
) )
eventBus.publish(uiResultEvent) eventBus.publish(uiResultEvent)
} }

View File

@@ -1,10 +1,18 @@
package com.muwire.core.search package com.muwire.core.search
import com.muwire.core.Event import com.muwire.core.Event
import com.muwire.core.InfoHash
class SearchEvent extends Event { class SearchEvent extends Event {
List<String> searchTerms List<String> searchTerms
byte [] searchHash byte [] searchHash
UUID uuid UUID uuid
String toString() {
def infoHash = null
if (searchHash != null)
infoHash = new InfoHash(searchHash)
"searchTerms: $searchTerms searchHash:$infoHash, uuid:$uuid"
}
} }

View File

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

View File

@@ -6,6 +6,7 @@ import com.muwire.core.Persona
class UIResultEvent extends Event { class UIResultEvent extends Event {
Persona sender Persona sender
UUID uuid
String name String name
long size long size
InfoHash infohash InfoHash infohash

View File

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

View File

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

View File

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

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, 60000, 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, infoHash : payload.infoHash))
} 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

@@ -4,6 +4,7 @@ import java.nio.charset.StandardCharsets
import com.muwire.core.Constants import com.muwire.core.Constants
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
import com.muwire.core.Persona
import groovy.util.logging.Log import groovy.util.logging.Log
import net.i2p.data.Base64 import net.i2p.data.Base64
@@ -16,6 +17,7 @@ class Request {
InfoHash infoHash InfoHash infoHash
Range range Range range
Persona downloader
Map<String, String> headers Map<String, String> headers
static Request parse(InfoHash infoHash, InputStream is) throws IOException { static Request parse(InfoHash infoHash, InputStream is) throws IOException {
@@ -85,7 +87,13 @@ class Request {
if (start < 0 || end < start) if (start < 0 || end < start)
throw new IOException("Invalid range $start - $end") 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()) 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

@@ -55,21 +55,21 @@ class JULLog extends Log {
@Override @Override
public boolean shouldDebug() { public boolean shouldDebug() {
level.intValue().intValue() >= Level.FINE.intValue() level.intValue().intValue() <= Level.FINE.intValue()
} }
@Override @Override
public boolean shouldInfo() { public boolean shouldInfo() {
level.intValue().intValue() >= Level.INFO.intValue() level.intValue().intValue() <= Level.INFO.intValue()
} }
@Override @Override
public boolean shouldWarn() { public boolean shouldWarn() {
level.intValue().intValue() >= Level.WARNING.intValue() level.intValue().intValue() <= Level.WARNING.intValue()
} }
@Override @Override
public boolean shouldError() { public boolean shouldError() {
level.intValue().intValue() >= Level.SEVERE.intValue() level.intValue().intValue() <= Level.SEVERE.intValue()
} }
} }

View File

@@ -77,13 +77,15 @@ public class InfoHash {
public String toString() { public String toString() {
String rv = "InfoHash[root:"+Base32.encode(root) + " hashList:"; String rv = "InfoHash[root:"+Base32.encode(root) + " hashList:";
List<String> b32HashList = new ArrayList<>(hashList.length / SIZE); List<String> b64HashList = new ArrayList<>();
byte [] tmp = new byte[SIZE]; if (hashList != null) {
for (int i = 0; i < hashList.length / SIZE; i++) { byte [] tmp = new byte[SIZE];
System.arraycopy(hashList, SIZE * i, tmp, 0, SIZE); for (int i = 0; i < hashList.length / SIZE; i++) {
b32HashList.add(Base32.encode(tmp)); System.arraycopy(hashList, SIZE * i, tmp, 0, SIZE);
b64HashList.add(Base32.encode(tmp));
}
} }
rv += b32HashList.toString(); rv += b64HashList.toString();
rv += "]"; rv += "]";
return rv; return rv;
} }

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()
@@ -74,6 +76,7 @@ class DownloadSessionTest {
initSession(20) initSession(20)
assert "GET $rootBase64" == readTillRN(fromDownloader) assert "GET $rootBase64" == readTillRN(fromDownloader)
assert "Range: 0-19" == readTillRN(fromDownloader) assert "Range: 0-19" == readTillRN(fromDownloader)
readTillRN(fromDownloader)
assert "" == readTillRN(fromDownloader) assert "" == readTillRN(fromDownloader)
toDownloader.write("200 OK\r\n".bytes) toDownloader.write("200 OK\r\n".bytes)
@@ -95,6 +98,7 @@ class DownloadSessionTest {
assert "GET $rootBase64" == readTillRN(fromDownloader) assert "GET $rootBase64" == readTillRN(fromDownloader)
readTillRN(fromDownloader) readTillRN(fromDownloader)
readTillRN(fromDownloader)
assert "" == readTillRN(fromDownloader) assert "" == readTillRN(fromDownloader)
toDownloader.write("200 OK\r\n".bytes) toDownloader.write("200 OK\r\n".bytes)
@@ -122,6 +126,7 @@ class DownloadSessionTest {
assert (start == 0 && end == ((1 << pieceSize) - 1)) || assert (start == 0 && end == ((1 << pieceSize) - 1)) ||
(start == (1 << pieceSize) && end == (1 << pieceSize)) (start == (1 << pieceSize) && end == (1 << pieceSize))
readTillRN(fromDownloader)
assert "" == readTillRN(fromDownloader) assert "" == readTillRN(fromDownloader)
toDownloader.write("200 OK\r\n".bytes) toDownloader.write("200 OK\r\n".bytes)
@@ -135,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.TRUSTED == service.getLevel(personas.persona1.destination)
assert TrustLevel.DISTRUSTED == service.getLevel(dests.dest2) assert TrustLevel.DISTRUSTED == service.getLevel(personas.persona2.destination)
} }
} }

View File

@@ -1,5 +1,5 @@
group = com.muwire group = com.muwire
version = 0.0.2 version = 0.0.13
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

@@ -41,6 +41,7 @@ griffon {
} }
mainClassName = 'com.muwire.gui.Launcher' mainClassName = 'com.muwire.gui.Launcher'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
apply from: 'gradle/publishing.gradle' apply from: 'gradle/publishing.gradle'
apply from: 'gradle/code-coverage.gradle' apply from: 'gradle/code-coverage.gradle'
@@ -58,6 +59,7 @@ dependencies {
compile "org.codehaus.griffon:griffon-guice:${griffon.version}" compile "org.codehaus.griffon:griffon-guice:${griffon.version}"
runtime "org.slf4j:slf4j-simple:${slf4jVersion}" 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.codehaus.griffon:griffon-fest-test:${griffon.version}"
testCompile "org.spockframework:spock-core:${spockVersion}" testCompile "org.spockframework:spock-core:${spockVersion}"

View File

@@ -16,4 +16,14 @@ mvcGroups {
view = 'com.muwire.gui.MainFrameView' view = 'com.muwire.gui.MainFrameView'
controller = 'com.muwire.gui.MainFrameController' controller = 'com.muwire.gui.MainFrameController'
} }
} 'SearchTab' {
model = 'com.muwire.gui.SearchTabModel'
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

@@ -3,11 +3,16 @@ package com.muwire.gui
import griffon.core.GriffonApplication import griffon.core.GriffonApplication
import griffon.core.artifact.GriffonController import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction import griffon.core.controller.ControllerAction
import griffon.core.mvc.MVCGroup
import griffon.core.mvc.MVCGroupConfiguration
import griffon.inject.MVCMember import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor import griffon.metadata.ArtifactProviderFor
import net.i2p.data.Base64
import javax.annotation.Nonnull import javax.annotation.Nonnull
import javax.inject.Inject import javax.inject.Inject
import com.muwire.core.Constants
import com.muwire.core.Core import com.muwire.core.Core
import com.muwire.core.download.DownloadStartedEvent import com.muwire.core.download.DownloadStartedEvent
import com.muwire.core.download.UIDownloadEvent import com.muwire.core.download.UIDownloadEvent
@@ -29,35 +34,140 @@ class MainFrameController {
@ControllerAction @ControllerAction
void search() { void search() {
def cardsPanel = builder.getVariable("cards-panel")
cardsPanel.getLayout().show(cardsPanel, "search window")
def search = builder.getVariable("search-field").text def search = builder.getVariable("search-field").text
def searchEvent = new SearchEvent(searchTerms : [search], uuid : UUID.randomUUID()) def uuid = UUID.randomUUID()
Map<String, Object> params = new HashMap<>()
params["search-terms"] = search
params["uuid"] = uuid.toString()
def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params)
model.results[uuid.toString()] = group
def searchEvent
if (model.hashSearch) {
searchEvent = new SearchEvent(searchHash : Base64.decode(search), uuid : uuid)
} else {
// this can be improved a lot
def terms = search.toLowerCase().trim().split(Constants.SPLIT_PATTERN)
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))
}
void search(String infoHash, String tabTitle) {
def cardsPanel = builder.getVariable("cards-panel")
cardsPanel.getLayout().show(cardsPanel, "search window")
def uuid = UUID.randomUUID()
Map<String, Object> params = new HashMap<>()
params["search-terms"] = tabTitle
params["uuid"] = uuid.toString()
def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params)
model.results[uuid.toString()] = group
def searchEvent = new SearchEvent(searchHash : Base64.decode(infoHash), uuid:uuid)
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true,
replyTo: core.me.destination, receivedOn: core.me.destination,
originator : core.me))
} }
private def selectedResult() { private def selectedResult() {
def resultsTable = builder.getVariable("results-table") def selected = builder.getVariable("result-tabs").getSelectedComponent()
int row = resultsTable.getSelectedRow() def group = selected.getClientProperty("mvc-group")
model.results[row] def table = selected.getClientProperty("results-table")
int row = table.getSelectedRow()
if (row == -1)
return
group.model.results[row]
}
private def selectedDownload() {
def selected = builder.getVariable("downloads-table").getSelectedRow()
model.downloads[selected].downloader
} }
@ControllerAction @ControllerAction
void download() { void download() {
def result = selectedResult() def result = selectedResult()
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name) if (result == null)
core.eventBus.publish(new UIDownloadEvent(result : result, target : file)) return // TODO disable button
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
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
void trust() { void trust() {
def result = selectedResult() def result = selectedResult()
core.eventBus.publish( new TrustEvent(destination : result.sender.destination, level : TrustLevel.TRUSTED)) if (result == null)
return // TODO disable button
core.eventBus.publish( new TrustEvent(persona : result.sender, level : TrustLevel.TRUSTED))
} }
@ControllerAction @ControllerAction
void distrust() { void distrust() {
def result = selectedResult() def result = selectedResult()
core.eventBus.publish( new TrustEvent(destination : result.sender.destination, level : TrustLevel.DISTRUSTED)) if (result == null)
return // TODO disable button
core.eventBus.publish( new TrustEvent(persona : result.sender, level : TrustLevel.DISTRUSTED))
}
@ControllerAction
void cancel() {
def downloader = selectedDownload()
downloader.cancel()
}
@ControllerAction
void resume() {
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)
}
@ControllerAction
void keywordSearch() {
model.hashSearch = false
}
@ControllerAction
void hashSearch() {
model.hashSearch = true
} }
void mvcGroupInit(Map<String, String> args) { void mvcGroupInit(Map<String, String> args) {

View File

@@ -0,0 +1,77 @@
package com.muwire.gui
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
import com.muwire.core.Core
@ArtifactProviderFor(GriffonController)
class OptionsController {
@MVCMember @Nonnull
OptionsModel model
@MVCMember @Nonnull
OptionsView view
@ControllerAction
void save() {
String text
Core core = application.context.get("core")
def i2pProps = core.i2pOptions
text = view.inboundLengthField.text
model.inboundLength = text
i2pProps["inbound.length"] = text
text = view.inboundQuantityField.text
model.inboundQuantity = text
i2pProps["inbound.quantity"] = text
text = view.outboundQuantityField.text
model.outboundQuantity = text
i2pProps["outbound.quantity"] = text
text = view.outboundLengthField.text
model.outboundLength = text
i2pProps["outbound.length"] = text
File i2pSettingsFile = new File(core.home, "i2p.properties")
i2pSettingsFile.withOutputStream {
i2pProps.store(it,"")
}
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)
boolean onlyTrusted = view.allowUntrustedCheckbox.model.isSelected()
model.onlyTrusted = onlyTrusted
settings.setAllowUntrusted(!onlyTrusted)
boolean shareDownloaded = view.shareDownloadedCheckbox.model.isSelected()
model.shareDownloadedFiles = shareDownloaded
settings.shareDownloadedFiles = shareDownloaded
File settingsFile = new File(core.home, "MuWire.properties")
settingsFile.withOutputStream {
settings.write(it)
}
cancel()
}
@ControllerAction
void cancel() {
view.d.setVisible(false)
mvcGroup.destroy()
}
}

View File

@@ -0,0 +1,11 @@
package com.muwire.gui
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonController)
class SearchTabController {
}

View File

@@ -21,7 +21,11 @@ class Initialize extends AbstractLifecycleHandler {
@Override @Override
void execute() { void execute() {
lookAndFeel((isMacOSX ? 'system' : 'nimbus'), 'gtk', ['metal', [boldFonts: false]]) if (isMacOSX()) {
lookAndFeel('nimbus') // otherwise the file chooser doesn't open???
} else {
lookAndFeel('system', 'gtk')
}
} }
} }

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
@@ -16,9 +17,13 @@ import static griffon.util.GriffonApplicationUtils.isMacOSX
import static groovy.swing.SwingBuilder.lookAndFeel import static groovy.swing.SwingBuilder.lookAndFeel
import java.beans.PropertyChangeEvent import java.beans.PropertyChangeEvent
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)
@@ -27,7 +32,11 @@ class Ready extends AbstractLifecycleHandler {
@Override @Override
void execute() { void execute() {
log.info "starting core services" 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) home = new File(home)
if (!home.exists()) { if (!home.exists()) {
log.info("creating home dir") log.info("creating home dir")
@@ -63,8 +72,13 @@ class Ready extends AbstractLifecycleHandler {
nickname = nickname.trim() nickname = nickname.trim()
break 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() def chooser = new JFileChooser()
chooser.setDialogTitle("Select a directory where downloads will be saved") chooser.setDialogTitle("Select a directory where downloads will be saved")
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY) chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY)
@@ -74,15 +88,22 @@ class Ready extends AbstractLifecycleHandler {
System.exit(0) System.exit(0)
} }
props.downloadLocation = chooser.getSelectedFile() props.downloadLocation = chooser.getSelectedFile()
break
} }
props.setNickname(nickname)
propsFile.withOutputStream { propsFile.withOutputStream {
props.write(it) props.write(it)
} }
} }
Core core = new Core(props, home) Core core
try {
core = new Core(props, home, metadata["application.version"])
} catch (Exception 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",
"Can't connect to I2P router", JOptionPane.WARNING_MESSAGE)
System.exit(0)
}
core.startServices() core.startServices()
application.context.put("muwire-settings", props) application.context.put("muwire-settings", props)
application.context.put("core",core) application.context.put("core",core)

View File

@@ -0,0 +1,25 @@
import javax.annotation.Nonnull
import javax.inject.Inject
import org.codehaus.griffon.runtime.core.AbstractLifecycleHandler
import com.muwire.core.Core
import griffon.core.GriffonApplication
import groovy.util.logging.Log
@Log
class Shutdown extends AbstractLifecycleHandler {
@Inject
Shutdown(@Nonnull GriffonApplication application) {
super(application)
}
@Override
void execute() {
log.info("shutting down")
Core core = application.context.get("core")
core.shutdown()
}
}

View File

@@ -1,50 +1,97 @@
package com.muwire.gui package com.muwire.gui
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
import com.muwire.core.download.DownloadStartedEvent import com.muwire.core.download.DownloadStartedEvent
import com.muwire.core.download.Downloader
import com.muwire.core.files.FileDownloadedEvent
import com.muwire.core.files.FileHashedEvent import com.muwire.core.files.FileHashedEvent
import com.muwire.core.files.FileLoadedEvent import com.muwire.core.files.FileLoadedEvent
import com.muwire.core.files.FileSharedEvent import com.muwire.core.files.FileSharedEvent
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
import griffon.core.GriffonApplication import griffon.core.GriffonApplication
import griffon.core.artifact.GriffonModel import griffon.core.artifact.GriffonModel
import griffon.core.env.Metadata
import griffon.core.mvc.MVCGroup
import griffon.inject.MVCMember import griffon.inject.MVCMember
import griffon.transform.FXObservable import griffon.transform.FXObservable
import griffon.transform.Observable import griffon.transform.Observable
import net.i2p.data.Destination
import griffon.metadata.ArtifactProviderFor import griffon.metadata.ArtifactProviderFor
@ArtifactProviderFor(GriffonModel) @ArtifactProviderFor(GriffonModel)
class MainFrameModel { class MainFrameModel {
@Inject Metadata metadata
@MVCMember @Nonnull @MVCMember @Nonnull
FactoryBuilderSupport builder FactoryBuilderSupport builder
@MVCMember @Nonnull
MainFrameController controller
@Inject @Nonnull GriffonApplication application @Inject @Nonnull GriffonApplication application
@Observable boolean coreInitialized = false @Observable boolean coreInitialized = false
@Observable def results = [] def results = new ConcurrentHashMap<>()
@Observable def downloads = [] def downloads = []
@Observable def uploads = [] def uploads = []
@Observable def shared = [] def shared = []
def connectionList = []
def searches = new LinkedList()
def trusted = []
def distrusted = []
boolean hashSearch
@Observable int connections @Observable int connections
@Observable String me @Observable String me
@Observable boolean searchButtonsEnabled
@Observable boolean cancelButtonEnabled
@Observable boolean retryButtonEnabled
private final Set<InfoHash> infoHashes = new HashSet<>() private final Set<InfoHash> infoHashes = new HashSet<>()
volatile Core core 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) { 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 -> application.addPropertyChangeListener("core", {e ->
coreInitialized = (e.getNewValue() != null) coreInitialized = (e.getNewValue() != null)
core = e.getNewValue() core = e.getNewValue()
@@ -58,22 +105,42 @@ class MainFrameModel {
core.eventBus.register(UploadEvent.class, this) core.eventBus.register(UploadEvent.class, this)
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)
Timer timer = new Timer("download-pumper", true) core.eventBus.register(UpdateAvailableEvent.class, this)
timer.schedule({ core.eventBus.register(FileDownloadedEvent.class, this)
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 {
def state = it.downloader.currentState
if (state == Downloader.DownloadState.FAILED ||
state == Downloader.DownloadState.DOWNLOADING)
it.downloader.resume()
updateTablePreservingSelection("downloads-table")
}
}
}
}
}, 60000, 60000)
runInsideUIAsync { runInsideUIAsync {
builder.getVariable("downloads-table").model.fireTableDataChanged() trusted.addAll(core.trustService.good.values())
builder.getVariable("uploads-table").model.fireTableDataChanged() distrusted.addAll(core.trustService.bad.values())
} }
}, 1000, 1000) })
} }
void onUIResultEvent(UIResultEvent e) { void onUIResultEvent(UIResultEvent e) {
runInsideUIAsync { MVCGroup resultsGroup = results.get(e.uuid)
results << e resultsGroup?.model.handleResult(e)
JTable table = builder.getVariable("results-table")
table.model.fireTableDataChanged()
}
} }
void onDownloadStartedEvent(DownloadStartedEvent e) { void onDownloadStartedEvent(DownloadStartedEvent e) {
@@ -83,14 +150,34 @@ class MainFrameModel {
} }
void onConnectionEvent(ConnectionEvent e) { void onConnectionEvent(ConnectionEvent e) {
if (e.getStatus() != ConnectionAttemptStatus.SUCCESSFUL)
return
runInsideUIAsync { runInsideUIAsync {
connections = core.connectionManager.getConnections().size() 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()
} }
} }
void onDisconnectionEvent(DisconnectionEvent e) { void onDisconnectionEvent(DisconnectionEvent e) {
runInsideUIAsync { runInsideUIAsync {
connections = core.connectionManager.getConnections().size() 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()
} }
} }
@@ -136,7 +223,66 @@ class MainFrameModel {
void onTrustEvent(TrustEvent e) { void onTrustEvent(TrustEvent e) {
runInsideUIAsync { runInsideUIAsync {
JTable table = builder.getVariable("results-table")
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()
}
}
}
void onQueryEvent(QueryEvent e) {
if (e.replyTo == core.me.destination)
return
StringBuilder sb = new StringBuilder()
e.searchEvent.searchTerms?.each {
sb.append(it)
sb.append(" ")
}
def search = sb.toString()
if (search.trim().size() == 0)
return
runInsideUIAsync {
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
}
void onUpdateAvailableEvent(UpdateAvailableEvent e) {
runInsideUIAsync {
int option = JOptionPane.showConfirmDialog(null,
"MuWire $e.version is available from $e.signer. You have "+ metadata["application.version"]+" Update?",
"New MuWire version availble", JOptionPane.OK_CANCEL_OPTION)
if (option == JOptionPane.CANCEL_OPTION)
return
controller.search(e.infoHash,"MuWire update")
}
}
void onFileDownloadedEvent(FileDownloadedEvent e) {
if (!core.muOptions.shareDownloadedFiles)
return
infoHashes.add(e.downloadedFile.infoHash)
runInsideUIAsync {
shared << e.downloadedFile
JTable table = builder.getVariable("shared-files-table")
table.model.fireTableDataChanged() table.model.fireTableDataChanged()
} }
} }

View File

@@ -0,0 +1,36 @@
package com.muwire.gui
import com.muwire.core.Core
import com.muwire.core.MuWireSettings
import griffon.core.artifact.GriffonModel
import griffon.transform.Observable
import griffon.metadata.ArtifactProviderFor
@ArtifactProviderFor(GriffonModel)
class OptionsModel {
@Observable String downloadRetryInterval
@Observable String updateCheckInterval
@Observable boolean onlyTrusted
@Observable boolean shareDownloadedFiles
// i2p options
@Observable String inboundLength
@Observable String inboundQuantity
@Observable String outboundLength
@Observable String outboundQuantity
void mvcGroupInit(Map<String, String> args) {
MuWireSettings settings = application.context.get("muwire-settings")
downloadRetryInterval = settings.downloadRetryInterval
updateCheckInterval = settings.updateCheckInterval
onlyTrusted = !settings.allowUntrusted()
shareDownloadedFiles = settings.shareDownloadedFiles
Core core = application.context.get("core")
inboundLength = core.i2pOptions["inbound.length"]
inboundQuantity = core.i2pOptions["inbound.quantity"]
outboundLength = core.i2pOptions["outbound.length"]
outboundQuantity = core.i2pOptions["outbound.quantity"]
}
}

View File

@@ -0,0 +1,50 @@
package com.muwire.gui
import javax.annotation.Nonnull
import javax.inject.Inject
import javax.swing.JTable
import com.muwire.core.Core
import com.muwire.core.search.UIResultEvent
import griffon.core.artifact.GriffonModel
import griffon.core.mvc.MVCGroup
import griffon.inject.MVCMember
import griffon.transform.Observable
import griffon.metadata.ArtifactProviderFor
@ArtifactProviderFor(GriffonModel)
class SearchTabModel {
@MVCMember @Nonnull
FactoryBuilderSupport builder
Core core
String uuid
def results = []
def hashBucket = [:]
void mvcGroupInit(Map<String, String> args) {
core = mvcGroup.parentGroup.model.core
mvcGroup.parentGroup.model.results[UUID.fromString(uuid)] = mvcGroup
}
void mvcGroupDestroy() {
mvcGroup.parentGroup.model.results.remove(uuid)
}
void handleResult(UIResultEvent e) {
runInsideUIAsync {
def bucket = hashBucket.get(e.infohash)
if (bucket == null) {
bucket = []
hashBucket[e.infohash] = bucket
}
bucket << e
results << e
JTable table = builder.getVariable("results-table")
table.model.fireTableDataChanged()
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 B

View File

@@ -3,15 +3,21 @@ package com.muwire.gui
import griffon.core.artifact.GriffonView import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor import griffon.metadata.ArtifactProviderFor
import net.i2p.data.DataHelper
import javax.swing.BorderFactory import javax.swing.BorderFactory
import javax.swing.Box import javax.swing.Box
import javax.swing.BoxLayout import javax.swing.BoxLayout
import javax.swing.JFileChooser import javax.swing.JFileChooser
import javax.swing.JLabel
import javax.swing.JSplitPane import javax.swing.JSplitPane
import javax.swing.ListSelectionModel
import javax.swing.SwingConstants import javax.swing.SwingConstants
import javax.swing.border.Border import javax.swing.border.Border
import javax.swing.table.DefaultTableCellRenderer
import com.muwire.core.Constants
import com.muwire.core.download.Downloader
import com.muwire.core.files.FileSharedEvent import com.muwire.core.files.FileSharedEvent
import java.awt.BorderLayout import java.awt.BorderLayout
@@ -20,6 +26,7 @@ import java.awt.FlowLayout
import java.awt.GridBagConstraints import java.awt.GridBagConstraints
import java.awt.GridBagLayout import java.awt.GridBagLayout
import java.awt.Insets import java.awt.Insets
import java.nio.charset.StandardCharsets
import javax.annotation.Nonnull import javax.annotation.Nonnull
@@ -41,6 +48,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()
@@ -48,15 +60,31 @@ class MainFrameView {
gridLayout(rows:1, cols: 2) gridLayout(rows:1, cols: 2)
button(text: "Searches", actionPerformed : showSearchWindow) button(text: "Searches", actionPerformed : showSearchWindow)
button(text: "Uploads", actionPerformed : showUploadsWindow) button(text: "Uploads", actionPerformed : showUploadsWindow)
button(text: "Monitor", actionPerformed : showMonitorWindow)
button(text: "Trust", actionPerformed : showTrustWindow)
} }
panel(constraints: BorderLayout.CENTER) { panel(id: "top-panel", constraints: BorderLayout.CENTER) {
borderLayout() cardLayout()
label("Enter search here:", constraints: BorderLayout.WEST) label(constraints : "top-connect-panel",
textField(id: "search-field", constraints: BorderLayout.CENTER, action : searchAction) 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.EAST) { panel(constraints: BorderLayout.CENTER) {
button(text: "Search", searchAction) borderLayout()
label(" Enter search here:", constraints: BorderLayout.WEST) // TODO: fix this
textField(id: "search-field", constraints: BorderLayout.CENTER, action : searchAction)
}
panel( constraints: BorderLayout.EAST) {
panel {
buttonGroup(id : "searchButtonGroup")
radioButton(text : "Keywords", selected : true, buttonGroup : searchButtonGroup, keywordSearchAction)
radioButton(text : "Hash", selected : false, buttonGroup : searchButtonGroup, hashSearchAction)
}
button(text: "Search", searchAction)
}
}
} }
} }
panel (id: "cards-panel", constraints : BorderLayout.CENTER) { panel (id: "cards-panel", constraints : BorderLayout.CENTER) {
@@ -67,22 +95,11 @@ class MainFrameView {
continuousLayout : true, constraints : BorderLayout.CENTER) { continuousLayout : true, constraints : BorderLayout.CENTER) {
panel (constraints : JSplitPane.TOP) { panel (constraints : JSplitPane.TOP) {
borderLayout() borderLayout()
scrollPane (constraints : BorderLayout.CENTER){ tabbedPane(id : "result-tabs", constraints: BorderLayout.CENTER)
table(id : "results-table") {
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: "Sender", type: String, read : {row -> row.sender.getHumanReadableName()})
closureColumn(header: "Trust", type: String, read : {row ->
model.core.trustService.getLevel(row.sender.destination)
})
}
}
}
panel(constraints : BorderLayout.SOUTH) { panel(constraints : BorderLayout.SOUTH) {
button(text : "Download", downloadAction) button(text : "Download", enabled : bind {model.searchButtonsEnabled}, downloadAction)
button(text : "Trust", trustAction) button(text : "Trust", enabled: bind {model.searchButtonsEnabled }, trustAction)
button(text : "Distrust", distrustAction) button(text : "Distrust", enabled : bind {model.searchButtonsEnabled}, distrustAction)
} }
} }
panel (constraints : JSplitPane.BOTTOM) { panel (constraints : JSplitPane.BOTTOM) {
@@ -90,21 +107,24 @@ class MainFrameView {
scrollPane (constraints : BorderLayout.CENTER) { scrollPane (constraints : BorderLayout.CENTER) {
table(id : "downloads-table") { table(id : "downloads-table") {
tableModel(list: model.downloads) { tableModel(list: model.downloads) {
closureColumn(header: "Name", type: String, read : {row -> row.downloader.file.getName()}) closureColumn(header: "Name", preferredWidth: 350, type: String, read : {row -> row.downloader.file.getName()})
closureColumn(header: "Status", type: String, read : {row -> row.downloader.getCurrentState()}) closureColumn(header: "Status", preferredWidth: 50, type: String, read : {row -> row.downloader.getCurrentState()})
closureColumn(header: "Progress", type: String, read: { row -> closureColumn(header: "Progress", preferredWidth: 20, type: String, read: { row ->
int pieces = row.downloader.nPieces int pieces = row.downloader.nPieces
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", preferredWidth : 10, type: Integer, read : {row -> row.downloader.activeWorkers()})
int position = row.downloader.positionInPiece() closureColumn(header: "Speed", preferredWidth: 50, type:String, read :{row ->
int pieceSize = row.downloader.pieceSize // TODO: fix for last piece DataHelper.formatSize2Decimal(row.downloader.speed(), false) + "B/sec"
"$position/$pieceSize bytes"
}) })
} }
} }
} }
panel (constraints : BorderLayout.SOUTH) {
button(text: "Cancel", enabled : bind {model.cancelButtonEnabled }, cancelAction )
button(text: "Retry", enabled : bind {model.retryButtonEnabled}, resumeAction)
}
} }
} }
} }
@@ -113,20 +133,23 @@ class MainFrameView {
panel { panel {
borderLayout() borderLayout()
panel (constraints : BorderLayout.NORTH) { panel (constraints : BorderLayout.NORTH) {
button(text : "Shared files", actionPerformed : shareFiles) button(text : "Click here to share files", actionPerformed : shareFiles)
} }
scrollPane ( constraints : BorderLayout.CENTER) { scrollPane ( constraints : BorderLayout.CENTER) {
table(id : "shared-files-table") { table(id : "shared-files-table") {
tableModel(list : model.shared) { tableModel(list : model.shared) {
closureColumn(header : "Name", type : String, read : {row -> row.file.getAbsolutePath()}) closureColumn(header : "Name", preferredWidth : 550, type : String, read : {row -> row.file.getAbsolutePath()})
closureColumn(header : "Size", type : Long, read : {row -> row.file.length()}) closureColumn(header : "Size", preferredWidth : 50, type : String,
read : {row -> DataHelper.formatSize2Decimal(row.file.length(),false) + "B"})
} }
} }
} }
} }
panel { panel {
borderLayout() borderLayout()
label("Uploads", constraints : BorderLayout.NORTH) panel (constraints : BorderLayout.NORTH){
label("Uploads")
}
scrollPane (constraints : BorderLayout.CENTER) { scrollPane (constraints : BorderLayout.CENTER) {
table(id : "uploads-table") { table(id : "uploads-table") {
tableModel(list : model.uploads) { tableModel(list : model.uploads) {
@@ -138,11 +161,86 @@ class MainFrameView {
int percent = (int)((position * 100.0) / total) int percent = (int)((position * 100.0) / total)
"$percent%" "$percent%"
}) })
closureColumn(header : "Downloader", type : String, read : { row ->
row.request.downloader?.getHumanReadableName()
})
} }
} }
} }
} }
} }
panel (constraints: "monitor window") {
gridLayout(rows : 1, cols : 2)
panel {
borderLayout()
panel (constraints : BorderLayout.NORTH){
label("Connections")
}
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "connections-table") {
tableModel(list : model.connectionList) {
closureColumn(header : "Destination", type: String, read : { row -> row.toBase32() })
}
}
}
}
panel {
borderLayout()
panel (constraints : BorderLayout.NORTH){
label("Incoming searches")
}
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "searches-table") {
tableModel(list : model.searches) {
closureColumn(header : "Keywords", type : String, read : {
sanitized = it.search.replace('<', ' ')
sanitized
})
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) { panel (border: etchedBorder(), constraints : BorderLayout.SOUTH) {
borderLayout() borderLayout()
@@ -156,6 +254,34 @@ class MainFrameView {
} }
} }
} }
void mvcGroupInit(Map<String, String> args) {
def downloadsTable = builder.getVariable("downloads-table")
def selectionModel = downloadsTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
selectionModel.addListSelectionListener({
int selectedRow = downloadsTable.getSelectedRow()
def downloader = model.downloads[selectedRow].downloader
switch(downloader.getCurrentState()) {
case Downloader.DownloadState.CONNECTING :
case Downloader.DownloadState.DOWNLOADING :
model.cancelButtonEnabled = true
model.retryButtonEnabled = false
break
case Downloader.DownloadState.FAILED:
model.cancelButtonEnabled = false
model.retryButtonEnabled = true
break
default:
model.cancelButtonEnabled = false
model.retryButtonEnabled = false
}
})
def centerRenderer = new DefaultTableCellRenderer()
centerRenderer.setHorizontalAlignment(JLabel.CENTER)
builder.getVariable("downloads-table").setDefaultRenderer(Integer.class, centerRenderer)
}
def showSearchWindow = { def showSearchWindow = {
def cardsPanel = builder.getVariable("cards-panel") def cardsPanel = builder.getVariable("cards-panel")
@@ -167,6 +293,16 @@ class MainFrameView {
cardsPanel.getLayout().show(cardsPanel, "uploads window") cardsPanel.getLayout().show(cardsPanel, "uploads window")
} }
def showMonitorWindow = {
def cardsPanel = builder.getVariable("cards-panel")
cardsPanel.getLayout().show(cardsPanel,"monitor window")
}
def showTrustWindow = {
def cardsPanel = builder.getVariable("cards-panel")
cardsPanel.getLayout().show(cardsPanel,"trust window")
}
def shareFiles = { def shareFiles = {
def chooser = new JFileChooser() def chooser = new JFileChooser()
chooser.setDialogTitle("Select file or directory to share") chooser.setDialogTitle("Select file or directory to share")

View File

@@ -0,0 +1,103 @@
package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.swing.JDialog
import javax.swing.JPanel
import javax.swing.JTabbedPane
import javax.swing.SwingConstants
import java.awt.BorderLayout
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 i
def retryField
def updateField
def allowUntrustedCheckbox
def shareDownloadedCheckbox
def inboundLengthField
def inboundQuantityField
def outboundLengthField
def outboundQuantityField
def buttonsPanel
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))
label(text : "Only allow trusted connections", constraints : gbc(gridx: 0, gridy : 2))
allowUntrustedCheckbox = checkBox(selected : bind {model.onlyTrusted}, constraints : gbc(gridx: 1, gridy : 2))
label(text : "Share downloaded files", constraints : gbc(gridx : 0, gridy:3))
shareDownloadedCheckbox = checkBox(selected : bind {model.shareDownloadedFiles}, constraints : gbc(gridx :1, gridy:3))
}
i = builder.panel {
gridBagLayout()
label(text : "Changing these settings requires a restart", constraints : gbc(gridx : 0, gridy : 0, gridwidth: 2))
label(text : "Inbound Length", constraints : gbc(gridx:0, gridy:1))
inboundLengthField = textField(text : bind {model.inboundLength}, columns : 2, constraints : gbc(gridx:1, gridy:1))
label(text : "Inbound Quantity", constraints : gbc(gridx:0, gridy:2))
inboundQuantityField = textField(text : bind {model.inboundQuantity}, columns : 2, constraints : gbc(gridx:1, gridy:2))
label(text : "Outbound Length", constraints : gbc(gridx:0, gridy:3))
outboundLengthField = textField(text : bind {model.outboundLength}, columns : 2, constraints : gbc(gridx:1, gridy:3))
label(text : "Outbound Quantity", constraints : gbc(gridx:0, gridy:4))
outboundQuantityField = textField(text : bind {model.outboundQuantity}, columns : 2, constraints : gbc(gridx:1, gridy:4))
}
buttonsPanel = builder.panel {
gridBagLayout()
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) {
def tabbedPane = new JTabbedPane()
tabbedPane.addTab("MuWire Options", p)
tabbedPane.addTab("I2P Options", i)
JPanel panel = new JPanel()
panel.setLayout(new BorderLayout())
panel.add(tabbedPane, BorderLayout.CENTER)
panel.add(buttonsPanel, BorderLayout.SOUTH)
d.getContentPane().add(panel)
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

@@ -0,0 +1,95 @@
package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.core.mvc.MVCGroup
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import net.i2p.data.DataHelper
import javax.swing.JLabel
import javax.swing.ListSelectionModel
import javax.swing.SwingConstants
import javax.swing.table.DefaultTableCellRenderer
import java.awt.BorderLayout
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class SearchTabView {
@MVCMember @Nonnull
FactoryBuilderSupport builder
@MVCMember @Nonnull
SearchTabModel model
def pane
def parent
def searchTerms
def resultsTable
void initUI() {
builder.with {
def resultsTable
def pane = scrollPane {
resultsTable = table(id : "results-table") {
tableModel(list: model.results) {
closureColumn(header: "Name", preferredWidth: 350, type: String, read : {row -> row.name.replace('<','_')})
closureColumn(header: "Size", preferredWidth: 50, type: String, read : {row -> DataHelper.formatSize2Decimal(row.size, false)+"B"})
closureColumn(header: "Sources", preferredWidth: 10, type : Integer, read : { row -> model.hashBucket[row.infohash].size()})
closureColumn(header: "Sender", preferredWidth: 170, type: String, read : {row -> row.sender.getHumanReadableName()})
closureColumn(header: "Trust", preferredWidth: 50, type: String, read : {row ->
model.core.trustService.getLevel(row.sender.destination)
})
}
}
}
this.pane = pane
this.pane.putClientProperty("mvc-group", mvcGroup)
this.pane.putClientProperty("results-table",resultsTable)
this.resultsTable = resultsTable
def selectionModel = resultsTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
selectionModel.addListSelectionListener( {
mvcGroup.parentGroup.model.searchButtonsEnabled = true
})
}
}
void mvcGroupInit(Map<String, String> args) {
searchTerms = args["search-terms"]
parent = mvcGroup.parentGroup.view.builder.getVariable("result-tabs")
parent.addTab(searchTerms, pane)
int index = parent.indexOfComponent(pane)
parent.setSelectedIndex(index)
def tabPanel
builder.with {
tabPanel = panel {
borderLayout()
panel {
label(text : searchTerms, constraints : BorderLayout.CENTER)
}
button(icon : imageIcon("/close_tab.png"), preferredSize : [20,20], constraints : BorderLayout.EAST, // TODO: in osx is probably WEST
actionPerformed : closeTab )
}
}
parent.setTabComponentAt(index, tabPanel)
def centerRenderer = new DefaultTableCellRenderer()
centerRenderer.setHorizontalAlignment(JLabel.CENTER)
resultsTable.columnModel.getColumn(1).setCellRenderer(centerRenderer)
resultsTable.setDefaultRenderer(Integer.class,centerRenderer)
resultsTable.columnModel.getColumn(4).setCellRenderer(centerRenderer)
}
def closeTab = {
int index = parent.indexOfTab(searchTerms)
parent.removeTabAt(index)
mvcGroup.parentGroup.model.searchButtonsEnabled = false
mvcGroup.destroy()
}
}

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,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 SearchTabIntegrationTest {
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

@@ -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(SearchTabController)
class SearchTabControllerTest {
private SearchTabController controller
@Rule
public final GriffonUnitRule griffon = new GriffonUnitRule()
@Test
void smokeTest() {
fail('Not yet implemented!')
}
}

View File

@@ -1,2 +1,3 @@
apply plugin : 'application' apply plugin : 'application'
mainClassName = 'com.muwire.hostcache.HostCache' mainClassName = 'com.muwire.hostcache.HostCache'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']

View File

@@ -1,9 +1,12 @@
package com.muwire.hostcache package com.muwire.hostcache
import java.util.logging.Level
import java.util.stream.Collectors import java.util.stream.Collectors
import groovy.util.logging.Log
import net.i2p.data.Destination import net.i2p.data.Destination
@Log
class Crawler { class Crawler {
final def pinger final def pinger
@@ -22,12 +25,14 @@ class Crawler {
synchronized def handleCrawlerPong(pong, Destination source) { synchronized def handleCrawlerPong(pong, Destination source) {
if (!inFlight.containsKey(source)) { if (!inFlight.containsKey(source)) {
log.info("response from host that hasn't been crawled")
return return
} }
Host host = inFlight.remove(source) Host host = inFlight.remove(source)
if (pong.uuid == null || pong.leafSlots == null || pong.peerSlots == null || pong.peers == null) { if (pong.uuid == null || pong.leafSlots == null || pong.peerSlots == null || pong.peers == null) {
hostPool.fail(host) hostPool.fail(host)
log.info("invalid crawler pong")
return return
} }
@@ -40,6 +45,7 @@ class Crawler {
} }
if (!uuid.equals(currentUUID)) { if (!uuid.equals(currentUUID)) {
log.info("uuid mismatch")
hostPool.fail(host) hostPool.fail(host)
return return
} }
@@ -50,7 +56,9 @@ class Crawler {
def peers def peers
try { try {
peers = pong.peers.stream().map({b64 -> new Destination(b64)}).collect(Collectors.toSet()) peers = pong.peers.stream().map({b64 -> new Destination(b64)}).collect(Collectors.toSet())
log.info("received ${peers.size()} peers")
} catch (Exception e) { } catch (Exception e) {
log.log(Level.WARNING,"couldn't parse peers", e)
hostPool.fail(host) hostPool.fail(host)
return return
} }

View File

@@ -1,9 +1,11 @@
package com.muwire.hostcache package com.muwire.hostcache
import java.util.logging.Level
import java.util.stream.Collectors import java.util.stream.Collectors
import groovy.json.JsonOutput import groovy.json.JsonOutput
import groovy.json.JsonSlurper import groovy.json.JsonSlurper
import groovy.util.logging.Log
import net.i2p.client.I2PClientFactory import net.i2p.client.I2PClientFactory
import net.i2p.client.I2PSession import net.i2p.client.I2PSession
import net.i2p.client.I2PSessionMuxedListener import net.i2p.client.I2PSessionMuxedListener
@@ -12,6 +14,7 @@ import net.i2p.client.datagram.I2PDatagramMaker
import net.i2p.util.SystemVersion import net.i2p.util.SystemVersion
import net.i2p.data.* import net.i2p.data.*
@Log
public class HostCache { public class HostCache {
public static void main(String[] args) { public static void main(String[] args) {
@@ -53,7 +56,7 @@ public class HostCache {
myDest = session.getMyDestination() myDest = session.getMyDestination()
// initialize hostpool and crawler // initialize hostpool and crawler
HostPool hostPool = new HostPool(3, 60 * 1000 * 1000) HostPool hostPool = new HostPool(3, 60 * 60 * 1000)
Pinger pinger = new Pinger(session) Pinger pinger = new Pinger(session)
Crawler crawler = new Crawler(pinger, hostPool, 5) Crawler crawler = new Crawler(pinger, hostPool, 5)
@@ -64,7 +67,7 @@ public class HostCache {
session.addMuxedSessionListener(new Listener(hostPool: hostPool, toReturn: 2, crawler: crawler), session.addMuxedSessionListener(new Listener(hostPool: hostPool, toReturn: 2, crawler: crawler),
I2PSession.PROTO_DATAGRAM, I2PSession.PORT_ANY) I2PSession.PROTO_DATAGRAM, I2PSession.PORT_ANY)
session.connect() session.connect()
println "INFO: connected, going to sleep" log.info("connected, going to sleep")
Thread.sleep(Integer.MAX_VALUE) Thread.sleep(Integer.MAX_VALUE)
} }
@@ -77,16 +80,16 @@ public class HostCache {
void reportAbuse(I2PSession sesison, int severity) {} void reportAbuse(I2PSession sesison, int severity) {}
void disconnected(I2PSession session) { void disconnected(I2PSession session) {
println "ERROR: session disconnected, exiting" log.severe("session disconnected, exiting")
System.exit(1) System.exit(1)
} }
void errorOccurred(I2PSession session, String message, Throwable error) { void errorOccurred(I2PSession session, String message, Throwable error) {
println "ERROR: ${message} ${error}" log.warning("${message} ${error}")
} }
void messageAvailable(I2PSession session, int msgId, long size, int proto, void messageAvailable(I2PSession session, int msgId, long size, int proto,
int fromport, int toport) { int fromport, int toport) {
if (proto != I2PSession.PROTO_DATAGRAM) { if (proto != I2PSession.PROTO_DATAGRAM) {
println "WARN: received unexpected protocol ${proto}" log.warning("received unexpected protocol ${proto}")
return return
} }
@@ -95,19 +98,19 @@ public class HostCache {
try { try {
dissector.loadI2PDatagram(payload) dissector.loadI2PDatagram(payload)
def sender = dissector.getSender() def sender = dissector.getSender()
println "INFO: Received something from ${sender.toBase32()}" def b32 = sender.toBase32()
payload = dissector.getPayload() payload = dissector.getPayload()
payload = json.parse(payload) payload = json.parse(payload)
if (payload.type == null) { if (payload.type == null) {
println "WARN: type field missing" log.warning("type field missing from $b32")
return return
} }
switch(payload.type) { switch(payload.type) {
case "Ping" : case "Ping" :
println "Ping" log.info("ping from $b32")
if (payload.leaf == null) { if (payload.leaf == null) {
println "WARN: ping didn't specify if leaf" log.warning("ping didn't specify if leaf from $b32")
return return
} }
payload.leaf = Boolean.parseBoolean(payload.leaf.toString()) payload.leaf = Boolean.parseBoolean(payload.leaf.toString())
@@ -116,14 +119,14 @@ public class HostCache {
respond(session, sender, payload) respond(session, sender, payload)
break break
case "CrawlerPong": case "CrawlerPong":
println "CrawlerPong" log.info("CrawlerPong from $b32")
crawler.handleCrawlerPong(payload, sender) crawler.handleCrawlerPong(payload, sender)
break break
default: default:
println "WARN: Unexpected message type ${payload.type}, dropping" log.warning("Unexpected message type ${payload.type}, dropping from $b32")
} }
} catch (Exception dfe) { } catch (Exception dfe) {
println "WARN: invalid datagram ${dfe}" log.log(Level.WARNING,"invalid datagram", dfe)
} }
} }
void messageAvailable(I2PSession session, int msgId, long size) { void messageAvailable(I2PSession session, int msgId, long size) {

View File

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

View File

@@ -0,0 +1,3 @@
apply plugin : 'application'
mainClassName = 'com.muwire.update.UpdateServer'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']

View File

@@ -0,0 +1,107 @@
package com.muwire.update
import java.util.logging.Level
import groovy.json.JsonSlurper
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
private final def slurper = new JsonSlurper()
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()
payload = slurper.parse(dissector.getPayload())
log.info("Got an update ping from "+sender.toBase32() + " reported version "+payload?.myVersion)
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)
}
}
}