Compare commits

..

1 Commits

Author SHA1 Message Date
Zlatin Balevsky
7718dc0821 force UTF8 for json serialization 2019-06-16 12:13:00 +01:00
299 changed files with 5085 additions and 36540 deletions

View File

@@ -4,38 +4,30 @@ MuWire is an easy to use file-sharing program which offers anonymity using [I2P
It is inspired by the LimeWire Gnutella client and developped by a former LimeWire developer. It is inspired by the LimeWire Gnutella client and developped by a former LimeWire developer.
The current stable release - 0.4.15 is avaiable for download at https://muwire.com. You can find technical documentation in the "doc" folder. The current stable release - 0.1.5 is avaiable for download at http://muwire.com. You can find technical documentation in the "doc" folder.
### Building ### Building
You need JDK 8 or newer. After installing that and setting up the appropriate paths, just type You need JRE 8 or newer. After installing that and setting up the appropriate paths, just type
``` ```
./gradlew clean assemble ./gradlew assemble
``` ```
If you want to run the unit tests, type If you want to run the unit tests, type
``` ```
./gradlew clean build ./gradlew build
``` ```
If you want to build binary bundles that do not depend on Java or I2P, see the https://github.com/zlatinb/muwire-pkg project Some of the UI tests will fail because they haven't been written yet :-/
### Running ### Running
After you build the application, look inside `gui/build/distributions`. Untar/unzip one of the `shadow` files and then run the jar contained inside by typing `java -jar gui-x.y.z.jar` in a terminal or command prompt. You need to have an I2P router up and running on the same machine. After you build the application, look inside "gui/build/distributions". Untar/unzip one of the "shadow" files and then run the jar contained inside by typing "java -jar MuWire-x.y.z.jar" in a terminal or command prompt. If you use a custom I2CP host and port, create a file $HOME/.MuWire/i2p.properties and put "i2cp.tcp.host=<host>" and "i2cp.tcp.post=<port>" in there.
If you have an I2P router running on the same machine that is all you need to do. If you use a custom I2CP host and port, create a file `i2p.properties` and put `i2cp.tcp.host=<host>` and `i2cp.tcp.port=<port>` in there. On Windows that file should go into `%HOME%\AppData\Roaming\MuWire`, on Mac into `$HOME/Library/Application Support/MuWire` and on Linux `$HOME/.MuWire` The first time you run MuWire it will ask you to select a nickname. This nickname will be displayed with search results, so that others can verify the file was shared by you. It is best to leave MuWire running all the time, just like I2P.
[Default I2CP port]\: `7654`
### GPG Fingerprint
```
471B 9FD4 5517 A5ED 101F C57D A728 3207 2D52 5E41
```
You can find the full key at https://keybase.io/zlatinb
[Default I2CP port]: https://geti2p.net/en/docs/ports ### Known bugs and limitations
* Many UI features you would expect are not there yet

26
TODO.md
View File

@@ -4,6 +4,10 @@ Not in any particular order yet
### Big Items ### Big Items
##### Alternate Locations
This helps peers discover new sources for a file while the download is in progress. Also makes sharing of partial files possible.
##### Bloom Filters ##### Bloom Filters
This reduces query traffic by not sending last hop queries to peers that definitely do not have the file This reduces query traffic by not sending last hop queries to peers that definitely do not have the file
@@ -12,15 +16,27 @@ This reduces query traffic by not sending last hop queries to peers that definit
This helps with scalability This helps with scalability
##### Trust List Sharing
For helping users make better decisions whom to trust
##### Content Control Panel
To allow every user to not route queries for content they do not like. This is mostly GUI work, the backend part is simple
##### Packaging With JRE, Embedded Router
For ease of deployment for new users, and so that users do not need to run a separate I2P router
##### Web UI, REST Interface, etc. ##### Web UI, REST Interface, etc.
Basically any non-gui non-cli user interface Basically any non-gui non-cli user interface
##### Metadata editing and search
To enable parsing of metadata from known file types and the user editing it or adding manual metadata
### Small Items ### Small Items
* Detect if router is dead and show warning or exit
* Wrapper of some kind for in-place upgrades * Wrapper of some kind for in-place upgrades
* Automatic adjustment of number of I2P tunnels * Download file sequentially
* Unsharing of files
* Multiple-selection download, Ctrl-A
* Automatic sharing of new files in shared directories (more like medium item)

View File

@@ -2,7 +2,7 @@ subprojects {
apply plugin: 'groovy' apply plugin: 'groovy'
dependencies { dependencies {
compile "net.i2p:i2p:${i2pVersion}" compile 'net.i2p:i2p:0.9.40'
compile 'org.codehaus.groovy:groovy-all:2.4.15' compile 'org.codehaus.groovy:groovy-all:2.4.15'
} }

View File

@@ -4,7 +4,6 @@ import java.util.concurrent.CountDownLatch
import com.muwire.core.Core import com.muwire.core.Core
import com.muwire.core.MuWireSettings import com.muwire.core.MuWireSettings
import com.muwire.core.UILoadedEvent
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
@@ -35,7 +34,7 @@ class Cli {
Core core Core core
try { try {
core = new Core(props, home, "0.5.2") core = new Core(props, home, "0.2.1")
} catch (Exception bad) { } catch (Exception bad) {
bad.printStackTrace(System.out) bad.printStackTrace(System.out)
println "Failed to initialize core, exiting" println "Failed to initialize core, exiting"
@@ -73,7 +72,7 @@ class Cli {
Timer timer = new Timer("status-printer", true) Timer timer = new Timer("status-printer", true)
timer.schedule({ timer.schedule({
println String.valueOf(new Date()) + " Connections $connectionsListener.connections Uploads $uploadsListener.uploads Shared $sharedListener.shared" println "Connections $connectionsListener.connections Uploads $uploadsListener.uploads Shared $sharedListener.shared"
} as TimerTask, 60000, 60000) } as TimerTask, 60000, 60000)
def latch = new CountDownLatch(1) def latch = new CountDownLatch(1)
@@ -85,7 +84,6 @@ class Cli {
core.eventBus.register(AllFilesLoadedEvent.class, fileLoader) core.eventBus.register(AllFilesLoadedEvent.class, fileLoader)
core.startServices() core.startServices()
core.eventBus.publish(new UILoadedEvent())
println "waiting for files to load" println "waiting for files to load"
latch.await() latch.await()
// now we begin // now we begin
@@ -119,11 +117,11 @@ class Cli {
volatile int uploads volatile int uploads
public void onUploadEvent(UploadEvent e) { public void onUploadEvent(UploadEvent e) {
uploads++ uploads++
println String.valueOf(new Date()) + " Starting upload of ${e.uploader.file.getName()} to ${e.uploader.request.downloader.getHumanReadableName()}" println "Starting upload of ${e.uploader.file.getName()} to ${e.uploader.request.downloader.getHumanReadableName()}"
} }
public void onUploadFinishedEvent(UploadFinishedEvent e) { public void onUploadFinishedEvent(UploadFinishedEvent e) {
uploads-- uploads--
println String.valueOf(new Date()) + " Finished upload of ${e.uploader.file.getName()} to ${e.uploader.request.downloader.getHumanReadableName()}" println "Finished upload of ${e.uploader.file.getName()} to ${e.uploader.request.downloader.getHumanReadableName()}"
} }
} }

View File

@@ -53,7 +53,7 @@ class CliDownloader {
Core core Core core
try { try {
core = new Core(props, home, "0.5.2") core = new Core(props, home, "0.2.1")
} catch (Exception bad) { } catch (Exception bad) {
bad.printStackTrace(System.out) bad.printStackTrace(System.out)
println "Failed to initialize core, exiting" println "Failed to initialize core, exiting"

View File

@@ -1,23 +0,0 @@
package com.muwire.cli
import com.muwire.core.util.DataUtil
import groovy.json.JsonSlurper
import net.i2p.data.Base64
class FileList {
public static void main(String [] args) {
if (args.length < 1) {
println "pass files.json as argument"
System.exit(1)
}
def slurper = new JsonSlurper()
File filesJson = new File(args[0])
filesJson.eachLine {
def json = slurper.parseText(it)
String name = DataUtil.readi18nString(Base64.decode(json.file))
println "$name,$json.length,$json.pieceSize,$json.infoHash"
}
}
}

View File

@@ -2,9 +2,8 @@ apply plugin : 'application'
mainClassName = 'com.muwire.core.Core' mainClassName = 'com.muwire.core.Core'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties'] applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
dependencies { dependencies {
compile "net.i2p:router:${i2pVersion}" compile 'net.i2p.client:mstreaming:0.9.40'
compile "net.i2p.client:mstreaming:${i2pVersion}" compile 'net.i2p.client:streaming:0.9.40'
compile "net.i2p.client:streaming:${i2pVersion}"
testCompile 'org.junit.jupiter:junit-jupiter-api:5.4.2' testCompile 'org.junit.jupiter:junit-jupiter-api:5.4.2'
testCompile 'junit:junit:4.12' testCompile 'junit:junit:4.12'

View File

@@ -0,0 +1,15 @@
package com.muwire.core
import net.i2p.crypto.SigType
class Constants {
public static final byte PERSONA_VERSION = (byte)1
public static final SigType SIG_TYPE = SigType.EdDSA_SHA512_Ed25519
public static final int MAX_HEADER_SIZE = 0x1 << 14
public static final int MAX_HEADERS = 16
public static final float DOWNLOAD_SEQUENTIAL_RATIO = 0.8f
public static final String SPLIT_PATTERN = "[\\.,_-]"
}

View File

@@ -1,7 +1,6 @@
package com.muwire.core package com.muwire.core
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.util.concurrent.atomic.AtomicBoolean
import com.muwire.core.connection.ConnectionAcceptor import com.muwire.core.connection.ConnectionAcceptor
import com.muwire.core.connection.ConnectionEstablisher import com.muwire.core.connection.ConnectionEstablisher
@@ -13,14 +12,10 @@ import com.muwire.core.connection.I2PConnector
import com.muwire.core.connection.LeafConnectionManager import com.muwire.core.connection.LeafConnectionManager
import com.muwire.core.connection.UltrapeerConnectionManager import com.muwire.core.connection.UltrapeerConnectionManager
import com.muwire.core.download.DownloadManager import com.muwire.core.download.DownloadManager
import com.muwire.core.download.SourceDiscoveredEvent
import com.muwire.core.download.UIDownloadCancelledEvent import com.muwire.core.download.UIDownloadCancelledEvent
import com.muwire.core.download.UIDownloadEvent import com.muwire.core.download.UIDownloadEvent
import com.muwire.core.download.UIDownloadPausedEvent
import com.muwire.core.download.UIDownloadResumedEvent
import com.muwire.core.files.FileDownloadedEvent import com.muwire.core.files.FileDownloadedEvent
import com.muwire.core.files.FileHashedEvent import com.muwire.core.files.FileHashedEvent
import com.muwire.core.files.FileHashingEvent
import com.muwire.core.files.FileHasher import com.muwire.core.files.FileHasher
import com.muwire.core.files.FileLoadedEvent import com.muwire.core.files.FileLoadedEvent
import com.muwire.core.files.FileManager import com.muwire.core.files.FileManager
@@ -28,32 +23,19 @@ import com.muwire.core.files.FileSharedEvent
import com.muwire.core.files.FileUnsharedEvent import com.muwire.core.files.FileUnsharedEvent
import com.muwire.core.files.HasherService import com.muwire.core.files.HasherService
import com.muwire.core.files.PersisterService import com.muwire.core.files.PersisterService
import com.muwire.core.files.UICommentEvent
import com.muwire.core.files.UIPersistFilesEvent
import com.muwire.core.files.AllFilesLoadedEvent
import com.muwire.core.files.DirectoryUnsharedEvent
import com.muwire.core.files.DirectoryWatcher
import com.muwire.core.hostcache.CacheClient import com.muwire.core.hostcache.CacheClient
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.mesh.MeshManager
import com.muwire.core.search.BrowseManager
import com.muwire.core.search.QueryEvent import com.muwire.core.search.QueryEvent
import com.muwire.core.search.ResultsEvent import com.muwire.core.search.ResultsEvent
import com.muwire.core.search.ResultsSender import com.muwire.core.search.ResultsSender
import com.muwire.core.search.SearchEvent import com.muwire.core.search.SearchEvent
import com.muwire.core.search.SearchManager import com.muwire.core.search.SearchManager
import com.muwire.core.search.UIBrowseEvent
import com.muwire.core.search.UIResultBatchEvent
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.trust.TrustSubscriber
import com.muwire.core.trust.TrustSubscriptionEvent
import com.muwire.core.update.UpdateClient 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
import com.muwire.core.content.ContentControlEvent
import com.muwire.core.content.ContentManager
import groovy.util.logging.Log import groovy.util.logging.Log
import net.i2p.I2PAppContext import net.i2p.I2PAppContext
@@ -62,7 +44,6 @@ import net.i2p.client.I2PSession
import net.i2p.client.streaming.I2PSocketManager import net.i2p.client.streaming.I2PSocketManager
import net.i2p.client.streaming.I2PSocketManagerFactory import net.i2p.client.streaming.I2PSocketManagerFactory
import net.i2p.client.streaming.I2PSocketOptions import net.i2p.client.streaming.I2PSocketOptions
import net.i2p.client.streaming.I2PSocketManager.DisconnectListener
import net.i2p.crypto.DSAEngine import net.i2p.crypto.DSAEngine
import net.i2p.crypto.SigType import net.i2p.crypto.SigType
import net.i2p.data.Destination import net.i2p.data.Destination
@@ -70,9 +51,6 @@ import net.i2p.data.PrivateKey
import net.i2p.data.Signature import net.i2p.data.Signature
import net.i2p.data.SigningPrivateKey import net.i2p.data.SigningPrivateKey
import net.i2p.router.Router
import net.i2p.router.RouterContext
@Log @Log
public class Core { public class Core {
@@ -83,7 +61,6 @@ public class Core {
final MuWireSettings muOptions final MuWireSettings muOptions
private final TrustService trustService private final TrustService trustService
private final TrustSubscriber trustSubscriber
private final PersisterService persisterService private final PersisterService persisterService
private final HostCache hostCache private final HostCache hostCache
private final ConnectionManager connectionManager private final ConnectionManager connectionManager
@@ -93,18 +70,23 @@ public class Core {
private final ConnectionEstablisher connectionEstablisher private final ConnectionEstablisher connectionEstablisher
private final HasherService hasherService private final HasherService hasherService
private final DownloadManager downloadManager private final DownloadManager downloadManager
private final DirectoryWatcher directoryWatcher
final FileManager fileManager
final UploadManager uploadManager
final ContentManager contentManager
private final Router router
final AtomicBoolean shutdown = new AtomicBoolean()
public Core(MuWireSettings props, File home, String myVersion) { public Core(MuWireSettings props, File home, String myVersion) {
this.home = home this.home = home
this.muOptions = props this.muOptions = props
log.info "Initializing I2P context"
I2PAppContext.getGlobalContext().logManager()
I2PAppContext.getGlobalContext()._logManager = new MuWireLogManager()
log.info("initializing I2P socket manager")
def i2pClient = new I2PClientFactory().createClient()
File keyDat = new File(home, "key.dat")
if (!keyDat.exists()) {
log.info("Creating new key.dat")
keyDat.withOutputStream {
i2pClient.createDestination(it, Constants.SIG_TYPE)
}
}
i2pOptions = new Properties() i2pOptions = new Properties()
def i2pOptionsFile = new File(home,"i2p.properties") def i2pOptionsFile = new File(home,"i2p.properties")
@@ -119,53 +101,13 @@ public class Core {
i2pOptions["inbound.nickname"] = "MuWire" i2pOptions["inbound.nickname"] = "MuWire"
i2pOptions["outbound.nickname"] = "MuWire" i2pOptions["outbound.nickname"] = "MuWire"
i2pOptions["inbound.length"] = "3" i2pOptions["inbound.length"] = "3"
i2pOptions["inbound.quantity"] = "4" i2pOptions["inbound.quantity"] = "2"
i2pOptions["outbound.length"] = "3" i2pOptions["outbound.length"] = "3"
i2pOptions["outbound.quantity"] = "4" i2pOptions["outbound.quantity"] = "2"
i2pOptions["i2cp.tcp.host"] = "127.0.0.1" i2pOptions["i2cp.tcp.host"] = "127.0.0.1"
i2pOptions["i2cp.tcp.port"] = "7654" i2pOptions["i2cp.tcp.port"] = "7654"
Random r = new Random()
int port = r.nextInt(60000) + 4000
i2pOptions["i2np.ntcp.port"] = String.valueOf(port)
i2pOptions["i2np.udp.port"] = String.valueOf(port)
i2pOptionsFile.withOutputStream { i2pOptions.store(it, "") }
} }
if (!props.embeddedRouter) {
log.info "Initializing I2P context"
I2PAppContext.getGlobalContext().logManager()
I2PAppContext.getGlobalContext()._logManager = new MuWireLogManager()
router = null
} else {
log.info("launching embedded router")
Properties routerProps = new Properties()
routerProps.setProperty("i2p.dir.base", home.getAbsolutePath())
routerProps.setProperty("i2p.dir.config", home.getAbsolutePath())
routerProps.setProperty("router.excludePeerCaps", "KLM")
routerProps.setProperty("i2np.inboundKBytesPerSecond", String.valueOf(props.inBw))
routerProps.setProperty("i2np.outboundKBytesPerSecond", String.valueOf(props.outBw))
routerProps.setProperty("i2cp.disableInterface", "true")
routerProps.setProperty("i2np.ntcp.port", i2pOptions["i2np.ntcp.port"])
routerProps.setProperty("i2np.udp.port", i2pOptions["i2np.udp.port"])
routerProps.setProperty("i2np.udp.internalPort", i2pOptions["i2np.udp.port"])
router = new Router(routerProps)
router.getContext().setLogManager(new MuWireLogManager())
router.runRouter()
while(!router.isRunning())
Thread.sleep(100)
}
log.info("initializing I2P socket manager")
def i2pClient = new I2PClientFactory().createClient()
File keyDat = new File(home, "key.dat")
if (!keyDat.exists()) {
log.info("Creating new key.dat")
keyDat.withOutputStream {
i2pClient.createDestination(it, Constants.SIG_TYPE)
}
}
// options like tunnel length and quantity // options like tunnel length and quantity
I2PSession i2pSession I2PSession i2pSession
I2PSocketManager socketManager I2PSocketManager socketManager
@@ -174,7 +116,6 @@ public class Core {
} }
socketManager.getDefaultOptions().setReadTimeout(60000) socketManager.getDefaultOptions().setReadTimeout(60000)
socketManager.getDefaultOptions().setConnectTimeout(30000) socketManager.getDefaultOptions().setConnectTimeout(30000)
socketManager.addDisconnectListener({eventBus.publish(new RouterDisconnectedEvent())} as DisconnectListener)
i2pSession = socketManager.getSession() i2pSession = socketManager.getSession()
def destination = new Destination() def destination = new Destination()
@@ -212,23 +153,15 @@ public class Core {
log.info "initializing file manager" log.info "initializing file manager"
fileManager = new FileManager(eventBus, props) 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)
eventBus.register(FileUnsharedEvent.class, fileManager) eventBus.register(FileUnsharedEvent.class, fileManager)
eventBus.register(SearchEvent.class, fileManager) eventBus.register(SearchEvent.class, fileManager)
eventBus.register(DirectoryUnsharedEvent.class, fileManager)
eventBus.register(UICommentEvent.class, fileManager)
log.info("initializing mesh manager")
MeshManager meshManager = new MeshManager(fileManager, home, props)
eventBus.register(SourceDiscoveredEvent.class, meshManager)
log.info "initializing persistence service" log.info "initializing persistence service"
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 60000, fileManager) persisterService = new PersisterService(new File(home, "files.json"), eventBus, 15000, fileManager)
eventBus.register(UILoadedEvent.class, persisterService)
eventBus.register(UIPersistFilesEvent.class, persisterService)
log.info("initializing host cache") log.info("initializing host cache")
File hostStorage = new File(home, "hosts.json") File hostStorage = new File(home, "hosts.json")
@@ -249,15 +182,13 @@ public class Core {
cacheClient = new CacheClient(eventBus,hostCache, connectionManager, i2pSession, props, 10000) cacheClient = new CacheClient(eventBus,hostCache, connectionManager, i2pSession, props, 10000)
log.info("initializing update client") log.info("initializing update client")
updateClient = new UpdateClient(eventBus, i2pSession, myVersion, props, fileManager, me) updateClient = new UpdateClient(eventBus, i2pSession, myVersion, props)
eventBus.register(FileDownloadedEvent.class, updateClient)
eventBus.register(UIResultBatchEvent.class, updateClient)
log.info("initializing connector") log.info("initializing connector")
I2PConnector i2pConnector = new I2PConnector(socketManager) I2PConnector i2pConnector = new I2PConnector(socketManager)
log.info "initializing results sender" log.info "initializing results sender"
ResultsSender resultsSender = new ResultsSender(eventBus, i2pConnector, me, props) ResultsSender resultsSender = new ResultsSender(eventBus, i2pConnector, me)
log.info "initializing search manager" log.info "initializing search manager"
SearchManager searchManager = new SearchManager(eventBus, me, resultsSender) SearchManager searchManager = new SearchManager(eventBus, me, resultsSender)
@@ -265,17 +196,14 @@ public class Core {
eventBus.register(ResultsEvent.class, searchManager) eventBus.register(ResultsEvent.class, searchManager)
log.info("initializing download manager") log.info("initializing download manager")
downloadManager = new DownloadManager(eventBus, trustService, meshManager, props, i2pConnector, home, me) downloadManager = new DownloadManager(eventBus, i2pConnector, home, me)
eventBus.register(UIDownloadEvent.class, downloadManager) eventBus.register(UIDownloadEvent.class, downloadManager)
eventBus.register(UILoadedEvent.class, downloadManager) eventBus.register(UILoadedEvent.class, downloadManager)
eventBus.register(FileDownloadedEvent.class, downloadManager) eventBus.register(FileDownloadedEvent.class, downloadManager)
eventBus.register(UIDownloadCancelledEvent.class, downloadManager) eventBus.register(UIDownloadCancelledEvent.class, downloadManager)
eventBus.register(SourceDiscoveredEvent.class, downloadManager)
eventBus.register(UIDownloadPausedEvent.class, downloadManager)
eventBus.register(UIDownloadResumedEvent.class, downloadManager)
log.info("initializing upload manager") log.info("initializing upload manager")
uploadManager = new UploadManager(eventBus, fileManager, meshManager, downloadManager) UploadManager uploadManager = new UploadManager(eventBus, fileManager)
log.info("initializing connection establisher") log.info("initializing connection establisher")
connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache) connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache)
@@ -283,40 +211,19 @@ public class Core {
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, fileManager, connectionEstablisher) i2pAcceptor, hostCache, trustService, searchManager, uploadManager, connectionEstablisher)
log.info("initializing directory watcher")
directoryWatcher = new DirectoryWatcher(eventBus, fileManager, home, props)
eventBus.register(FileSharedEvent.class, directoryWatcher)
eventBus.register(AllFilesLoadedEvent.class, directoryWatcher)
eventBus.register(DirectoryUnsharedEvent.class, directoryWatcher)
log.info("initializing hasher service") log.info("initializing hasher service")
hasherService = new HasherService(new FileHasher(), eventBus, fileManager, props) hasherService = new HasherService(new FileHasher(), eventBus, fileManager)
eventBus.register(FileSharedEvent.class, hasherService) eventBus.register(FileSharedEvent.class, hasherService)
eventBus.register(FileUnsharedEvent.class, hasherService)
eventBus.register(DirectoryUnsharedEvent.class, hasherService)
log.info("initializing trust subscriber")
trustSubscriber = new TrustSubscriber(eventBus, i2pConnector, props)
eventBus.register(UILoadedEvent.class, trustSubscriber)
eventBus.register(TrustSubscriptionEvent.class, trustSubscriber)
log.info("initializing content manager")
contentManager = new ContentManager()
eventBus.register(ContentControlEvent.class, contentManager)
eventBus.register(QueryEvent.class, contentManager)
log.info("initializing browse manager")
BrowseManager browseManager = new BrowseManager(i2pConnector, eventBus)
eventBus.register(UIBrowseEvent.class, browseManager)
} }
public void startServices() { public void startServices() {
hasherService.start() hasherService.start()
trustService.start() trustService.start()
trustService.waitForLoad() trustService.waitForLoad()
persisterService.start()
hostCache.start() hostCache.start()
connectionManager.start() connectionManager.start()
cacheClient.start() cacheClient.start()
@@ -327,28 +234,14 @@ public class Core {
} }
public void shutdown() { public void shutdown() {
if (!shutdown.compareAndSet(false, true)) {
log.info("already shutting down")
return
}
log.info("shutting down trust subscriber")
trustSubscriber.stop()
log.info("shutting down download manageer") log.info("shutting down download manageer")
downloadManager.shutdown() downloadManager.shutdown()
log.info("shutting down connection acceeptor") log.info("shutting down connection acceeptor")
connectionAcceptor.stop() connectionAcceptor.stop()
log.info("shutting down connection establisher") log.info("shutting down connection establisher")
connectionEstablisher.stop() connectionEstablisher.stop()
log.info("shutting down directory watcher")
directoryWatcher.stop()
log.info("shutting down cache client")
cacheClient.stop()
log.info("shutting down connection manager") log.info("shutting down connection manager")
connectionManager.shutdown() connectionManager.shutdown()
if (router != null) {
log.info("shutting down embedded router")
router.shutdown(0)
}
} }
static main(args) { static main(args) {
@@ -375,7 +268,7 @@ public class Core {
} }
} }
Core core = new Core(props, home, "0.5.2") Core core = new Core(props, home, "0.2.1")
core.startServices() core.startServices()
// ... at the end, sleep or execute script // ... at the end, sleep or execute script

View File

@@ -48,9 +48,4 @@ class EventBus {
} }
currentHandlers.add handler currentHandlers.add handler
} }
synchronized void unregister(Class<? extends Event> eventType, def handler) {
log.info("Unregistering $handler for type $eventType")
handlers[eventType]?.remove(handler)
}
} }

View File

@@ -1,42 +1,19 @@
package com.muwire.core package com.muwire.core
import java.util.stream.Collectors
import com.muwire.core.hostcache.CrawlerResponse import com.muwire.core.hostcache.CrawlerResponse
import com.muwire.core.util.DataUtil
import net.i2p.data.Base64
import net.i2p.util.ConcurrentHashSet
class MuWireSettings { class MuWireSettings {
final boolean isLeaf final boolean isLeaf
boolean allowUntrusted boolean allowUntrusted
boolean searchExtraHop
boolean allowTrustLists
int trustListInterval
Set<Persona> trustSubscriptions
int downloadRetryInterval int downloadRetryInterval
int updateCheckInterval int updateCheckInterval
boolean autoDownloadUpdate
String updateType
String nickname String nickname
File downloadLocation File downloadLocation
File incompleteLocation String sharedFiles
CrawlerResponse crawlerResponse CrawlerResponse crawlerResponse
boolean shareDownloadedFiles boolean shareDownloadedFiles
boolean shareHiddenFiles boolean watchSharedDirectories
boolean searchComments
boolean browseFiles
Set<String> watchedDirectories
float downloadSequentialRatio
int hostClearInterval, hostHopelessInterval, hostRejectInterval
int meshExpiration
int speedSmoothSeconds
boolean embeddedRouter
int inBw, outBw
Set<String> watchedKeywords
Set<String> watchedRegexes
MuWireSettings() { MuWireSettings() {
this(new Properties()) this(new Properties())
@@ -44,111 +21,34 @@ class MuWireSettings {
MuWireSettings(Properties props) { MuWireSettings(Properties props) {
isLeaf = Boolean.valueOf(props.get("leaf","false")) isLeaf = Boolean.valueOf(props.get("leaf","false"))
allowUntrusted = Boolean.valueOf(props.getProperty("allowUntrusted","true")) allowUntrusted = Boolean.valueOf(props.get("allowUntrusted","true"))
searchExtraHop = Boolean.valueOf(props.getProperty("searchExtraHop","false"))
allowTrustLists = Boolean.valueOf(props.getProperty("allowTrustLists","true"))
trustListInterval = Integer.valueOf(props.getProperty("trustListInterval","1"))
crawlerResponse = CrawlerResponse.valueOf(props.get("crawlerResponse","REGISTERED")) crawlerResponse = CrawlerResponse.valueOf(props.get("crawlerResponse","REGISTERED"))
nickname = props.getProperty("nickname","MuWireUser") nickname = props.getProperty("nickname","MuWireUser")
downloadLocation = new File((String)props.getProperty("downloadLocation", downloadLocation = new File((String)props.getProperty("downloadLocation",
System.getProperty("user.home"))) System.getProperty("user.home")))
String incompleteLocationProp = props.getProperty("incompleteLocation") sharedFiles = props.getProperty("sharedFiles")
if (incompleteLocationProp != null) downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","15"))
incompleteLocation = new File(incompleteLocationProp) updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","36"))
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","60"))
updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","24"))
autoDownloadUpdate = Boolean.parseBoolean(props.getProperty("autoDownloadUpdate","true"))
updateType = props.getProperty("updateType","jar")
shareDownloadedFiles = Boolean.parseBoolean(props.getProperty("shareDownloadedFiles","true")) shareDownloadedFiles = Boolean.parseBoolean(props.getProperty("shareDownloadedFiles","true"))
shareHiddenFiles = Boolean.parseBoolean(props.getProperty("shareHiddenFiles","false")) watchSharedDirectories = Boolean.parseBoolean(props.getProperty("watchSharedDirectories","true"))
downloadSequentialRatio = Float.valueOf(props.getProperty("downloadSequentialRatio","0.8"))
hostClearInterval = Integer.valueOf(props.getProperty("hostClearInterval","15"))
hostHopelessInterval = Integer.valueOf(props.getProperty("hostHopelessInterval", "1440"))
hostRejectInterval = Integer.valueOf(props.getProperty("hostRejectInterval", "1"))
meshExpiration = Integer.valueOf(props.getProperty("meshExpiration","60"))
embeddedRouter = Boolean.valueOf(props.getProperty("embeddedRouter","false"))
inBw = Integer.valueOf(props.getProperty("inBw","256"))
outBw = Integer.valueOf(props.getProperty("outBw","128"))
searchComments = Boolean.valueOf(props.getProperty("searchComments","true"))
browseFiles = Boolean.valueOf(props.getProperty("browseFiles","true"))
speedSmoothSeconds = Integer.valueOf(props.getProperty("speedSmoothSeconds","60"))
watchedDirectories = readEncodedSet(props, "watchedDirectories")
watchedKeywords = readEncodedSet(props, "watchedKeywords")
watchedRegexes = readEncodedSet(props, "watchedRegexes")
trustSubscriptions = new HashSet<>()
if (props.containsKey("trustSubscriptions")) {
props.getProperty("trustSubscriptions").split(",").each {
trustSubscriptions.add(new Persona(new ByteArrayInputStream(Base64.decode(it))))
}
}
} }
void write(OutputStream out) throws IOException { void write(OutputStream out) throws IOException {
Properties props = new Properties() Properties props = new Properties()
props.setProperty("leaf", isLeaf.toString()) props.setProperty("leaf", isLeaf.toString())
props.setProperty("allowUntrusted", allowUntrusted.toString()) props.setProperty("allowUntrusted", allowUntrusted.toString())
props.setProperty("searchExtraHop", String.valueOf(searchExtraHop))
props.setProperty("allowTrustLists", String.valueOf(allowTrustLists))
props.setProperty("trustListInterval", String.valueOf(trustListInterval))
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())
if (incompleteLocation != null)
props.setProperty("incompleteLocation", incompleteLocation.getAbsolutePath())
props.setProperty("downloadRetryInterval", String.valueOf(downloadRetryInterval)) props.setProperty("downloadRetryInterval", String.valueOf(downloadRetryInterval))
props.setProperty("updateCheckInterval", String.valueOf(updateCheckInterval)) props.setProperty("updateCheckInterval", String.valueOf(updateCheckInterval))
props.setProperty("autoDownloadUpdate", String.valueOf(autoDownloadUpdate))
props.setProperty("updateType",String.valueOf(updateType))
props.setProperty("shareDownloadedFiles", String.valueOf(shareDownloadedFiles)) props.setProperty("shareDownloadedFiles", String.valueOf(shareDownloadedFiles))
props.setProperty("shareHiddenFiles", String.valueOf(shareHiddenFiles)) props.setProperty("watchSharedDirectories", String.valueOf(watchSharedDirectories))
props.setProperty("downloadSequentialRatio", String.valueOf(downloadSequentialRatio)) if (sharedFiles != null)
props.setProperty("hostClearInterval", String.valueOf(hostClearInterval)) props.setProperty("sharedFiles", sharedFiles)
props.setProperty("hostHopelessInterval", String.valueOf(hostHopelessInterval))
props.setProperty("hostRejectInterval", String.valueOf(hostRejectInterval))
props.setProperty("meshExpiration", String.valueOf(meshExpiration))
props.setProperty("embeddedRouter", String.valueOf(embeddedRouter))
props.setProperty("inBw", String.valueOf(inBw))
props.setProperty("outBw", String.valueOf(outBw))
props.setProperty("searchComments", String.valueOf(searchComments))
props.setProperty("browseFiles", String.valueOf(browseFiles))
props.setProperty("speedSmoothSeconds", String.valueOf(speedSmoothSeconds))
writeEncodedSet(watchedDirectories, "watchedDirectories", props)
writeEncodedSet(watchedKeywords, "watchedKeywords", props)
writeEncodedSet(watchedRegexes, "watchedRegexes", props)
if (!trustSubscriptions.isEmpty()) {
String encoded = trustSubscriptions.stream().
map({it.toBase64()}).
collect(Collectors.joining(","))
props.setProperty("trustSubscriptions", encoded)
}
props.store(out, "") props.store(out, "")
} }
private static Set<String> readEncodedSet(Properties props, String property) {
Set<String> rv = new ConcurrentHashSet<>()
if (props.containsKey(property)) {
String[] encoded = props.getProperty(property).split(",")
encoded.each { rv << DataUtil.readi18nString(Base64.decode(it)) }
}
rv
}
private static void writeEncodedSet(Set<String> set, String property, Properties props) {
if (set.isEmpty())
return
String encoded = set.stream().
map({Base64.encode(DataUtil.encodei18nString(it))}).
collect(Collectors.joining(","))
props.setProperty(property, encoded)
}
boolean isLeaf() { boolean isLeaf() {
isLeaf isLeaf
} }

View File

@@ -82,13 +82,4 @@ public class Persona {
Persona other = (Persona)o Persona other = (Persona)o
name.equals(other.name) && destination.equals(other.destination) name.equals(other.name) && destination.equals(other.destination)
} }
public static void main(String []args) {
if (args.length != 1) {
println "This utility decodes a bas64-encoded persona"
System.exit(1)
}
Persona p = new Persona(new ByteArrayInputStream(Base64.decode(args[0])))
println p.getHumanReadableName()
}
} }

View File

@@ -1,4 +0,0 @@
package com.muwire.core
class RouterDisconnectedEvent extends Event {
}

View File

@@ -1,7 +0,0 @@
package com.muwire.core
class SplitPattern {
public static final String SPLIT_PATTERN = "[\\*\\+\\-,\\.:;\\(\\)=_/\\\\\\!\\\"\\\'\\\$%\\|\\[\\]\\{\\}\\?]";
}

View File

@@ -22,9 +22,6 @@ import net.i2p.data.Destination
@Log @Log
abstract class Connection implements Closeable { abstract class Connection implements Closeable {
private static final int SEARCHES = 10
private static final long INTERVAL = 1000
final EventBus eventBus final EventBus eventBus
final Endpoint endpoint final Endpoint endpoint
final boolean incoming final boolean incoming
@@ -35,7 +32,6 @@ abstract class Connection implements Closeable {
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
private final LinkedList<Long> searchTimestamps = new LinkedList<>()
protected final String name protected final String name
@@ -132,8 +128,6 @@ abstract class Connection implements Closeable {
query.firstHop = e.firstHop query.firstHop = e.firstHop
query.keywords = e.searchEvent.getSearchTerms() query.keywords = e.searchEvent.getSearchTerms()
query.oobInfohash = e.searchEvent.oobInfohash query.oobInfohash = e.searchEvent.oobInfohash
query.searchComments = e.searchEvent.searchComments
query.compressedResults = e.searchEvent.compressedResults
if (e.searchEvent.searchHash != null) if (e.searchEvent.searchHash != null)
query.infohash = Base64.encode(e.searchEvent.searchHash) query.infohash = Base64.encode(e.searchEvent.searchHash)
query.replyTo = e.replyTo.toBase64() query.replyTo = e.replyTo.toBase64()
@@ -162,25 +156,7 @@ abstract class Connection implements Closeable {
} }
} }
private boolean throttleSearch() {
final long now = System.currentTimeMillis()
if (searchTimestamps.size() < SEARCHES) {
searchTimestamps.addLast(now)
return false
}
Long oldest = searchTimestamps.getFirst()
if (now - oldest.longValue() < INTERVAL)
return true
searchTimestamps.addLast(now)
searchTimestamps.removeFirst()
false
}
protected void handleSearch(def search) { protected void handleSearch(def search) {
if (throttleSearch()) {
log.info("dropping excessive search")
return
}
UUID uuid = UUID.fromString(search.uuid) UUID uuid = UUID.fromString(search.uuid)
byte [] infohash = null byte [] infohash = null
if (search.infohash != null) { if (search.infohash != null) {
@@ -211,19 +187,11 @@ abstract class Connection implements Closeable {
boolean oob = false boolean oob = false
if (search.oobInfohash != null) if (search.oobInfohash != null)
oob = search.oobInfohash oob = search.oobInfohash
boolean searchComments = false
if (search.searchComments != null)
searchComments = search.searchComments
boolean compressedResults = false
if (search.compressedResults != null)
compressedResults = search.compressedResults
SearchEvent searchEvent = new SearchEvent(searchTerms : search.keywords, SearchEvent searchEvent = new SearchEvent(searchTerms : search.keywords,
searchHash : infohash, searchHash : infohash,
uuid : uuid, uuid : uuid,
oobInfohash : oob, oobInfohash : oob)
searchComments : searchComments,
compressedResults : compressedResults)
QueryEvent event = new QueryEvent ( searchEvent : searchEvent, QueryEvent event = new QueryEvent ( searchEvent : searchEvent,
replyTo : replyTo, replyTo : replyTo,
originator : originator, originator : originator,

View File

@@ -5,32 +5,23 @@ import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.logging.Level import java.util.logging.Level
import java.util.zip.DeflaterOutputStream import java.util.zip.DeflaterOutputStream
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
import java.util.zip.InflaterInputStream import java.util.zip.InflaterInputStream
import com.muwire.core.Constants
import com.muwire.core.EventBus import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings import com.muwire.core.MuWireSettings
import com.muwire.core.Persona import com.muwire.core.Persona
import com.muwire.core.files.FileManager
import com.muwire.core.hostcache.HostCache import com.muwire.core.hostcache.HostCache
import com.muwire.core.trust.TrustLevel import com.muwire.core.trust.TrustLevel
import com.muwire.core.trust.TrustService import com.muwire.core.trust.TrustService
import com.muwire.core.upload.UploadManager import com.muwire.core.upload.UploadManager
import com.muwire.core.util.DataUtil
import com.muwire.core.search.InvalidSearchResultException import com.muwire.core.search.InvalidSearchResultException
import com.muwire.core.search.ResultsParser import com.muwire.core.search.ResultsParser
import com.muwire.core.search.ResultsSender
import com.muwire.core.search.SearchManager import com.muwire.core.search.SearchManager
import com.muwire.core.search.UIResultBatchEvent
import com.muwire.core.search.UIResultEvent
import com.muwire.core.search.UnexpectedResultsException import com.muwire.core.search.UnexpectedResultsException
import groovy.json.JsonOutput import groovy.json.JsonOutput
import groovy.json.JsonSlurper import groovy.json.JsonSlurper
import groovy.util.logging.Log import groovy.util.logging.Log
import net.i2p.data.Base64
@Log @Log
class ConnectionAcceptor { class ConnectionAcceptor {
@@ -43,7 +34,6 @@ class ConnectionAcceptor {
final TrustService trustService final TrustService trustService
final SearchManager searchManager final SearchManager searchManager
final UploadManager uploadManager final UploadManager uploadManager
final FileManager fileManager
final ConnectionEstablisher establisher final ConnectionEstablisher establisher
final ExecutorService acceptorThread final ExecutorService acceptorThread
@@ -54,7 +44,7 @@ class ConnectionAcceptor {
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,
FileManager fileManager, ConnectionEstablisher establisher) { ConnectionEstablisher establisher) {
this.eventBus = eventBus this.eventBus = eventBus
this.manager = manager this.manager = manager
this.settings = settings this.settings = settings
@@ -62,7 +52,6 @@ class ConnectionAcceptor {
this.hostCache = hostCache this.hostCache = hostCache
this.trustService = trustService this.trustService = trustService
this.searchManager = searchManager this.searchManager = searchManager
this.fileManager = fileManager
this.uploadManager = uploadManager this.uploadManager = uploadManager
this.establisher = establisher this.establisher = establisher
@@ -134,15 +123,6 @@ class ConnectionAcceptor {
case (byte)'P': case (byte)'P':
processPOST(e) processPOST(e)
break break
case (byte)'R':
processRESULTS(e)
break
case (byte)'T':
processTRUST(e)
break
case (byte)'B':
processBROWSE(e)
break
default: default:
throw new Exception("Invalid read $read") throw new Exception("Invalid read $read")
} }
@@ -243,17 +223,15 @@ class ConnectionAcceptor {
Persona sender = new Persona(dis) Persona sender = new Persona(dis)
if (sender.destination != e.getDestination()) if (sender.destination != e.getDestination())
throw new IOException("Sender destination mismatch expected ${e.getDestination()}, got $sender.destination") throw new IOException("Sender destination mismatch expected $e.getDestination(), got $sender.destination")
int nResults = dis.readUnsignedShort() int nResults = dis.readUnsignedShort()
UIResultEvent[] results = new UIResultEvent[nResults]
for (int i = 0; i < nResults; i++) { for (int i = 0; i < nResults; i++) {
int jsonSize = dis.readUnsignedShort() int jsonSize = dis.readUnsignedShort()
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)
results[i] = ResultsParser.parse(sender, resultsUUID, json) eventBus.publish(ResultsParser.parse(sender, resultsUUID, json))
} }
eventBus.publish(new UIResultBatchEvent(uuid: resultsUUID, results: results))
} 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)
} finally { } finally {
@@ -261,146 +239,4 @@ class ConnectionAcceptor {
} }
} }
private void processRESULTS(Endpoint e) {
InputStream is = e.getInputStream()
DataInputStream dis = new DataInputStream(is)
byte[] esults = new byte[7]
dis.readFully(esults)
if (esults != "ESULTS ".getBytes(StandardCharsets.US_ASCII))
throw new IOException("Invalid RESULTS connection")
JsonSlurper slurper = new JsonSlurper()
try {
String uuid = DataUtil.readTillRN(dis)
UUID resultsUUID = UUID.fromString(uuid)
if (!searchManager.hasLocalSearch(resultsUUID))
throw new UnexpectedResultsException(resultsUUID.toString())
// parse all headers
Map<String,String> headers = new HashMap<>()
String header
while((header = DataUtil.readTillRN(is)) != "" && headers.size() < Constants.MAX_HEADERS) {
int colon = header.indexOf(':')
if (colon == -1 || colon == header.length() - 1)
throw new IOException("invalid header $header")
String key = header.substring(0, colon)
String value = header.substring(colon + 1)
headers[key] = value.trim()
}
if (!headers.containsKey("Sender"))
throw new IOException("No Sender header")
if (!headers.containsKey("Count"))
throw new IOException("No Count header")
byte [] personaBytes = Base64.decode(headers['Sender'])
Persona sender = new Persona(new ByteArrayInputStream(personaBytes))
if (sender.destination != e.getDestination())
throw new IOException("Sender destination mismatch expected ${e.getDestination()}, got $sender.destination")
int nResults = Integer.parseInt(headers['Count'])
if (nResults > Constants.MAX_RESULTS)
throw new IOException("too many results $nResults")
dis = new DataInputStream(new GZIPInputStream(dis))
UIResultEvent[] results = new UIResultEvent[nResults]
for (int i = 0; i < nResults; i++) {
int jsonSize = dis.readUnsignedShort()
byte [] payload = new byte[jsonSize]
dis.readFully(payload)
def json = slurper.parse(payload)
results[i] = ResultsParser.parse(sender, resultsUUID, json)
}
eventBus.publish(new UIResultBatchEvent(uuid: resultsUUID, results: results))
} catch (IOException bad) {
log.log(Level.WARNING, "failed to process RESULTS", bad)
} finally {
e.close()
}
}
private void processBROWSE(Endpoint e) {
try {
byte [] rowse = new byte[7]
DataInputStream dis = new DataInputStream(e.getInputStream())
dis.readFully(rowse)
if (rowse != "ROWSE\r\n".getBytes(StandardCharsets.US_ASCII))
throw new IOException("Invalid BROWSE connection")
String header
while ((header = DataUtil.readTillRN(dis)) != ""); // ignore headers for now
OutputStream os = e.getOutputStream()
if (!settings.browseFiles) {
os.write("403 Not Allowed\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
os.flush()
e.close()
return
}
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
def sharedFiles = fileManager.getSharedFiles().values()
os.write("Count: ${sharedFiles.size()}\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))
JsonOutput jsonOutput = new JsonOutput()
sharedFiles.each {
def obj = ResultsSender.sharedFileToObj(it, false)
def json = jsonOutput.toJson(obj)
dos.writeShort((short)json.length())
dos.write(json.getBytes(StandardCharsets.US_ASCII))
}
dos.flush()
dos.close()
} finally {
e.close()
}
}
private void processTRUST(Endpoint e) {
try {
byte[] RUST = new byte[6]
DataInputStream dis = new DataInputStream(e.getInputStream())
dis.readFully(RUST)
if (RUST != "RUST\r\n".getBytes(StandardCharsets.US_ASCII))
throw new IOException("Invalid TRUST connection")
String header
while ((header = DataUtil.readTillRN(dis)) != ""); // ignore headers for now
OutputStream os = e.getOutputStream()
if (!settings.allowTrustLists) {
os.write("403 Not Allowed\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
os.flush()
e.close()
return
}
os.write("200 OK\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
List<Persona> good = new ArrayList<>(trustService.good.values())
int size = Math.min(Short.MAX_VALUE * 2, good.size())
good = good.subList(0, size)
DataOutputStream dos = new DataOutputStream(os)
dos.writeShort(size)
good.each {
it.write(dos)
}
List<Persona> bad = new ArrayList<>(trustService.bad.values())
size = Math.min(Short.MAX_VALUE * 2, bad.size())
bad = bad.subList(0, size)
dos.writeShort(size)
bad.each {
it.write(dos)
}
dos.flush()
} finally {
e.close()
}
}
} }

View File

@@ -31,7 +31,7 @@ class ConnectionEstablisher {
final HostCache hostCache final HostCache hostCache
final Timer timer final Timer timer
final ExecutorService executor, closer final ExecutorService executor
final Set inProgress = new ConcurrentHashSet() final Set inProgress = new ConcurrentHashSet()
@@ -51,8 +51,6 @@ class ConnectionEstablisher {
rv.setName("connector-${System.currentTimeMillis()}") rv.setName("connector-${System.currentTimeMillis()}")
rv rv
} as ThreadFactory) } as ThreadFactory)
closer = Executors.newSingleThreadExecutor()
} }
void start() { void start() {
@@ -62,7 +60,6 @@ class ConnectionEstablisher {
void stop() { void stop() {
timer.cancel() timer.cancel()
executor.shutdownNow() executor.shutdownNow()
closer.shutdown()
} }
private void connectIfNeeded() { private void connectIfNeeded() {
@@ -123,10 +120,8 @@ class ConnectionEstablisher {
} }
private void fail(Endpoint endpoint) { private void fail(Endpoint endpoint) {
closer.execute {
endpoint.close() endpoint.close()
eventBus.publish(new ConnectionEvent(endpoint: endpoint, incoming: false, leaf: false, status: ConnectionAttemptStatus.FAILED)) eventBus.publish(new ConnectionEvent(endpoint: endpoint, incoming: false, leaf: false, status: ConnectionAttemptStatus.FAILED))
} as Runnable
} }
private void readK(Endpoint e) { private void readK(Endpoint e) {
@@ -180,7 +175,7 @@ class ConnectionEstablisher {
log.log(Level.WARNING,"Problem parsing post-rejection payload",ignore) log.log(Level.WARNING,"Problem parsing post-rejection payload",ignore)
} finally { } finally {
// the end // the end
closer.execute({e.close()} as Runnable) e.close()
} }
} }

View File

@@ -2,6 +2,7 @@ package com.muwire.core.connection
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import java.nio.charset.StandardCharsets
import com.muwire.core.EventBus import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings import com.muwire.core.MuWireSettings
@@ -66,7 +67,7 @@ class PeerConnection extends Connection {
protected void write(Object message) { protected void write(Object message) {
byte[] payload byte[] payload
if (message instanceof Map) { if (message instanceof Map) {
payload = JsonOutput.toJson(message).bytes payload = JsonOutput.toJson(message).getBytes(StandardCharsets.UTF_8)
DataUtil.packHeader(payload.length, writeHeader) DataUtil.packHeader(payload.length, writeHeader)
log.fine "$name writing message type ${message.type} length $payload.length" log.fine "$name writing message type ${message.type} length $payload.length"
writeHeader[0] &= (byte)0x7F writeHeader[0] &= (byte)0x7F

View File

@@ -1,9 +0,0 @@
package com.muwire.core.content
import com.muwire.core.Event
class ContentControlEvent extends Event {
String term
boolean regex
boolean add
}

View File

@@ -1,30 +0,0 @@
package com.muwire.core.content
import java.util.concurrent.ConcurrentHashMap
import com.muwire.core.search.QueryEvent
import net.i2p.util.ConcurrentHashSet
class ContentManager {
Set<Matcher> matchers = new ConcurrentHashSet()
void onContentControlEvent(ContentControlEvent e) {
Matcher m
if (e.regex)
m = new RegexMatcher(e.term)
else
m = new KeywordMatcher(e.term)
if (e.add)
matchers.add(m)
else
matchers.remove(m)
}
void onQueryEvent(QueryEvent e) {
if (e.searchEvent.searchTerms == null)
return
matchers.each { it.process(e) }
}
}

View File

@@ -1,36 +0,0 @@
package com.muwire.core.content
class KeywordMatcher extends Matcher {
private final String keyword
KeywordMatcher(String keyword) {
this.keyword = keyword
}
@Override
protected boolean match(List<String> searchTerms) {
boolean found = false
searchTerms.each {
if (keyword == it)
found = true
}
found
}
@Override
public String getTerm() {
keyword
}
@Override
public int hashCode() {
keyword.hashCode()
}
@Override
public boolean equals(Object o) {
if (!(o instanceof KeywordMatcher))
return false
KeywordMatcher other = (KeywordMatcher) o
keyword.equals(other.keyword)
}
}

View File

@@ -1,9 +0,0 @@
package com.muwire.core.content
import com.muwire.core.Persona
class Match {
Persona persona
String [] keywords
long timestamp
}

View File

@@ -1,20 +0,0 @@
package com.muwire.core.content
import com.muwire.core.search.QueryEvent
abstract class Matcher {
final List<Match> matches = Collections.synchronizedList(new ArrayList<>())
final Set<UUID> uuids = new HashSet<>()
protected abstract boolean match(List<String> searchTerms);
public abstract String getTerm();
public void process(QueryEvent qe) {
def terms = qe.searchEvent.searchTerms
if (match(terms) && uuids.add(qe.searchEvent.uuid)) {
long now = System.currentTimeMillis()
matches << new Match(persona : qe.originator, keywords : terms, timestamp : now)
}
}
}

View File

@@ -1,35 +0,0 @@
package com.muwire.core.content
import java.util.regex.Pattern
import java.util.stream.Collectors
class RegexMatcher extends Matcher {
private final Pattern pattern
RegexMatcher(String pattern) {
this.pattern = Pattern.compile(pattern)
}
@Override
protected boolean match(List<String> keywords) {
String combined = keywords.join(" ")
return pattern.matcher(combined).find()
}
@Override
public String getTerm() {
pattern.pattern()
}
@Override
public int hashCode() {
pattern.pattern().hashCode()
}
@Override
public boolean equals(Object o) {
if (!(o instanceof RegexMatcher))
return false
RegexMatcher other = (RegexMatcher) o
pattern.pattern() == other.pattern.pattern()
}
}

View File

@@ -3,10 +3,6 @@ package com.muwire.core.download
import com.muwire.core.connection.I2PConnector import com.muwire.core.connection.I2PConnector
import com.muwire.core.files.FileDownloadedEvent import com.muwire.core.files.FileDownloadedEvent
import com.muwire.core.files.FileHasher import com.muwire.core.files.FileHasher
import com.muwire.core.mesh.Mesh
import com.muwire.core.mesh.MeshManager
import com.muwire.core.trust.TrustLevel
import com.muwire.core.trust.TrustService
import com.muwire.core.util.DataUtil import com.muwire.core.util.DataUtil
import groovy.json.JsonBuilder import groovy.json.JsonBuilder
@@ -18,37 +14,31 @@ import net.i2p.util.ConcurrentHashSet
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.Persona import com.muwire.core.Persona
import com.muwire.core.UILoadedEvent import com.muwire.core.UILoadedEvent
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executor import java.util.concurrent.Executor
import java.util.concurrent.Executors import java.util.concurrent.Executors
public class DownloadManager { public class DownloadManager {
private final EventBus eventBus private final EventBus eventBus
private final TrustService trustService
private final MeshManager meshManager
private final MuWireSettings muSettings
private final I2PConnector connector private final I2PConnector connector
private final Executor executor private final Executor executor
private final File home private final File incompletes, home
private final Persona me private final Persona me
private final Map<InfoHash, Downloader> downloaders = new ConcurrentHashMap<>() private final Set<Downloader> downloaders = new ConcurrentHashSet<>()
public DownloadManager(EventBus eventBus, TrustService trustService, MeshManager meshManager, MuWireSettings muSettings, public DownloadManager(EventBus eventBus, I2PConnector connector, File home, Persona me) {
I2PConnector connector, File home, Persona me) {
this.eventBus = eventBus this.eventBus = eventBus
this.trustService = trustService
this.meshManager = meshManager
this.muSettings = muSettings
this.connector = connector this.connector = connector
this.incompletes = new File(home,"incompletes")
this.home = home this.home = home
this.me = me 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")
@@ -60,11 +50,6 @@ public class DownloadManager {
public void onUIDownloadEvent(UIDownloadEvent e) { public void onUIDownloadEvent(UIDownloadEvent e) {
File incompletes = muSettings.incompleteLocation
if (incompletes == null)
incompletes = new File(home, "incompletes")
incompletes.mkdirs()
def size = e.result[0].size def size = e.result[0].size
def infohash = e.result[0].infohash def infohash = e.result[0].infohash
def pieceSize = e.result[0].pieceSize def pieceSize = e.result[0].pieceSize
@@ -73,30 +58,18 @@ public class DownloadManager {
e.result.each { e.result.each {
destinations.add(it.sender.destination) destinations.add(it.sender.destination)
} }
destinations.addAll(e.sources)
destinations.remove(me.destination)
Pieces pieces = getPieces(infohash, size, pieceSize, e.sequential)
def downloader = new Downloader(eventBus, this, me, e.target, size, def downloader = new Downloader(eventBus, this, me, e.target, size,
infohash, pieceSize, connector, destinations, infohash, pieceSize, connector, destinations,
incompletes, pieces) incompletes)
downloaders.put(infohash, downloader) downloaders.add(downloader)
persistDownloaders() persistDownloaders()
executor.execute({downloader.download()} as Runnable) executor.execute({downloader.download()} as Runnable)
eventBus.publish(new DownloadStartedEvent(downloader : downloader)) eventBus.publish(new DownloadStartedEvent(downloader : downloader))
} }
public void onUIDownloadCancelledEvent(UIDownloadCancelledEvent e) { public void onUIDownloadCancelledEvent(UIDownloadCancelledEvent e) {
downloaders.remove(e.downloader.infoHash) downloaders.remove(e.downloader)
persistDownloaders()
}
public void onUIDownloadPausedEvent(UIDownloadPausedEvent e) {
persistDownloaders()
}
public void onUIDownloadResumedEvent(UIDownloadResumedEvent e) {
persistDownloaders() persistDownloaders()
} }
@@ -124,64 +97,23 @@ public class DownloadManager {
byte [] root = Base64.decode(json.hashRoot) byte [] root = Base64.decode(json.hashRoot)
infoHash = new InfoHash(root) infoHash = new InfoHash(root)
} }
boolean sequential = false
if (json.sequential != null)
sequential = json.sequential
File incompletes
if (json.incompletes != null)
incompletes = new File(DataUtil.readi18nString(Base64.decode(json.incompletes)))
else
incompletes = new File(home, "incompletes")
Pieces pieces = getPieces(infoHash, (long)json.length, json.pieceSizePow2, sequential)
def downloader = new Downloader(eventBus, this, me, file, (long)json.length, def downloader = new Downloader(eventBus, this, me, file, (long)json.length,
infoHash, json.pieceSizePow2, connector, destinations, incompletes, pieces) infoHash, json.pieceSizePow2, connector, destinations, incompletes)
if (json.paused != null) downloaders.add(downloader)
downloader.paused = json.paused
downloaders.put(infoHash, downloader)
downloader.readPieces()
if (!downloader.paused)
downloader.download() downloader.download()
eventBus.publish(new DownloadStartedEvent(downloader : downloader)) eventBus.publish(new DownloadStartedEvent(downloader : downloader))
} }
} }
private Pieces getPieces(InfoHash infoHash, long length, int pieceSizePow2, boolean sequential) {
int pieceSize = 0x1 << pieceSizePow2
int nPieces = (int)(length / pieceSize)
if (length % pieceSize != 0)
nPieces++
Mesh mesh = meshManager.getOrCreate(infoHash, nPieces, sequential)
mesh.pieces
}
void onSourceDiscoveredEvent(SourceDiscoveredEvent e) {
Downloader downloader = downloaders.get(e.infoHash)
if (downloader == null)
return
boolean ok = false
switch(trustService.getLevel(e.source.destination)) {
case TrustLevel.TRUSTED: ok = true; break
case TrustLevel.NEUTRAL: ok = muSettings.allowUntrusted; break
case TrustLevel.DISTRUSTED: ok = false; break
}
if (ok)
downloader.addSource(e.source.destination)
}
void onFileDownloadedEvent(FileDownloadedEvent e) { void onFileDownloadedEvent(FileDownloadedEvent e) {
downloaders.remove(e.downloader.infoHash) downloaders.remove(e.downloader)
persistDownloaders() persistDownloaders()
} }
private void persistDownloaders() { private void persistDownloaders() {
File downloadsFile = new File(home,"downloads.json") File downloadsFile = new File(home,"downloads.json")
downloadsFile.withPrintWriter { writer -> downloadsFile.withPrintWriter { writer ->
downloaders.values().each { downloader -> downloaders.each { downloader ->
if (!downloader.cancelled) { if (!downloader.cancelled) {
def json = [:] def json = [:]
json.file = Base64.encode(DataUtil.encodei18nString(downloader.file.getAbsolutePath())) json.file = Base64.encode(DataUtil.encodei18nString(downloader.file.getAbsolutePath()))
@@ -198,13 +130,6 @@ public class DownloadManager {
json.hashList = Base64.encode(infoHash.hashList) json.hashList = Base64.encode(infoHash.hashList)
else else
json.hashRoot = Base64.encode(infoHash.getRoot()) json.hashRoot = Base64.encode(infoHash.getRoot())
json.paused = downloader.paused
json.sequential = downloader.pieces.ratio == 0f
json.incompletes = Base64.encode(DataUtil.encodei18nString(downloader.incompletes.getAbsolutePath()))
writer.println(JsonOutput.toJson(json)) writer.println(JsonOutput.toJson(json))
} }
} }
@@ -212,7 +137,7 @@ public class DownloadManager {
} }
public void shutdown() { public void shutdown() {
downloaders.values().each { it.stop() } downloaders.each { it.stop() }
Downloader.executorService.shutdownNow() Downloader.executorService.shutdownNow()
} }
} }

View File

@@ -3,56 +3,49 @@ package com.muwire.core.download;
import net.i2p.data.Base64 import net.i2p.data.Base64
import com.muwire.core.Constants import com.muwire.core.Constants
import com.muwire.core.EventBus
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.util.DataUtil
import static com.muwire.core.util.DataUtil.readTillRN import static com.muwire.core.util.DataUtil.readTillRN
import groovy.util.logging.Log import groovy.util.logging.Log
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.MappedByteBuffer
import java.nio.channels.FileChannel import java.nio.channels.FileChannel
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.StandardOpenOption import java.nio.file.StandardOpenOption
import java.security.MessageDigest import java.security.MessageDigest
import java.security.NoSuchAlgorithmException import java.security.NoSuchAlgorithmException
import java.util.logging.Level
@Log @Log
class DownloadSession { class DownloadSession {
private final EventBus eventBus private static int SAMPLES = 10
private final String meB64 private final String meB64
private final Pieces pieces private final Pieces downloaded, claimed
private final InfoHash infoHash private final InfoHash infoHash
private final Endpoint endpoint private final Endpoint endpoint
private final File file private final File file
private final int pieceSize private final int pieceSize
private final long fileLength private final long fileLength
private final Set<Integer> available
private final MessageDigest digest private final MessageDigest digest
private long lastSpeedRead = System.currentTimeMillis() private final LinkedList<Long> timestamps = new LinkedList<>()
private long dataSinceLastRead private final LinkedList<Integer> reads = new LinkedList<>()
private MappedByteBuffer mapped private ByteBuffer mapped
DownloadSession(EventBus eventBus, String meB64, Pieces pieces, InfoHash infoHash, Endpoint endpoint, File file, DownloadSession(String meB64, Pieces downloaded, Pieces claimed, InfoHash infoHash, Endpoint endpoint, File file,
int pieceSize, long fileLength, Set<Integer> available) { int pieceSize, long fileLength) {
this.eventBus = eventBus
this.meB64 = meB64 this.meB64 = meB64
this.pieces = pieces this.downloaded = downloaded
this.claimed = claimed
this.endpoint = endpoint this.endpoint = endpoint
this.infoHash = infoHash this.infoHash = infoHash
this.file = file this.file = file
this.pieceSize = pieceSize this.pieceSize = pieceSize
this.fileLength = fileLength this.fileLength = fileLength
this.available = available
try { try {
digest = MessageDigest.getInstance("SHA-256") digest = MessageDigest.getInstance("SHA-256")
} catch (NoSuchAlgorithmException impossible) { } catch (NoSuchAlgorithmException impossible) {
@@ -70,100 +63,70 @@ class DownloadSession {
OutputStream os = endpoint.getOutputStream() OutputStream os = endpoint.getOutputStream()
InputStream is = endpoint.getInputStream() InputStream is = endpoint.getInputStream()
int[] pieceAndPosition int piece
if (available.isEmpty()) while(true) {
pieceAndPosition = pieces.claim() piece = downloaded.getRandomPiece()
else if (claimed.isMarked(piece)) {
pieceAndPosition = pieces.claim(new HashSet<>(available)) if (downloaded.donePieces() + claimed.donePieces() == downloaded.nPieces) {
if (pieceAndPosition == null) log.info("all pieces claimed")
return false return false
int piece = pieceAndPosition[0] }
int position = pieceAndPosition[1] continue
boolean steal = pieceAndPosition[2] == 1 }
boolean unclaim = true break
}
claimed.markDownloaded(piece)
log.info("will download piece $piece from position $position steal $steal") log.info("will download piece $piece")
long start = piece * pieceSize
long end = Math.min(fileLength, start + pieceSize) - 1
long length = end - start + 1
long pieceStart = piece * ((long)pieceSize)
long end = Math.min(fileLength, pieceStart + pieceSize) - 1
long start = pieceStart + position
String root = Base64.encode(infoHash.getRoot()) String root = Base64.encode(infoHash.getRoot())
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".getBytes(StandardCharsets.US_ASCII)) os.write("Range: $start-$end\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("X-Persona: $meB64\r\n".getBytes(StandardCharsets.US_ASCII)) os.write("X-Persona: $meB64\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
String xHave = DataUtil.encodeXHave(pieces.getDownloaded(), pieces.nPieces)
os.write("X-Have: $xHave\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
os.flush() os.flush()
String codeString = readTillRN(is) String code = readTillRN(is)
int space = codeString.indexOf(' ') if (code.startsWith("404 ")) {
if (space > 0)
codeString = codeString.substring(0, space)
int code = Integer.parseInt(codeString.trim())
if (code == 404) {
log.warning("file not found") log.warning("file not found")
endpoint.close() endpoint.close()
return false return
} }
if (!(code == 200 || code == 416)) { if (code.startsWith("416 ")) {
log.warning("range $start-$end cannot be satisfied")
return // leave endpoint open
}
if (!code.startsWith("200 ")) {
log.warning("unknown code $code") log.warning("unknown code $code")
endpoint.close() endpoint.close()
return false return false
} }
// parse all headers // parse all headers
Map<String,String> headers = new HashMap<>() Set<String> headers = new HashSet<>()
String header String header
while((header = readTillRN(is)) != "" && headers.size() < Constants.MAX_HEADERS) { while((header = readTillRN(is)) != "" && headers.size() < Constants.MAX_HEADERS)
int colon = header.indexOf(':') headers.add(header)
if (colon == -1 || colon == header.length() - 1)
throw new IOException("invalid header $header") long receivedStart = -1
String key = header.substring(0, colon) long receivedEnd = -1
String value = header.substring(colon + 1) for (String receivedHeader : headers) {
headers[key] = value.trim() def group = (receivedHeader =~ /^Content-Range: (\d+)-(\d+)$/)
if (group.size() != 1) {
log.info("ignoring header $receivedHeader")
continue
} }
// prase X-Alt if present receivedStart = Long.parseLong(group[0][1])
if (headers.containsKey("X-Alt")) { receivedEnd = Long.parseLong(group[0][2])
headers["X-Alt"].split(",").each {
if (it.length() > 0) {
byte [] raw = Base64.decode(it)
Persona source = new Persona(new ByteArrayInputStream(raw))
eventBus.publish(new SourceDiscoveredEvent(infoHash : infoHash, source : source))
} }
}
}
// parse X-Have if present
if (headers.containsKey("X-Have")) {
DataUtil.decodeXHave(headers["X-Have"]).each {
available.add(it)
}
if (!available.contains(piece))
return true // try again next time
} else {
if (code != 200)
throw new IOException("Code $code but no X-Have")
available.clear()
}
if (code != 200)
return true
String range = headers["Content-Range"]
if (range == null)
throw new IOException("Code 200 but no Content-Range")
def group = (range =~ /^(\d+)-(\d+)$/)
if (group.size() != 1)
throw new IOException("invalid Content-Range header $range")
long receivedStart = Long.parseLong(group[0][1])
long receivedEnd = Long.parseLong(group[0][2])
if (receivedStart != start || receivedEnd != end) { if (receivedStart != start || receivedEnd != end) {
log.warning("We don't support mismatching ranges yet") log.warning("We don't support mismatching ranges yet")
@@ -172,12 +135,9 @@ class DownloadSession {
} }
// start the download // start the download
FileChannel channel
try {
channel = Files.newByteChannel(file.toPath(), EnumSet.of(StandardOpenOption.READ, StandardOpenOption.WRITE, channel = Files.newByteChannel(file.toPath(), EnumSet.of(StandardOpenOption.READ, StandardOpenOption.WRITE,
StandardOpenOption.SPARSE, StandardOpenOption.CREATE)) StandardOpenOption.SPARSE, StandardOpenOption.CREATE)) // TODO: double-check, maybe CREATE_NEW
mapped = channel.map(FileChannel.MapMode.READ_WRITE, pieceStart, end - pieceStart + 1) mapped = channel.map(FileChannel.MapMode.READ_WRITE, start, end - start + 1)
mapped.position(position)
byte[] tmp = new byte[0x1 << 13] byte[] tmp = new byte[0x1 << 13]
while(mapped.hasRemaining()) { while(mapped.hasRemaining()) {
@@ -188,8 +148,13 @@ class DownloadSession {
throw new IOException() throw new IOException()
synchronized(this) { synchronized(this) {
mapped.put(tmp, 0, read) mapped.put(tmp, 0, read)
dataSinceLastRead += read
pieces.markPartial(piece, mapped.position()) if (timestamps.size() == SAMPLES) {
timestamps.removeFirst()
reads.removeFirst()
}
timestamps.addLast(System.currentTimeMillis())
reads.addLast(read)
} }
} }
@@ -198,19 +163,13 @@ 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)
pieces.markPartial(piece, 0) throw new BadHashException()
throw new BadHashException("bad hash on piece $piece")
} downloaded.markDownloaded(piece)
} finally { } finally {
claimed.clear(piece)
try { channel?.close() } catch (IOException ignore) {} try { channel?.close() } catch (IOException ignore) {}
DataUtil.tryUnmap(mapped)
}
pieces.markDownloaded(piece)
unclaim = false
} finally {
if (unclaim && !steal)
pieces.unclaim(piece)
} }
return true return true
} }
@@ -222,11 +181,24 @@ class DownloadSession {
} }
synchronized int speed() { synchronized int speed() {
if (timestamps.size() < SAMPLES)
return 0
int totalRead = 0
int idx = 0
final long now = System.currentTimeMillis() final long now = System.currentTimeMillis()
long interval = Math.max(1000, now - lastSpeedRead)
lastSpeedRead = now; while(idx < SAMPLES && timestamps.get(idx) < now - 1000)
int rv = (int) (dataSinceLastRead * 1000.0 / interval) idx++
dataSinceLastRead = 0 if (idx == SAMPLES)
rv 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

@@ -4,14 +4,9 @@ import com.muwire.core.InfoHash
import com.muwire.core.Persona import com.muwire.core.Persona
import com.muwire.core.connection.Endpoint import com.muwire.core.connection.Endpoint
import java.nio.file.AtomicMoveNotSupportedException
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.time.Instant
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ExecutorService import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
import java.util.logging.Level import java.util.logging.Level
import com.muwire.core.Constants import com.muwire.core.Constants
@@ -19,16 +14,13 @@ import com.muwire.core.DownloadedFile
import com.muwire.core.EventBus import com.muwire.core.EventBus
import com.muwire.core.connection.I2PConnector import com.muwire.core.connection.I2PConnector
import com.muwire.core.files.FileDownloadedEvent import com.muwire.core.files.FileDownloadedEvent
import com.muwire.core.util.DataUtil
import groovy.util.logging.Log import groovy.util.logging.Log
import net.i2p.data.Destination import net.i2p.data.Destination
import net.i2p.util.ConcurrentHashSet
@Log @Log
public class Downloader { public class Downloader {
public enum DownloadState { CONNECTING, HASHLIST, DOWNLOADING, FAILED, CANCELLED, FINISHED }
public enum DownloadState { CONNECTING, HASHLIST, DOWNLOADING, FAILED, CANCELLED, PAUSED, FINISHED }
private enum WorkerState { CONNECTING, HASHLIST, DOWNLOADING, FINISHED} private enum WorkerState { CONNECTING, HASHLIST, DOWNLOADING, FINISHED}
private static final ExecutorService executorService = Executors.newCachedThreadPool({r -> private static final ExecutorService executorService = Executors.newCachedThreadPool({r ->
@@ -42,34 +34,25 @@ public class Downloader {
private final DownloadManager downloadManager private final DownloadManager downloadManager
private final Persona me 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 InfoHash infoHash private InfoHash infoHash
private final int pieceSize private final int pieceSize
private final I2PConnector connector private final I2PConnector connector
private final Set<Destination> destinations private final Set<Destination> destinations
private final int nPieces private final int nPieces
private final File incompletes
private final File piecesFile private final File piecesFile
private final File incompleteFile
final int pieceSizePow2 final int pieceSizePow2
private final Map<Destination, DownloadWorker> activeWorkers = new ConcurrentHashMap<>() private final Map<Destination, DownloadWorker> activeWorkers = new ConcurrentHashMap<>()
private final Set<Destination> successfulDestinations = new ConcurrentHashSet<>()
private volatile boolean cancelled, paused private volatile boolean cancelled
private final AtomicBoolean eventFired = new AtomicBoolean() private volatile boolean eventFired
private boolean piecesFileClosed
private ArrayList speedArr = new ArrayList<Integer>()
private int speedPos = 0
private int speedAvg = 0
private long timestamp = Instant.now().toEpochMilli()
public Downloader(EventBus eventBus, DownloadManager downloadManager, public Downloader(EventBus eventBus, DownloadManager downloadManager,
Persona me, File file, long length, InfoHash infoHash, Persona me, File file, long length, InfoHash infoHash,
int pieceSizePow2, I2PConnector connector, Set<Destination> destinations, int pieceSizePow2, I2PConnector connector, Set<Destination> destinations,
File incompletes, Pieces pieces) { File incompletes) {
this.eventBus = eventBus this.eventBus = eventBus
this.me = me this.me = me
this.downloadManager = downloadManager this.downloadManager = downloadManager
@@ -78,13 +61,19 @@ public class Downloader {
this.length = length this.length = length
this.connector = connector this.connector = connector
this.destinations = destinations this.destinations = destinations
this.incompletes = incompletes
this.piecesFile = new File(incompletes, file.getName()+".pieces") this.piecesFile = new File(incompletes, file.getName()+".pieces")
this.incompleteFile = new File(incompletes, file.getName()+".part")
this.pieceSizePow2 = pieceSizePow2 this.pieceSizePow2 = pieceSizePow2
this.pieceSize = 1 << pieceSizePow2 this.pieceSize = 1 << pieceSizePow2
this.pieces = pieces
this.nPieces = pieces.nPieces int nPieces
if (length % pieceSize == 0)
nPieces = length / pieceSize
else
nPieces = length / pieceSize + 1
this.nPieces = nPieces
downloaded = new Pieces(nPieces, Constants.DOWNLOAD_SEQUENTIAL_RATIO)
claimed = new Pieces(nPieces)
} }
public synchronized InfoHash getInfoHash() { public synchronized InfoHash getInfoHash() {
@@ -110,82 +99,44 @@ public class Downloader {
if (!piecesFile.exists()) if (!piecesFile.exists())
return return
piecesFile.eachLine { piecesFile.eachLine {
String [] split = it.split(",") int piece = Integer.parseInt(it)
int piece = Integer.parseInt(split[0]) downloaded.markDownloaded(piece)
if (split.length == 1)
pieces.markDownloaded(piece)
else {
int position = Integer.parseInt(split[1])
pieces.markPartial(piece, position)
}
} }
} }
void writePieces() { void writePieces() {
synchronized(piecesFile) {
if (piecesFileClosed)
return
piecesFile.withPrintWriter { writer -> piecesFile.withPrintWriter { writer ->
pieces.write(writer) downloaded.getDownloaded().each { piece ->
writer.println(piece)
} }
} }
} }
public long donePieces() { public long donePieces() {
pieces.donePieces() downloaded.donePieces()
} }
public int speed() { public int speed() {
int currSpeed = 0 int total = 0
if (getCurrentState() == DownloadState.DOWNLOADING) { if (getCurrentState() == DownloadState.DOWNLOADING) {
activeWorkers.values().each { activeWorkers.values().each {
if (it.currentState == WorkerState.DOWNLOADING) if (it.currentState == WorkerState.DOWNLOADING)
currSpeed += it.speed() total += it.speed()
} }
} }
total
if (speedArr.size() != downloadManager.muSettings.speedSmoothSeconds) {
speedArr.clear()
downloadManager.muSettings.speedSmoothSeconds.times { speedArr.add(0) }
speedPos = 0
}
// normalize to speedArr.size
currSpeed /= speedArr.size()
// compute new speedAvg and update speedArr
if ( speedArr[speedPos] > speedAvg ) {
speedAvg = 0
} else {
speedAvg -= speedArr[speedPos]
}
speedAvg += currSpeed
speedArr[speedPos] = currSpeed
// this might be necessary due to rounding errors
if (speedAvg < 0)
speedAvg = 0
// rolling index over the speedArr
speedPos++
if (speedPos >= speedArr.size())
speedPos=0
speedAvg
} }
public DownloadState getCurrentState() { public DownloadState getCurrentState() {
if (cancelled) if (cancelled)
return DownloadState.CANCELLED return DownloadState.CANCELLED
if (paused)
return DownloadState.PAUSED
boolean allFinished = true boolean allFinished = true
activeWorkers.values().each { activeWorkers.values().each {
allFinished &= it.currentState == WorkerState.FINISHED allFinished &= it.currentState == WorkerState.FINISHED
} }
if (allFinished) { if (allFinished) {
if (pieces.isComplete()) if (downloaded.isComplete())
return DownloadState.FINISHED return DownloadState.FINISHED
return DownloadState.FAILED return DownloadState.FAILED
} }
@@ -219,18 +170,9 @@ public class Downloader {
public void cancel() { public void cancel() {
cancelled = true cancelled = true
stop() stop()
synchronized(piecesFile) { file.delete()
piecesFileClosed = true
piecesFile.delete() piecesFile.delete()
} }
incompleteFile.delete()
pieces.clearAll()
}
public void pause() {
paused = true
stop()
}
void stop() { void stop() {
activeWorkers.values().each { activeWorkers.values().each {
@@ -248,8 +190,6 @@ public class Downloader {
} }
public void resume() { public void resume() {
paused = false
readPieces()
destinations.each { destination -> destinations.each { destination ->
def worker = activeWorkers.get(destination) def worker = activeWorkers.get(destination)
if (worker != null) { if (worker != null) {
@@ -266,21 +206,12 @@ public class Downloader {
} }
} }
void addSource(Destination d) {
if (activeWorkers.containsKey(d))
return
DownloadWorker newWorker = new DownloadWorker(d)
activeWorkers.put(d, newWorker)
executorService.submit(newWorker)
}
class DownloadWorker implements Runnable { class DownloadWorker implements Runnable {
private final Destination destination private final Destination destination
private volatile WorkerState currentState private volatile WorkerState currentState
private volatile Thread downloadThread private volatile Thread downloadThread
private Endpoint endpoint private Endpoint endpoint
private volatile DownloadSession currentSession private volatile DownloadSession currentSession
private final Set<Integer> available = new HashSet<>()
DownloadWorker(Destination destination) { DownloadWorker(Destination destination) {
this.destination = destination this.destination = destination
@@ -300,38 +231,23 @@ public class Downloader {
} }
currentState = WorkerState.DOWNLOADING currentState = WorkerState.DOWNLOADING
boolean requestPerformed boolean requestPerformed
while(!pieces.isComplete()) { while(!downloaded.isComplete()) {
currentSession = new DownloadSession(eventBus, me.toBase64(), pieces, getInfoHash(), currentSession = new DownloadSession(me.toBase64(), downloaded, claimed, getInfoHash(), endpoint, file, pieceSize, length)
endpoint, incompleteFile, pieceSize, length, available)
requestPerformed = currentSession.request() requestPerformed = currentSession.request()
if (!requestPerformed) if (!requestPerformed)
break break
successfulDestinations.add(endpoint.destination)
writePieces() writePieces()
} }
} catch (Exception bad) { } catch (Exception bad) {
log.log(Level.WARNING,"Exception while downloading",DataUtil.findRoot(bad)) log.log(Level.WARNING,"Exception while downloading",bad)
} finally { } finally {
writePieces()
currentState = WorkerState.FINISHED currentState = WorkerState.FINISHED
if (pieces.isComplete() && eventFired.compareAndSet(false, true)) { if (downloaded.isComplete() && !eventFired) {
synchronized(piecesFile) {
piecesFileClosed = true
piecesFile.delete() piecesFile.delete()
} eventFired = true
activeWorkers.values().each {
if (it.destination != destination)
it.cancel()
}
try {
Files.move(incompleteFile.toPath(), file.toPath(), StandardCopyOption.ATOMIC_MOVE)
} catch (AtomicMoveNotSupportedException e) {
Files.copy(incompleteFile.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING)
incompleteFile.delete()
}
eventBus.publish( eventBus.publish(
new FileDownloadedEvent( new FileDownloadedEvent(
downloadedFile : new DownloadedFile(file.getCanonicalFile(), getInfoHash(), pieceSizePow2, successfulDestinations), downloadedFile : new DownloadedFile(file, getInfoHash(), pieceSizePow2, Collections.emptySet()),
downloader : Downloader.this)) downloader : Downloader.this))
} }

View File

@@ -1,11 +1,10 @@
package com.muwire.core.download package com.muwire.core.download
class Pieces { class Pieces {
private final BitSet done, claimed private final BitSet bitSet
private final int nPieces private final int nPieces
private final float ratio private final float ratio
private final Random random = new Random() private final Random random = new Random()
private final Map<Integer,Integer> partials = new HashMap<>()
Pieces(int nPieces) { Pieces(int nPieces) {
this(nPieces, 1.0f) this(nPieces, 1.0f)
@@ -14,104 +13,52 @@ class Pieces {
Pieces(int nPieces, float ratio) { Pieces(int nPieces, float ratio) {
this.nPieces = nPieces this.nPieces = nPieces
this.ratio = ratio this.ratio = ratio
done = new BitSet(nPieces) bitSet = new BitSet(nPieces)
claimed = new BitSet(nPieces)
} }
synchronized int[] claim() { synchronized int getRandomPiece() {
int claimedCardinality = claimed.cardinality() int cardinality = bitSet.cardinality()
if (claimedCardinality == nPieces) { if (cardinality == nPieces)
// steal return -1
int downloadedCardinality = done.cardinality()
if (downloadedCardinality == nPieces)
return null
int rv = done.nextClearBit(0)
return [rv, partials.getOrDefault(rv, 0), 1]
}
// if fuller than ratio just do sequential // if fuller than ratio just do sequential
if ( (1.0f * claimedCardinality) / nPieces >= ratio) { if ( (1.0f * cardinality) / nPieces > ratio) {
int rv = claimed.nextClearBit(0) return bitSet.nextClearBit(0)
claimed.set(rv)
return [rv, partials.getOrDefault(rv, 0), 0]
} }
while(true) { while(true) {
int start = random.nextInt(nPieces) int start = random.nextInt(nPieces)
if (claimed.get(start)) if (bitSet.get(start))
continue continue
claimed.set(start) return start
return [start, partials.getOrDefault(start,0), 0]
} }
} }
synchronized int[] claim(Set<Integer> available) { def getDownloaded() {
for (int i = done.nextSetBit(0); i >= 0; i = done.nextSetBit(i+1))
available.remove(i)
if (available.isEmpty())
return null
Set<Integer> availableCopy = new HashSet<>(available)
for (int i = claimed.nextSetBit(0); i >= 0; i = claimed.nextSetBit(i+1))
availableCopy.remove(i)
if (availableCopy.isEmpty()) {
// steal
int rv = available.first()
return [rv, partials.getOrDefault(rv, 0), 1]
}
List<Integer> toList = availableCopy.toList()
if (ratio > 0f)
Collections.shuffle(toList)
int rv = toList[0]
claimed.set(rv)
[rv, partials.getOrDefault(rv, 0), 0]
}
synchronized def getDownloaded() {
def rv = [] def rv = []
for (int i = done.nextSetBit(0); i >= 0; i = done.nextSetBit(i+1)) { for (int i = bitSet.nextSetBit(0); i >= 0; i = bitSet.nextSetBit(i+1)) {
rv << i rv << i
} }
rv rv
} }
synchronized void markDownloaded(int piece) { synchronized void markDownloaded(int piece) {
done.set(piece) bitSet.set(piece)
claimed.set(piece)
partials.remove(piece)
} }
synchronized void markPartial(int piece, int position) { synchronized void clear(int piece) {
partials.put(piece, position) bitSet.clear(piece)
}
synchronized void unclaim(int piece) {
claimed.clear(piece)
} }
synchronized boolean isComplete() { synchronized boolean isComplete() {
done.cardinality() == nPieces bitSet.cardinality() == nPieces
}
synchronized boolean isMarked(int piece) {
bitSet.get(piece)
} }
synchronized int donePieces() { synchronized int donePieces() {
done.cardinality() bitSet.cardinality()
}
synchronized boolean isDownloaded(int piece) {
done.get(piece)
}
synchronized void clearAll() {
done.clear()
claimed.clear()
partials.clear()
}
synchronized void write(PrintWriter writer) {
for (int i = done.nextSetBit(0); i >= 0; i = done.nextSetBit(i+1)) {
writer.println(i)
}
partials.each { piece, position ->
writer.println("$piece,$position")
}
} }
} }

View File

@@ -1,10 +0,0 @@
package com.muwire.core.download
import com.muwire.core.Event
import com.muwire.core.InfoHash
import com.muwire.core.Persona
class SourceDiscoveredEvent extends Event {
InfoHash infoHash
Persona source
}

View File

@@ -3,12 +3,8 @@ package com.muwire.core.download
import com.muwire.core.Event import com.muwire.core.Event
import com.muwire.core.search.UIResultEvent import com.muwire.core.search.UIResultEvent
import net.i2p.data.Destination
class UIDownloadEvent extends Event { class UIDownloadEvent extends Event {
UIResultEvent[] result UIResultEvent[] result
Set<Destination> sources
File target File target
boolean sequential
} }

View File

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

View File

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

View File

@@ -1,7 +0,0 @@
package com.muwire.core.files
import com.muwire.core.Event
class DirectoryUnsharedEvent extends Event {
File directory
}

View File

@@ -1,166 +1,4 @@
package com.muwire.core.files package com.muwire.core.files
import java.nio.file.FileSystem
import java.nio.file.FileSystems
import java.nio.file.Path
import java.nio.file.Paths
import static java.nio.file.StandardWatchEventKinds.*
import java.nio.file.ClosedWatchServiceException
import java.nio.file.WatchEvent
import java.nio.file.WatchKey
import java.nio.file.WatchService
import java.util.concurrent.ConcurrentHashMap
import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import com.muwire.core.SharedFile
import groovy.util.logging.Log
import net.i2p.util.SystemVersion
@Log
class DirectoryWatcher { class DirectoryWatcher {
private static final long WAIT_TIME = 1000
private static final WatchEvent.Kind[] kinds
static {
if (SystemVersion.isMac())
kinds = [ENTRY_MODIFY, ENTRY_DELETE]
else
kinds = [ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE]
}
private final File home
private final MuWireSettings muOptions
private final EventBus eventBus
private final FileManager fileManager
private final Thread watcherThread, publisherThread
private final Map<File, Long> waitingFiles = new ConcurrentHashMap<>()
private final Map<File, WatchKey> watchedDirectories = new ConcurrentHashMap<>()
private WatchService watchService
private volatile boolean shutdown
DirectoryWatcher(EventBus eventBus, FileManager fileManager, File home, MuWireSettings muOptions) {
this.home = home
this.muOptions = muOptions
this.eventBus = eventBus
this.fileManager = fileManager
this.watcherThread = new Thread({watch() } as Runnable, "directory-watcher")
watcherThread.setDaemon(true)
this.publisherThread = new Thread({publish()} as Runnable, "watched-files-publisher")
publisherThread.setDaemon(true)
}
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
watchService = FileSystems.getDefault().newWatchService()
watcherThread.start()
publisherThread.start()
}
void stop() {
shutdown = true
watcherThread?.interrupt()
publisherThread?.interrupt()
watchService?.close()
}
void onFileSharedEvent(FileSharedEvent e) {
if (!e.file.isDirectory())
return
File canonical = e.file.getCanonicalFile()
Path path = canonical.toPath()
WatchKey wk = path.register(watchService, kinds)
watchedDirectories.put(canonical, wk)
if (muOptions.watchedDirectories.add(canonical.toString()))
saveMuSettings()
}
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent e) {
WatchKey wk = watchedDirectories.remove(e.directory)
wk?.cancel()
if (muOptions.watchedDirectories.remove(e.directory.toString()))
saveMuSettings()
}
private void saveMuSettings() {
File muSettingsFile = new File(home, "MuWire.properties")
muSettingsFile.withOutputStream {
muOptions.write(it)
}
}
private void watch() {
try {
while(!shutdown) {
WatchKey key = watchService.take()
key.pollEvents().each {
switch(it.kind()) {
case ENTRY_CREATE: processCreated(key.watchable(), it.context()); break
case ENTRY_MODIFY: processModified(key.watchable(), it.context()); break
case ENTRY_DELETE: processDeleted(key.watchable(), it.context()); break
}
}
key.reset()
}
} catch (InterruptedException|ClosedWatchServiceException e) {
if (!shutdown)
throw e
}
}
private void processCreated(Path parent, Path path) {
File f= join(parent, path)
log.fine("created entry $f")
if (f.isDirectory())
f.toPath().register(watchService, kinds)
else
waitingFiles.put(f, System.currentTimeMillis())
}
private void processModified(Path parent, Path path) {
File f = join(parent, path)
log.fine("modified entry $f")
waitingFiles.put(f, System.currentTimeMillis())
}
private void processDeleted(Path parent, Path path) {
File f = join(parent, path)
log.fine("deleted entry $f")
SharedFile sf = fileManager.fileToSharedFile.get(f)
if (sf != null)
eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
}
private static File join(Path parent, Path path) {
File parentFile = parent.toFile().getCanonicalFile()
new File(parentFile, path.toFile().getName()).getCanonicalFile()
}
private void publish() {
try {
while(!shutdown) {
Thread.sleep(WAIT_TIME)
long now = System.currentTimeMillis()
def published = []
waitingFiles.each { file, timestamp ->
if (now - timestamp > WAIT_TIME) {
log.fine("publishing file $file")
eventBus.publish new FileSharedEvent(file : file)
published << file
}
}
published.each {
waitingFiles.remove(it)
}
}
} catch (InterruptedException e) {
if (!shutdown)
throw e
}
}
} }

View File

@@ -7,10 +7,4 @@ class FileHashedEvent extends Event {
SharedFile sharedFile SharedFile sharedFile
String error String error
@Override
public String toString() {
super.toString() + " sharedFile " + sharedFile?.file.getAbsolutePath() + " error: $error"
}
} }

View File

@@ -1,7 +1,6 @@
package com.muwire.core.files package com.muwire.core.files
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
import com.muwire.core.util.DataUtil
import net.i2p.data.Base64 import net.i2p.data.Base64
@@ -19,8 +18,6 @@ class FileHasher {
/** /**
* @param size of the file to be shared * @param size of the file to be shared
* @return the size of each piece in power of 2 * @return the size of each piece in power of 2
* piece size is minimum 128 KBytees and maximum 16 MBytes in power of 2 steps (2^17 - 2^24)
* there can be up to 8192 pieces maximum per file
*/ */
static int getPieceSize(long size) { static int getPieceSize(long size) {
if (size <= 0x1 << 30) if (size <= 0x1 << 30)
@@ -60,7 +57,6 @@ class FileHasher {
for (int i = 0; i < numPieces - 1; i++) { for (int i = 0; i < numPieces - 1; i++) {
buf = raf.getChannel().map(MapMode.READ_ONLY, ((long)size) * i, size) buf = raf.getChannel().map(MapMode.READ_ONLY, ((long)size) * i, size)
digest.update buf digest.update buf
DataUtil.tryUnmap(buf)
output.write(digest.digest(), 0, 32) output.write(digest.digest(), 0, 32)
} }
def lastPieceLength = length - (numPieces - 1) * ((long)size) def lastPieceLength = length - (numPieces - 1) * ((long)size)

View File

@@ -1,15 +0,0 @@
package com.muwire.core.files
import com.muwire.core.Event
import com.muwire.core.SharedFile
class FileHashingEvent extends Event {
File hashingFile
@Override
public String toString() {
super.toString() + " hashingFile " + hashingFile.getAbsolutePath()
}
}

View File

@@ -8,10 +8,8 @@ import com.muwire.core.UILoadedEvent
import com.muwire.core.search.ResultsEvent import com.muwire.core.search.ResultsEvent
import com.muwire.core.search.SearchEvent import com.muwire.core.search.SearchEvent
import com.muwire.core.search.SearchIndex import com.muwire.core.search.SearchIndex
import com.muwire.core.util.DataUtil
import groovy.util.logging.Log import groovy.util.logging.Log
import net.i2p.data.Base64
@Log @Log
class FileManager { class FileManager {
@@ -22,7 +20,6 @@ class FileManager {
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 Map<String, Set<File>> commentToFile = new HashMap<>()
final SearchIndex index = new SearchIndex() final SearchIndex index = new SearchIndex()
FileManager(EventBus eventBus, MuWireSettings settings) { FileManager(EventBus eventBus, MuWireSettings settings) {
@@ -65,18 +62,6 @@ class FileManager {
} }
existingFiles.add(sf.getFile()) existingFiles.add(sf.getFile())
String comment = sf.getComment()
if (comment != null) {
comment = DataUtil.readi18nString(Base64.decode(comment))
index.add(comment)
Set<File> existingComment = commentToFile.get(comment)
if(existingComment == null) {
existingComment = new HashSet<>()
commentToFile.put(comment, existingComment)
}
existingComment.add(sf.getFile())
}
index.add(name) index.add(name)
} }
@@ -102,45 +87,9 @@ class FileManager {
} }
} }
String comment = sf.getComment()
if (comment != null) {
Set<File> existingComment = commentToFile.get(comment)
if (existingComment != null) {
existingComment.remove(sf.getFile())
if (existingComment.isEmpty()) {
commentToFile.remove(comment)
index.remove(comment)
}
}
}
index.remove(name) index.remove(name)
} }
void onUICommentEvent(UICommentEvent e) {
if (e.oldComment != null) {
def comment = DataUtil.readi18nString(Base64.decode(e.oldComment))
Set<File> existingFiles = commentToFile.get(comment)
existingFiles.remove(e.sharedFile.getFile())
if (existingFiles.isEmpty()) {
commentToFile.remove(comment)
index.remove(comment)
}
}
String comment = e.sharedFile.getComment()
comment = DataUtil.readi18nString(Base64.decode(comment))
if (comment != null) {
index.add(comment)
Set<File> existingComment = commentToFile.get(comment)
if(existingComment == null) {
existingComment = new HashSet<>()
commentToFile.put(comment, existingComment)
}
existingComment.add(e.sharedFile.getFile())
}
}
Map<File, SharedFile> getSharedFiles() { Map<File, SharedFile> getSharedFiles() {
synchronized(fileToSharedFile) { synchronized(fileToSharedFile) {
return new HashMap<>(fileToSharedFile) return new HashMap<>(fileToSharedFile)
@@ -163,15 +112,10 @@ class FileManager {
} else { } else {
def names = index.search e.searchTerms def names = index.search e.searchTerms
Set<File> files = new HashSet<>() Set<File> files = new HashSet<>()
names.each { names.each { files.addAll nameToFiles.getOrDefault(it, []) }
files.addAll nameToFiles.getOrDefault(it, [])
if (e.searchComments)
files.addAll commentToFile.getOrDefault(it, [])
}
Set<SharedFile> sharedFiles = new HashSet<>() Set<SharedFile> sharedFiles = new HashSet<>()
files.each { sharedFiles.add fileToSharedFile[it] } files.each { sharedFiles.add fileToSharedFile[it] }
files = filter(sharedFiles, e.oobInfohash) files = filter(sharedFiles, e.oobInfohash)
if (!sharedFiles.isEmpty()) if (!sharedFiles.isEmpty())
re = new ResultsEvent(results: sharedFiles.asList(), uuid: e.uuid, searchEvent: e) re = new ResultsEvent(results: sharedFiles.asList(), uuid: e.uuid, searchEvent: e)
@@ -191,16 +135,4 @@ class FileManager {
} }
rv rv
} }
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent e) {
e.directory.listFiles().each {
if (it.isDirectory())
eventBus.publish(new DirectoryUnsharedEvent(directory : it))
else {
SharedFile sf = fileToSharedFile.get(it)
if (sf != null)
eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
}
}
}
} }

View File

@@ -5,9 +5,4 @@ import com.muwire.core.Event
class FileSharedEvent extends Event { class FileSharedEvent extends Event {
File file File file
@Override
public String toString() {
return super.toString() + " file: "+file.getAbsolutePath()
}
} }

View File

@@ -4,7 +4,6 @@ import java.util.concurrent.Executor
import java.util.concurrent.Executors import java.util.concurrent.Executors
import com.muwire.core.EventBus import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import com.muwire.core.SharedFile import com.muwire.core.SharedFile
class HasherService { class HasherService {
@@ -12,15 +11,12 @@ class HasherService {
final FileHasher hasher final FileHasher hasher
final EventBus eventBus final EventBus eventBus
final FileManager fileManager final FileManager fileManager
final Set<File> hashed = new HashSet<>()
final MuWireSettings settings
Executor executor Executor executor
HasherService(FileHasher hasher, EventBus eventBus, FileManager fileManager, MuWireSettings settings) { HasherService(FileHasher hasher, EventBus eventBus, FileManager fileManager) {
this.hasher = hasher this.hasher = hasher
this.eventBus = eventBus this.eventBus = eventBus
this.fileManager = fileManager this.fileManager = fileManager
this.settings = settings
} }
void start() { void start() {
@@ -28,33 +24,21 @@ class HasherService {
} }
void onFileSharedEvent(FileSharedEvent evt) { void onFileSharedEvent(FileSharedEvent evt) {
File canonical = evt.file.getCanonicalFile() if (fileManager.fileToSharedFile.containsKey(evt.file))
if (!settings.shareHiddenFiles && canonical.isHidden())
return return
if (fileManager.fileToSharedFile.containsKey(canonical)) executor.execute( { -> process(evt.file) } as Runnable)
return
if (hashed.add(canonical))
executor.execute( { -> process(canonical) } as Runnable)
}
void onFileUnsharedEvent(FileUnsharedEvent evt) {
hashed.remove(evt.unsharedFile.file)
}
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent evt) {
hashed.remove(evt.directory)
} }
private void process(File f) { private void process(File f) {
f = f.getCanonicalFile()
if (f.isDirectory()) { if (f.isDirectory()) {
f.listFiles().each {eventBus.publish new FileSharedEvent(file: it) } f.listFiles().each {onFileSharedEvent new FileSharedEvent(file: it) }
} else { } else {
if (f.length() == 0) { if (f.length() == 0) {
eventBus.publish new FileHashedEvent(error: "Not sharing empty file $f") eventBus.publish new FileHashedEvent(error: "Not sharing empty file $f")
} else if (f.length() > FileHasher.MAX_SIZE) { } else if (f.length() > FileHasher.MAX_SIZE) {
eventBus.publish new FileHashedEvent(error: "$f is too large to be shared ${f.length()}") eventBus.publish new FileHashedEvent(error: "$f is too large to be shared ${f.length()}")
} else { } else {
eventBus.publish new FileHashingEvent(hashingFile: f)
def hash = hasher.hashFile f def hash = hasher.hashFile f
eventBus.publish new FileHashedEvent(sharedFile: new SharedFile(f, hash, FileHasher.getPieceSize(f.length()))) eventBus.publish new FileHashedEvent(sharedFile: new SharedFile(f, hash, FileHasher.getPieceSize(f.length())))
} }

View File

@@ -3,9 +3,6 @@ package com.muwire.core.files
import java.nio.file.CopyOption import java.nio.file.CopyOption
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.StandardCopyOption import java.nio.file.StandardCopyOption
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.ThreadFactory
import java.util.logging.Level import java.util.logging.Level
import java.util.stream.Collectors import java.util.stream.Collectors
@@ -14,7 +11,6 @@ import com.muwire.core.EventBus
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
import com.muwire.core.Service import com.muwire.core.Service
import com.muwire.core.SharedFile import com.muwire.core.SharedFile
import com.muwire.core.UILoadedEvent
import com.muwire.core.util.DataUtil import com.muwire.core.util.DataUtil
import groovy.json.JsonOutput import groovy.json.JsonOutput
@@ -31,35 +27,25 @@ class PersisterService extends Service {
final int interval final int interval
final Timer timer final Timer timer
final FileManager fileManager final FileManager fileManager
final ExecutorService persisterExecutor = Executors.newSingleThreadExecutor({ r ->
new Thread(r, "file persister")
} as ThreadFactory)
PersisterService(File location, EventBus listener, int interval, FileManager fileManager) { PersisterService(File location, EventBus listener, int interval, FileManager fileManager) {
this.location = location this.location = location
this.listener = listener this.listener = listener
this.interval = interval this.interval = interval
this.fileManager = fileManager this.fileManager = fileManager
timer = new Timer("file persister timer", true) timer = new Timer("file persister", true)
}
void start() {
timer.schedule({load()} as TimerTask, 1)
} }
void stop() { void stop() {
timer.cancel() timer.cancel()
} }
void onUILoadedEvent(UILoadedEvent e) {
timer.schedule({load()} as TimerTask, 1)
}
void onUIPersistFilesEvent(UIPersistFilesEvent e) {
persistFiles()
}
void load() { void load() {
Thread.currentThread().setPriority(Thread.MIN_PRIORITY)
if (location.exists() && location.isFile()) { if (location.exists() && location.isFile()) {
int loaded = 0
def slurper = new JsonSlurper() def slurper = new JsonSlurper()
try { try {
location.eachLine { location.eachLine {
@@ -69,9 +55,6 @@ class PersisterService extends Service {
if (event != null) { if (event != null) {
log.fine("loaded file $event.loadedFile.file") log.fine("loaded file $event.loadedFile.file")
listener.publish event listener.publish event
loaded++
if (loaded % 10 == 0)
Thread.sleep(20)
} }
} }
} }
@@ -125,19 +108,16 @@ class PersisterService extends Service {
List sources = (List)json.sources List sources = (List)json.sources
Set<Destination> sourceSet = sources.stream().map({d -> new Destination(d.toString())}).collect Collectors.toSet() Set<Destination> sourceSet = sources.stream().map({d -> new Destination(d.toString())}).collect Collectors.toSet()
DownloadedFile df = new DownloadedFile(file, ih, pieceSize, sourceSet) DownloadedFile df = new DownloadedFile(file, ih, pieceSize, sourceSet)
df.setComment(json.comment)
return new FileLoadedEvent(loadedFile : df) return new FileLoadedEvent(loadedFile : df)
} }
SharedFile sf = new SharedFile(file, ih, pieceSize) SharedFile sf = new SharedFile(file, ih, pieceSize)
sf.setComment(json.comment)
return new FileLoadedEvent(loadedFile: sf) return new FileLoadedEvent(loadedFile: sf)
} }
private void persistFiles() { private void persistFiles() {
persisterExecutor.submit( {
def sharedFiles = fileManager.getSharedFiles() def sharedFiles = fileManager.getSharedFiles()
File tmp = File.createTempFile("muwire-files", "tmp") File tmp = File.createTempFile("muwire-files", "tmp")
@@ -151,18 +131,21 @@ class PersisterService extends Service {
} }
Files.copy(tmp.toPath(), location.toPath(), StandardCopyOption.REPLACE_EXISTING) Files.copy(tmp.toPath(), location.toPath(), StandardCopyOption.REPLACE_EXISTING)
tmp.delete() tmp.delete()
} as Runnable)
} }
private def toJson(File f, SharedFile sf) { private def toJson(File f, SharedFile sf) {
def json = [:] def json = [:]
json.file = sf.getB64EncodedFileName() json.file = Base64.encode DataUtil.encodei18nString(f.getCanonicalFile().toString())
json.length = sf.getCachedLength() json.length = f.length()
InfoHash ih = sf.getInfoHash() InfoHash ih = sf.getInfoHash()
json.infoHash = sf.getB64EncodedHashRoot() json.infoHash = Base64.encode ih.getRoot()
json.pieceSize = sf.getPieceSize() json.pieceSize = sf.getPieceSize()
json.hashList = sf.getB64EncodedHashList() byte [] tmp = new byte [32]
json.comment = sf.getComment() json.hashList = []
for (int i = 0;i < ih.getHashList().length / 32; i++) {
System.arraycopy(ih.getHashList(), i * 32, tmp, 0, 32)
json.hashList.add Base64.encode(tmp)
}
if (sf instanceof DownloadedFile) { if (sf instanceof DownloadedFile) {
json.sources = sf.sources.stream().map( {d -> d.toBase64()}).collect(Collectors.toList()) json.sources = sf.sources.stream().map( {d -> d.toBase64()}).collect(Collectors.toList())

View File

@@ -1,9 +0,0 @@
package com.muwire.core.files
import com.muwire.core.Event
import com.muwire.core.SharedFile
class UICommentEvent extends Event {
SharedFile sharedFile
String oldComment
}

View File

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

View File

@@ -6,12 +6,7 @@ class CacheServers {
private static final int TO_GIVE = 3 private static final int TO_GIVE = 3
private static Set<Destination> CACHES = [ private static Set<Destination> CACHES = [
// zlatinb new Destination("Wddh2E6FyyXBF7SvUYHKdN-vjf3~N6uqQWNeBDTM0P33YjiQCOsyedrjmDZmWFrXUJfJLWnCb5bnKezfk4uDaMyj~uvDG~yvLVcFgcPWSUd7BfGgym-zqcG1q1DcM8vfun-US7YamBlmtC6MZ2j-~Igqzmgshita8aLPCfNAA6S6e2UMjjtG7QIXlxpMec75dkHdJlVWbzrk9z8Qgru3YIk0UztYgEwDNBbm9wInsbHhr3HtAfa02QcgRVqRN2PnQXuqUJs7R7~09FZPEviiIcUpkY3FeyLlX1sgQFBeGeA96blaPvZNGd6KnNdgfLgMebx5SSxC-N4KZMSMBz5cgonQF3~m2HHFRSI85zqZNG5X9bJN85t80ltiv1W1es8ZnQW4es11r7MrvJNXz5bmSH641yJIvS6qI8OJJNpFVBIQSXLD-96TayrLQPaYw~uNZ-eXaE6G5dYhiuN8xHsFI1QkdaUaVZnvDGfsRbpS5GtpUbBDbyLkdPurG0i7dN1wAAAA")
new Destination("Wddh2E6FyyXBF7SvUYHKdN-vjf3~N6uqQWNeBDTM0P33YjiQCOsyedrjmDZmWFrXUJfJLWnCb5bnKezfk4uDaMyj~uvDG~yvLVcFgcPWSUd7BfGgym-zqcG1q1DcM8vfun-US7YamBlmtC6MZ2j-~Igqzmgshita8aLPCfNAA6S6e2UMjjtG7QIXlxpMec75dkHdJlVWbzrk9z8Qgru3YIk0UztYgEwDNBbm9wInsbHhr3HtAfa02QcgRVqRN2PnQXuqUJs7R7~09FZPEviiIcUpkY3FeyLlX1sgQFBeGeA96blaPvZNGd6KnNdgfLgMebx5SSxC-N4KZMSMBz5cgonQF3~m2HHFRSI85zqZNG5X9bJN85t80ltiv1W1es8ZnQW4es11r7MrvJNXz5bmSH641yJIvS6qI8OJJNpFVBIQSXLD-96TayrLQPaYw~uNZ-eXaE6G5dYhiuN8xHsFI1QkdaUaVZnvDGfsRbpS5GtpUbBDbyLkdPurG0i7dN1wAAAA"),
// sNL
new Destination("JC63wJNOqSJmymkj4~UJWywBTvDGikKMoYP0HX2Wz9c5l3otXSkwnxWAFL4cKr~Ygh3BNNi2t93vuLIiI1W8AsE42kR~PwRx~Y-WvIHXR6KUejRmOp-n8WidtjKg9k4aDy428uSOedqXDxys5mpoeQXwDsv1CoPTTwnmb1GWFy~oTGIsCguCl~aJWGnqiKarPO3GJQ~ev-NbvAQzUfC3HeP1e6pdI5CGGjExahTCID5UjpJw8GaDXWlGmYWWH303Xu4x-vAHQy1dJLsOBCn8dZravsn5BKJk~j0POUon45CCx-~NYtaPe0Itt9cMdD2ciC76Rep1D0X0sm1SjlSs8sZ52KmF3oaLZ6OzgI9QLMIyBUrfi41sK5I0qTuUVBAkvW1xr~L-20dYJ9TrbOaOb2-vDIfKaxVi6xQOuhgQDiSBhd3qv2m0xGu-BM9DQYfNA0FdMjnZmqjmji9RMavzQSsVFIbQGLbrLepiEFlb7TseCK5UtRp8TxnG7L4gbYevBQAEAAcAAA=="),
// dark_trion
new Destination("Gec9L29FVcQvYDgpcYuEYdltJn06PPoOWAcAM8Af-gDm~ehlrJcwlLXXs0hidq~yP2A0X7QcDi6i6shAfuEofTchxGJl8LRNqj9lio7WnB7cIixXWL~uCkD7Np5LMX0~akNX34oOb9RcBYVT2U5rFGJmJ7OtBv~IBkGeLhsMrqaCjahd0jdBO~QJ-t82ZKZhh044d24~JEfF9zSJxdBoCdAcXzryGNy7sYtFVDFsPKJudAxSW-UsSQiGw2~k-TxyF0r-iAt1IdzfNu8Lu0WPqLdhDYJWcPldx2PR5uJorI~zo~z3I5RX3NwzarlbD4nEP5s65ahPSfVCEkzmaJUBgP8DvBqlFaX89K4nGRYc7jkEjJ8cX4L6YPXUpTPWcfKkW259WdQY3YFh6x7rzijrGZewpczOLCrt-bZRYgDrUibmZxKZmNhy~lQu4gYVVjkz1i4tL~DWlhIc4y0x2vItwkYLArPPi~ejTnt-~Lhb7oPMXRcWa3UrwGKpFvGZY4NXBQAEAAcAAA==")
] ]
static List<Destination> getCacheServers() { static List<Destination> getCacheServers() {

View File

@@ -7,39 +7,20 @@ class Host {
private static final int MAX_FAILURES = 3 private static final int MAX_FAILURES = 3
final Destination destination final Destination destination
private final int clearInterval, hopelessInterval, rejectionInterval
int failures,successes int failures,successes
long lastAttempt
long lastSuccessfulAttempt
long lastRejection
public Host(Destination destination, int clearInterval, int hopelessInterval, int rejectionInterval) { public Host(Destination destination) {
this.destination = destination this.destination = destination
this.clearInterval = clearInterval
this.hopelessInterval = hopelessInterval
this.rejectionInterval = rejectionInterval
}
private void connectSuccessful() {
failures = 0
successes++
lastAttempt = System.currentTimeMillis()
} }
synchronized void onConnect() { synchronized void onConnect() {
connectSuccessful() failures = 0
lastSuccessfulAttempt = lastAttempt successes++
}
synchronized void onReject() {
connectSuccessful()
lastRejection = lastAttempt;
} }
synchronized void onFailure() { synchronized void onFailure() {
failures++ failures++
successes = 0 successes = 0
lastAttempt = System.currentTimeMillis()
} }
synchronized boolean isFailed() { synchronized boolean isFailed() {
@@ -53,18 +34,4 @@ class Host {
synchronized void clearFailures() { synchronized void clearFailures() {
failures = 0 failures = 0
} }
synchronized boolean canTryAgain() {
lastSuccessfulAttempt > 0 &&
System.currentTimeMillis() - lastAttempt > (clearInterval * 60 * 1000)
}
synchronized boolean isHopeless() {
isFailed() &&
System.currentTimeMillis() - lastSuccessfulAttempt > (hopelessInterval * 60 * 1000)
}
synchronized boolean isRecentlyRejected() {
System.currentTimeMillis() - lastRejection < (rejectionInterval * 60 * 1000)
}
} }

View File

@@ -52,7 +52,7 @@ class HostCache extends Service {
hosts.get(e.destination).clearFailures() hosts.get(e.destination).clearFailures()
return return
} }
Host host = new Host(e.destination, settings.hostClearInterval, settings.hostHopelessInterval, settings.hostRejectInterval) Host host = new Host(e.destination)
if (allowHost(host)) { if (allowHost(host)) {
hosts.put(e.destination, host) hosts.put(e.destination, host)
} }
@@ -64,16 +64,14 @@ class HostCache extends Service {
Destination dest = e.endpoint.destination Destination dest = e.endpoint.destination
Host host = hosts.get(dest) Host host = hosts.get(dest)
if (host == null) { if (host == null) {
host = new Host(dest, settings.hostClearInterval, settings.hostHopelessInterval, settings.hostRejectInterval) host = new Host(dest)
hosts.put(dest, host) hosts.put(dest, host)
} }
switch(e.status) { switch(e.status) {
case ConnectionAttemptStatus.SUCCESSFUL: case ConnectionAttemptStatus.SUCCESSFUL:
host.onConnect()
break
case ConnectionAttemptStatus.REJECTED: case ConnectionAttemptStatus.REJECTED:
host.onReject() host.onConnect()
break break
case ConnectionAttemptStatus.FAILED: case ConnectionAttemptStatus.FAILED:
host.onFailure() host.onFailure()
@@ -84,10 +82,6 @@ class HostCache extends Service {
List<Destination> getHosts(int n) { List<Destination> getHosts(int n) {
List<Destination> rv = new ArrayList<>(hosts.keySet()) List<Destination> rv = new ArrayList<>(hosts.keySet())
rv.retainAll {allowHost(hosts[it])} rv.retainAll {allowHost(hosts[it])}
rv.removeAll {
def h = hosts[it];
(h.isFailed() && !h.canTryAgain()) || h.isRecentlyRejected()
}
if (rv.size() <= n) if (rv.size() <= n)
return rv return rv
Collections.shuffle(rv) Collections.shuffle(rv)
@@ -106,37 +100,15 @@ class HostCache extends Service {
rv[0..n-1] rv[0..n-1]
} }
int countFailingHosts() {
List<Destination> rv = new ArrayList<>(hosts.keySet())
rv.retainAll {
hosts[it].isFailed()
}
rv.size()
}
int countHopelessHosts() {
List<Destination> rv = new ArrayList<>(hosts.keySet())
rv.retainAll {
hosts[it].isHopeless()
}
rv.size()
}
void load() { void load() {
if (storage.exists()) { if (storage.exists()) {
JsonSlurper slurper = new JsonSlurper() JsonSlurper slurper = new JsonSlurper()
storage.eachLine { storage.eachLine {
def entry = slurper.parseText(it) def entry = slurper.parseText(it)
Destination dest = new Destination(entry.destination) Destination dest = new Destination(entry.destination)
Host host = new Host(dest, settings.hostClearInterval, settings.hostHopelessInterval, settings.hostRejectInterval) Host host = new Host(dest)
host.failures = Integer.valueOf(String.valueOf(entry.failures)) host.failures = Integer.valueOf(String.valueOf(entry.failures))
host.successes = Integer.valueOf(String.valueOf(entry.successes)) host.successes = Integer.valueOf(String.valueOf(entry.successes))
if (entry.lastAttempt != null)
host.lastAttempt = entry.lastAttempt
if (entry.lastSuccessfulAttempt != null)
host.lastSuccessfulAttempt = entry.lastSuccessfulAttempt
if (entry.lastRejection != null)
host.lastRejection = entry.lastRejection
if (allowHost(host)) if (allowHost(host))
hosts.put(dest, host) hosts.put(dest, host)
} }
@@ -146,6 +118,8 @@ class HostCache extends Service {
} }
private boolean allowHost(Host host) { private boolean allowHost(Host host) {
if (host.isFailed())
return false
if (host.destination == myself) if (host.destination == myself)
return false return false
TrustLevel trust = trustService.getLevel(host.destination) TrustLevel trust = trustService.getLevel(host.destination)
@@ -164,14 +138,11 @@ class HostCache extends Service {
storage.delete() storage.delete()
storage.withPrintWriter { writer -> storage.withPrintWriter { writer ->
hosts.each { dest, host -> hosts.each { dest, host ->
if (allowHost(host) && !host.isHopeless()) { if (allowHost(host)) {
def map = [:] def map = [:]
map.destination = dest.toBase64() map.destination = dest.toBase64()
map.failures = host.failures map.failures = host.failures
map.successes = host.successes map.successes = host.successes
map.lastAttempt = host.lastAttempt
map.lastSuccessfulAttempt = host.lastSuccessfulAttempt
map.lastRejection = host.lastRejection
def json = JsonOutput.toJson(map) def json = JsonOutput.toJson(map)
writer.println json writer.println json
} }

View File

@@ -1,28 +0,0 @@
package com.muwire.core.mesh
import com.muwire.core.InfoHash
import com.muwire.core.Persona
import com.muwire.core.download.Pieces
import net.i2p.data.Destination
import net.i2p.util.ConcurrentHashSet
class Mesh {
private final InfoHash infoHash
private final Set<Persona> sources = new ConcurrentHashSet<>()
private final Pieces pieces
Mesh(InfoHash infoHash, Pieces pieces) {
this.infoHash = infoHash
this.pieces = pieces
}
Set<Persona> getRandom(int n, Persona exclude) {
List<Persona> tmp = new ArrayList<>(sources)
tmp.remove(exclude)
Collections.shuffle(tmp)
if (tmp.size() < n)
return tmp
tmp[0..n-1]
}
}

View File

@@ -1,103 +0,0 @@
package com.muwire.core.mesh
import java.util.stream.Collectors
import com.muwire.core.Constants
import com.muwire.core.InfoHash
import com.muwire.core.MuWireSettings
import com.muwire.core.Persona
import com.muwire.core.download.Pieces
import com.muwire.core.download.SourceDiscoveredEvent
import com.muwire.core.files.FileManager
import com.muwire.core.util.DataUtil
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import net.i2p.data.Base64
class MeshManager {
private final Map<InfoHash, Mesh> meshes = Collections.synchronizedMap(new HashMap<>())
private final FileManager fileManager
private final File home
private final MuWireSettings settings
MeshManager(FileManager fileManager, File home, MuWireSettings settings) {
this.fileManager = fileManager
this.home = home
this.settings = settings
load()
}
Mesh get(InfoHash infoHash) {
meshes.get(infoHash)
}
Mesh getOrCreate(InfoHash infoHash, int nPieces, boolean sequential) {
synchronized(meshes) {
if (meshes.containsKey(infoHash))
return meshes.get(infoHash)
float ratio = sequential ? 0f : settings.downloadSequentialRatio
Pieces pieces = new Pieces(nPieces, ratio)
if (fileManager.rootToFiles.containsKey(infoHash)) {
for (int i = 0; i < nPieces; i++)
pieces.markDownloaded(i)
}
Mesh rv = new Mesh(infoHash, pieces)
meshes.put(infoHash, rv)
return rv
}
}
void onSourceDiscoveredEvent(SourceDiscoveredEvent e) {
Mesh mesh = meshes.get(e.infoHash)
if (mesh == null)
return
mesh.sources.add(e.source)
save()
}
private void save() {
File meshFile = new File(home, "mesh.json")
synchronized(meshes) {
meshFile.withPrintWriter { writer ->
meshes.values().each { mesh ->
def json = [:]
json.timestamp = System.currentTimeMillis()
json.infoHash = Base64.encode(mesh.infoHash.getRoot())
json.sources = mesh.sources.stream().map({it.toBase64()}).collect(Collectors.toList())
json.nPieces = mesh.pieces.nPieces
json.xHave = DataUtil.encodeXHave(mesh.pieces.downloaded, mesh.pieces.nPieces)
writer.println(JsonOutput.toJson(json))
}
}
}
}
private void load() {
File meshFile = new File(home, "mesh.json")
if (!meshFile.exists())
return
long now = System.currentTimeMillis()
JsonSlurper slurper = new JsonSlurper()
meshFile.eachLine {
def json = slurper.parseText(it)
if (now - json.timestamp > settings.meshExpiration * 60 * 1000)
return
InfoHash infoHash = new InfoHash(Base64.decode(json.infoHash))
Pieces pieces = new Pieces(json.nPieces, settings.downloadSequentialRatio)
Mesh mesh = new Mesh(infoHash, pieces)
json.sources.each { source ->
Persona persona = new Persona(new ByteArrayInputStream(Base64.decode(source)))
mesh.sources.add(persona)
}
if (json.xHave != null)
DataUtil.decodeXHave(json.xHave).each { pieces.markDownloaded(it) }
if (!mesh.sources.isEmpty())
meshes.put(infoHash, mesh)
}
}
}

View File

@@ -1,87 +0,0 @@
package com.muwire.core.search
import com.muwire.core.Constants
import com.muwire.core.EventBus
import com.muwire.core.connection.Endpoint
import com.muwire.core.connection.I2PConnector
import com.muwire.core.util.DataUtil
import groovy.json.JsonSlurper
import groovy.util.logging.Log
import java.nio.charset.StandardCharsets
import java.util.concurrent.Executor
import java.util.concurrent.Executors
import java.util.logging.Level
import java.util.zip.GZIPInputStream
@Log
class BrowseManager {
private final I2PConnector connector
private final EventBus eventBus
private final Executor browserThread = Executors.newSingleThreadExecutor()
BrowseManager(I2PConnector connector, EventBus eventBus) {
this.connector = connector
this.eventBus = eventBus
}
void onUIBrowseEvent(UIBrowseEvent e) {
browserThread.execute({
Endpoint endpoint = null
try {
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.CONNECTING))
endpoint = connector.connect(e.host.destination)
OutputStream os = endpoint.getOutputStream()
os.write("BROWSE\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
InputStream is = endpoint.getInputStream()
String code = DataUtil.readTillRN(is)
if (!code.startsWith("200"))
throw new IOException("Invalid code $code")
// parse all headers
Map<String,String> headers = new HashMap<>()
String header
while((header = DataUtil.readTillRN(is)) != "" && headers.size() < Constants.MAX_HEADERS) {
int colon = header.indexOf(':')
if (colon == -1 || colon == header.length() - 1)
throw new IOException("invalid header $header")
String key = header.substring(0, colon)
String value = header.substring(colon + 1)
headers[key] = value.trim()
}
if (!headers.containsKey("Count"))
throw new IOException("No count header")
int results = Integer.parseInt(headers['Count'])
// at this stage, start pulling the results
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.FETCHING, totalResults : results))
JsonSlurper slurper = new JsonSlurper()
DataInputStream dis = new DataInputStream(new GZIPInputStream(is))
UUID uuid = UUID.randomUUID()
for (int i = 0; i < results; i++) {
int size = dis.readUnsignedShort()
byte [] tmp = new byte[size]
dis.readFully(tmp)
def json = slurper.parse(tmp)
UIResultEvent result = ResultsParser.parse(e.host, uuid, json)
eventBus.publish(result)
}
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.FINISHED))
} catch (Exception bad) {
log.log(Level.WARNING, "browse failed", bad)
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.FAILED))
} finally {
endpoint?.close()
}
} as Runnable)
}
}

View File

@@ -1,5 +0,0 @@
package com.muwire.core.search;
public enum BrowseStatus {
CONNECTING, FETCHING, FINISHED, FAILED
}

View File

@@ -1,8 +0,0 @@
package com.muwire.core.search
import com.muwire.core.Event
class BrowseStatusEvent extends Event {
BrowseStatus status
int totalResults
}

View File

@@ -1,7 +1,5 @@
package com.muwire.core.search package com.muwire.core.search
import java.util.stream.Collectors
import javax.naming.directory.InvalidSearchControlsException import javax.naming.directory.InvalidSearchControlsException
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
@@ -9,7 +7,6 @@ import com.muwire.core.Persona
import com.muwire.core.util.DataUtil import com.muwire.core.util.DataUtil
import net.i2p.data.Base64 import net.i2p.data.Base64
import net.i2p.data.Destination
class ResultsParser { class ResultsParser {
public static UIResultEvent parse(Persona p, UUID uuid, def json) throws InvalidSearchResultException { public static UIResultEvent parse(Persona p, UUID uuid, def json) throws InvalidSearchResultException {
@@ -61,7 +58,6 @@ class ResultsParser {
size : size, size : size,
infohash : parsedIH, infohash : parsedIH,
pieceSize : pieceSize, pieceSize : pieceSize,
sources : Collections.emptySet(),
uuid : uuid) uuid : uuid)
} catch (Exception e) { } catch (Exception e) {
throw new InvalidSearchResultException("parsing search result failed",e) throw new InvalidSearchResultException("parsing search result failed",e)
@@ -86,27 +82,11 @@ class ResultsParser {
if (infoHash.length != InfoHash.SIZE) if (infoHash.length != InfoHash.SIZE)
throw new InvalidSearchResultException("invalid infohash size $infoHash.length") throw new InvalidSearchResultException("invalid infohash size $infoHash.length")
int pieceSize = json.pieceSize int pieceSize = json.pieceSize
Set<Destination> sources = Collections.emptySet()
if (json.sources != null)
sources = json.sources.stream().map({new Destination(it)}).collect(Collectors.toSet())
String comment = null
if (json.comment != null)
comment = DataUtil.readi18nString(Base64.decode(json.comment))
boolean browse = false
if (json.browse != null)
browse = json.browse
return new UIResultEvent( sender : p, return new UIResultEvent( sender : p,
name : name, name : name,
size : size, size : size,
infohash : new InfoHash(infoHash), infohash : new InfoHash(infoHash),
pieceSize : pieceSize, pieceSize : pieceSize,
sources : sources,
comment : comment,
browse : browse,
uuid: uuid) 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

@@ -4,7 +4,6 @@ import com.muwire.core.SharedFile
import com.muwire.core.connection.Endpoint import com.muwire.core.connection.Endpoint
import com.muwire.core.connection.I2PConnector import com.muwire.core.connection.I2PConnector
import com.muwire.core.files.FileHasher import com.muwire.core.files.FileHasher
import com.muwire.core.util.DataUtil
import com.muwire.core.Persona import com.muwire.core.Persona
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
@@ -12,14 +11,9 @@ import java.util.concurrent.Executor
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.ThreadFactory import java.util.concurrent.ThreadFactory
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import java.util.logging.Level
import java.util.stream.Collectors
import java.util.zip.GZIPOutputStream
import com.muwire.core.DownloadedFile
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 groovy.json.JsonOutput import groovy.json.JsonOutput
import groovy.util.logging.Log import groovy.util.logging.Log
@@ -45,46 +39,33 @@ class ResultsSender {
private final I2PConnector connector private final I2PConnector connector
private final Persona me private final Persona me
private final EventBus eventBus private final EventBus eventBus
private final MuWireSettings settings
ResultsSender(EventBus eventBus, I2PConnector connector, Persona me, MuWireSettings settings) { ResultsSender(EventBus eventBus, I2PConnector connector, Persona me) {
this.connector = connector; this.connector = connector;
this.eventBus = eventBus this.eventBus = eventBus
this.me = me this.me = me
this.settings = settings
} }
void sendResults(UUID uuid, SharedFile[] results, Destination target, boolean oobInfohash, boolean compressedResults) { void sendResults(UUID uuid, SharedFile[] results, Destination target, boolean oobInfohash) {
log.info("Sending $results.length results for uuid $uuid to ${target.toBase32()} oobInfohash : $oobInfohash") log.info("Sending $results.length results for uuid $uuid to ${target.toBase32()} oobInfohash : $oobInfohash")
if (target.equals(me.destination)) { if (target.equals(me.destination)) {
def uiResultEvents = []
results.each { results.each {
long length = it.getFile().length() long length = it.getFile().length()
int pieceSize = it.getPieceSize() int pieceSize = it.getPieceSize()
if (pieceSize == 0) if (pieceSize == 0)
pieceSize = FileHasher.getPieceSize(length) pieceSize = FileHasher.getPieceSize(length)
Set<Destination> suggested = Collections.emptySet()
if (it instanceof DownloadedFile)
suggested = it.sources
def comment = null
if (it.getComment() != null) {
comment = DataUtil.readi18nString(Base64.decode(it.getComment()))
}
def uiResultEvent = new UIResultEvent( sender : me, def uiResultEvent = new UIResultEvent( sender : me,
name : it.getFile().getName(), name : it.getFile().getName(),
size : length, size : length,
infohash : it.getInfoHash(), infohash : it.getInfoHash(),
pieceSize : pieceSize, pieceSize : pieceSize,
uuid : uuid, uuid : uuid
sources : suggested,
comment : comment
) )
uiResultEvents << uiResultEvent eventBus.publish(uiResultEvent)
} }
eventBus.publish(new UIResultBatchEvent(uuid : uuid, results : uiResultEvents))
} else { } else {
executor.execute(new ResultSendJob(uuid : uuid, results : results, executor.execute(new ResultSendJob(uuid : uuid, results : results,
target: target, oobInfohash : oobInfohash, compressedResults : compressedResults)) target: target, oobInfohash : oobInfohash))
} }
} }
@@ -93,14 +74,12 @@ class ResultsSender {
SharedFile [] results SharedFile [] results
Destination target Destination target
boolean oobInfohash boolean oobInfohash
boolean compressedResults
@Override @Override
public void run() { public void run() {
try { byte [] tmp = new byte[InfoHash.SIZE]
JsonOutput jsonOutput = new JsonOutput() JsonOutput jsonOutput = new JsonOutput()
Endpoint endpoint = null; Endpoint endpoint = null;
if (!compressedResults) {
try { try {
endpoint = connector.connect(target) endpoint = connector.connect(target)
DataOutputStream os = new DataOutputStream(endpoint.getOutputStream()) DataOutputStream os = new DataOutputStream(endpoint.getOutputStream())
@@ -108,43 +87,7 @@ class ResultsSender {
me.write(os) me.write(os)
os.writeShort((short)results.length) os.writeShort((short)results.length)
results.each { results.each {
def obj = sharedFileToObj(it, settings.browseFiles) byte [] name = it.getFile().getName().getBytes(StandardCharsets.UTF_8)
def json = jsonOutput.toJson(obj)
os.writeShort((short)json.length())
os.write(json.getBytes(StandardCharsets.US_ASCII))
}
os.flush()
} finally {
endpoint?.close()
}
} else {
try {
endpoint = connector.connect(target)
OutputStream os = endpoint.getOutputStream()
os.write("RESULTS $uuid\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("Sender: ${me.toBase64()}\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("Count: $results.length\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))
results.each {
def obj = sharedFileToObj(it, settings.browseFiles)
def json = jsonOutput.toJson(obj)
dos.writeShort((short)json.length())
dos.write(json.getBytes(StandardCharsets.US_ASCII))
}
dos.close()
} finally {
endpoint?.close()
}
}
} catch (Exception e) {
log.log(Level.WARNING, "problem sending results",e)
}
}
}
public static def sharedFileToObj(SharedFile sf, boolean browseFiles) {
byte [] name = sf.getFile().getName().getBytes(StandardCharsets.UTF_8)
def baos = new ByteArrayOutputStream() def baos = new ByteArrayOutputStream()
def daos = new DataOutputStream(baos) def daos = new DataOutputStream(baos)
daos.writeShort((short) name.length) daos.writeShort((short) name.length)
@@ -153,19 +96,28 @@ class ResultsSender {
String encodedName = Base64.encode(baos.toByteArray()) String encodedName = Base64.encode(baos.toByteArray())
def obj = [:] def obj = [:]
obj.type = "Result" obj.type = "Result"
obj.version = 2 obj.version = oobInfohash ? 2 : 1
obj.name = encodedName obj.name = encodedName
obj.infohash = Base64.encode(sf.getInfoHash().getRoot()) obj.infohash = Base64.encode(it.getInfoHash().getRoot())
obj.size = sf.getCachedLength() obj.size = it.getFile().length()
obj.pieceSize = sf.getPieceSize() obj.pieceSize = it.getPieceSize()
if (!oobInfohash) {
if (sf instanceof DownloadedFile) byte [] hashList = it.getInfoHash().getHashList()
obj.sources = sf.sources.stream().map({dest -> dest.toBase64()}).collect(Collectors.toSet()) def hashListB64 = []
for (int i = 0; i < hashList.length / InfoHash.SIZE; i++) {
if (sf.getComment() != null) System.arraycopy(hashList, InfoHash.SIZE * i, tmp, 0, InfoHash.SIZE)
obj.comment = sf.getComment() hashListB64 << Base64.encode(tmp)
}
obj.browse = browseFiles obj.hashList = hashListB64
obj }
def json = jsonOutput.toJson(obj)
os.writeShort((short)json.length())
os.write(json.getBytes(StandardCharsets.US_ASCII))
}
os.flush()
} finally {
endpoint?.close()
}
}
} }
} }

View File

@@ -9,13 +9,11 @@ class SearchEvent extends Event {
byte [] searchHash byte [] searchHash
UUID uuid UUID uuid
boolean oobInfohash boolean oobInfohash
boolean searchComments
boolean compressedResults
String toString() { String toString() {
def infoHash = null def infoHash = null
if (searchHash != null) if (searchHash != null)
infoHash = new InfoHash(searchHash) infoHash = new InfoHash(searchHash)
"searchTerms: $searchTerms searchHash:$infoHash, uuid:$uuid oobInfohash:$oobInfohash searchComments:$searchComments compressedResults:$compressedResults" "searchTerms: $searchTerms searchHash:$infoHash, uuid:$uuid oobInfohash:$oobInfohash"
} }
} }

View File

@@ -1,6 +1,6 @@
package com.muwire.core.search package com.muwire.core.search
import com.muwire.core.SplitPattern import com.muwire.core.Constants
class SearchIndex { class SearchIndex {
@@ -32,11 +32,8 @@ class SearchIndex {
} }
private static String[] split(String source) { private static String[] split(String source) {
source = source.replaceAll(SplitPattern.SPLIT_PATTERN, " ").toLowerCase() source = source.replaceAll(Constants.SPLIT_PATTERN, " ").toLowerCase()
String [] split = source.split(" ") source.split(" ")
def rv = []
split.each { if (it.length() > 0) rv << it }
rv.toArray(new String[0])
} }
String[] search(List<String> terms) { String[] search(List<String> terms) {

View File

@@ -44,7 +44,7 @@ public class SearchManager {
log.info("No results for search uuid $event.uuid") log.info("No results for search uuid $event.uuid")
return return
} }
resultsSender.sendResults(event.uuid, event.results, target, event.searchEvent.oobInfohash, event.searchEvent.compressedResults) resultsSender.sendResults(event.uuid, event.results, target, event.searchEvent.oobInfohash)
} }
boolean hasLocalSearch(UUID uuid) { boolean hasLocalSearch(UUID uuid) {

View File

@@ -1,8 +0,0 @@
package com.muwire.core.search
import com.muwire.core.Event
import com.muwire.core.Persona
class UIBrowseEvent extends Event {
Persona host
}

View File

@@ -1,8 +0,0 @@
package com.muwire.core.search
import com.muwire.core.Event
class UIResultBatchEvent extends Event {
UUID uuid
UIResultEvent[] results
}

View File

@@ -4,18 +4,13 @@ import com.muwire.core.Event
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
import com.muwire.core.Persona import com.muwire.core.Persona
import net.i2p.data.Destination
class UIResultEvent extends Event { class UIResultEvent extends Event {
Persona sender Persona sender
Set<Destination> sources
UUID uuid UUID uuid
String name String name
long size long size
InfoHash infohash InfoHash infohash
int pieceSize int pieceSize
String comment
boolean browse
@Override @Override
public String toString() { public String toString() {

View File

@@ -1,31 +0,0 @@
package com.muwire.core.trust
import java.util.concurrent.ConcurrentHashMap
import com.muwire.core.Persona
import net.i2p.util.ConcurrentHashSet
class RemoteTrustList {
public enum Status { NEW, UPDATING, UPDATED, UPDATE_FAILED }
private final Persona persona
private final Set<Persona> good, bad
volatile long timestamp
volatile boolean forceUpdate
Status status = Status.NEW
RemoteTrustList(Persona persona) {
this.persona = persona
good = new ConcurrentHashSet<>()
bad = new ConcurrentHashSet<>()
}
@Override
public boolean equals(Object o) {
if (!(o instanceof RemoteTrustList))
return false
RemoteTrustList other = (RemoteTrustList)o
persona == other.persona
}
}

View File

@@ -1,161 +0,0 @@
package com.muwire.core.trust
import java.nio.charset.StandardCharsets
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.logging.Level
import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import com.muwire.core.Persona
import com.muwire.core.UILoadedEvent
import com.muwire.core.connection.Endpoint
import com.muwire.core.connection.I2PConnector
import com.muwire.core.util.DataUtil
import groovy.util.logging.Log
import net.i2p.data.Destination
@Log
class TrustSubscriber {
private final EventBus eventBus
private final I2PConnector i2pConnector
private final MuWireSettings settings
private final Map<Destination, RemoteTrustList> remoteTrustLists = new ConcurrentHashMap<>()
private final Object waitLock = new Object()
private volatile boolean shutdown
private volatile Thread thread
private final ExecutorService updateThreads = Executors.newCachedThreadPool()
TrustSubscriber(EventBus eventBus, I2PConnector i2pConnector, MuWireSettings settings) {
this.eventBus = eventBus
this.i2pConnector = i2pConnector
this.settings = settings
}
void onUILoadedEvent(UILoadedEvent e) {
thread = new Thread({checkLoop()} as Runnable, "trust-subscriber")
thread.setDaemon(true)
thread.start()
}
void stop() {
shutdown = true
thread?.interrupt()
updateThreads.shutdownNow()
}
void onTrustSubscriptionEvent(TrustSubscriptionEvent e) {
if (!e.subscribe) {
remoteTrustLists.remove(e.persona.destination)
} else {
RemoteTrustList trustList = remoteTrustLists.putIfAbsent(e.persona.destination, new RemoteTrustList(e.persona))
trustList?.forceUpdate = true
synchronized(waitLock) {
waitLock.notify()
}
}
}
private void checkLoop() {
try {
while(!shutdown) {
synchronized(waitLock) {
waitLock.wait(60 * 1000)
}
final long now = System.currentTimeMillis()
remoteTrustLists.values().each { trustList ->
if (trustList.status == RemoteTrustList.Status.UPDATING)
return
if (!trustList.forceUpdate &&
now - trustList.timestamp < settings.trustListInterval * 60 * 60 * 1000)
return
trustList.forceUpdate = false
updateThreads.submit(new UpdateJob(trustList))
}
}
} catch (InterruptedException e) {
if (!shutdown)
throw e
}
}
private class UpdateJob implements Runnable {
private final RemoteTrustList trustList
UpdateJob(RemoteTrustList trustList) {
this.trustList = trustList
}
public void run() {
trustList.status = RemoteTrustList.Status.UPDATING
eventBus.publish(new TrustSubscriptionUpdatedEvent(trustList : trustList))
if (check(trustList, System.currentTimeMillis()))
trustList.status = RemoteTrustList.Status.UPDATED
else
trustList.status = RemoteTrustList.Status.UPDATE_FAILED
eventBus.publish(new TrustSubscriptionUpdatedEvent(trustList : trustList))
}
}
private boolean check(RemoteTrustList trustList, long now) {
log.info("fetching trust list from ${trustList.persona.getHumanReadableName()}")
Endpoint endpoint = null
try {
endpoint = i2pConnector.connect(trustList.persona.destination)
OutputStream os = endpoint.getOutputStream()
InputStream is = endpoint.getInputStream()
os.write("TRUST\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
os.flush()
String codeString = DataUtil.readTillRN(is)
int space = codeString.indexOf(' ')
if (space > 0)
codeString = codeString.substring(0,space)
int code = Integer.parseInt(codeString.trim())
if (code != 200) {
log.info("couldn't fetch trust list, code $code")
return false
}
// swallow any headers
String header
while (( header = DataUtil.readTillRN(is)) != "");
DataInputStream dis = new DataInputStream(is)
Set<Persona> good = new HashSet<>()
int nGood = dis.readUnsignedShort()
for (int i = 0; i < nGood; i++) {
Persona p = new Persona(dis)
good.add(p)
}
Set<Persona> bad = new HashSet<>()
int nBad = dis.readUnsignedShort()
for (int i = 0; i < nBad; i++) {
Persona p = new Persona(dis)
bad.add(p)
}
trustList.timestamp = now
trustList.good.clear()
trustList.good.addAll(good)
trustList.bad.clear()
trustList.bad.addAll(bad)
return true
} catch (Exception e) {
log.log(Level.WARNING,"exception fetching trust list from ${trustList.persona.getHumanReadableName()}",e)
return false
} finally {
endpoint?.close()
}
}
}

View File

@@ -1,9 +0,0 @@
package com.muwire.core.trust
import com.muwire.core.Event
import com.muwire.core.Persona
class TrustSubscriptionEvent extends Event {
Persona persona
boolean subscribe
}

View File

@@ -1,7 +0,0 @@
package com.muwire.core.trust
import com.muwire.core.Event
class TrustSubscriptionUpdatedEvent extends Event {
RemoteTrustList trustList
}

View File

@@ -3,15 +3,7 @@ package com.muwire.core.update
import java.util.logging.Level import java.util.logging.Level
import com.muwire.core.EventBus import com.muwire.core.EventBus
import com.muwire.core.InfoHash
import com.muwire.core.MuWireSettings import com.muwire.core.MuWireSettings
import com.muwire.core.Persona
import com.muwire.core.download.UIDownloadEvent
import com.muwire.core.files.FileDownloadedEvent
import com.muwire.core.files.FileManager
import com.muwire.core.search.QueryEvent
import com.muwire.core.search.SearchEvent
import com.muwire.core.search.UIResultBatchEvent
import groovy.json.JsonOutput import groovy.json.JsonOutput
import groovy.json.JsonSlurper import groovy.json.JsonSlurper
@@ -21,7 +13,6 @@ import net.i2p.client.I2PSessionMuxedListener
import net.i2p.client.SendMessageOptions import net.i2p.client.SendMessageOptions
import net.i2p.client.datagram.I2PDatagramDissector import net.i2p.client.datagram.I2PDatagramDissector
import net.i2p.client.datagram.I2PDatagramMaker import net.i2p.client.datagram.I2PDatagramMaker
import net.i2p.data.Base64
import net.i2p.util.VersionComparator import net.i2p.util.VersionComparator
@Log @Log
@@ -30,24 +21,16 @@ class UpdateClient {
final I2PSession session final I2PSession session
final String myVersion final String myVersion
final MuWireSettings settings final MuWireSettings settings
final FileManager fileManager
final Persona me
private final Timer timer private final Timer timer
private long lastUpdateCheckTime private long lastUpdateCheckTime
private volatile InfoHash updateInfoHash UpdateClient(EventBus eventBus, I2PSession session, String myVersion, MuWireSettings settings) {
private volatile String version, signer
private volatile boolean updateDownloading
UpdateClient(EventBus eventBus, I2PSession session, String myVersion, MuWireSettings settings, FileManager fileManager, Persona me) {
this.eventBus = eventBus this.eventBus = eventBus
this.session = session this.session = session
this.myVersion = myVersion this.myVersion = myVersion
this.settings = settings this.settings = settings
this.fileManager = fileManager
this.me = me
timer = new Timer("update-client",true) timer = new Timer("update-client",true)
} }
@@ -60,24 +43,6 @@ class UpdateClient {
timer.cancel() timer.cancel()
} }
void onUIResultBatchEvent(UIResultBatchEvent results) {
if (results.results[0].infohash != updateInfoHash)
return
if (updateDownloading)
return
updateDownloading = true
def file = new File(settings.downloadLocation, results.results[0].name)
def downloadEvent = new UIDownloadEvent(result: results.results[0], sources : results.results[0].sources, target : file)
eventBus.publish(downloadEvent)
}
void onFileDownloadedEvent(FileDownloadedEvent e) {
if (e.downloadedFile.infoHash != updateInfoHash)
return
updateDownloading = false
eventBus.publish(new UpdateDownloadedEvent(version : version, signer : signer))
}
private void checkUpdate() { private void checkUpdate() {
final long now = System.currentTimeMillis() final long now = System.currentTimeMillis()
if (lastUpdateCheckTime > 0) { if (lastUpdateCheckTime > 0) {
@@ -141,32 +106,8 @@ class UpdateClient {
return return
} }
String infoHash
if (settings.updateType == "jar") {
infoHash = payload.infoHash
} else
infoHash = payload[settings.updateType]
if (!settings.autoDownloadUpdate) {
log.info("new version $payload.version available, publishing event") log.info("new version $payload.version available, publishing event")
eventBus.publish(new UpdateAvailableEvent(version : payload.version, signer : payload.signer, infoHash : infoHash)) eventBus.publish(new UpdateAvailableEvent(version : payload.version, signer : payload.signer, infoHash : payload.infoHash))
} else {
log.info("new version $payload.version available")
updateInfoHash = new InfoHash(Base64.decode(infoHash))
if (fileManager.rootToFiles.containsKey(updateInfoHash))
eventBus.publish(new UpdateDownloadedEvent(version : payload.version, signer : payload.signer))
else {
updateDownloading = false
version = payload.version
signer = payload.signer
log.info("starting search for new version hash $payload.infoHash")
def searchEvent = new SearchEvent(searchHash : updateInfoHash.getRoot(), uuid : UUID.randomUUID(), oobInfohash : true)
def queryEvent = new QueryEvent(searchEvent : searchEvent, firstHop : true, replyTo : me.destination,
receivedOn : me.destination, originator : me)
eventBus.publish(queryEvent)
}
}
} catch (Exception e) { } catch (Exception e) {
log.log(Level.WARNING,"Invalid datagram",e) log.log(Level.WARNING,"Invalid datagram",e)

View File

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

View File

@@ -3,5 +3,5 @@ package com.muwire.core.update
import net.i2p.data.Destination import net.i2p.data.Destination
class UpdateServers { class UpdateServers {
static final Destination UPDATE_SERVER = new Destination("VJYAiCPZHNLraWvLkeRLxRiT4PHAqNqRO1nH240r7u1noBw8Pa~-lJOhKR7CccPkEN8ejSi4H6XjqKYLC8BKLVLeOgnAbedUVx81MV7DETPDdPEGV4RVu6YDFri7-tJOeqauGHxtlXT44YWuR69xKrTG3u4~iTWgxKnlBDht9Q3aVpSPFD2KqEizfVxolqXI0zmAZ2xMi8jfl0oe4GbgHrD9hR2FYj6yKfdqcUgHVobY4kDdJt-u31QqwWdsQMEj8Y3tR2XcNaITEVPiAjoKgBrYwB4jddWPNaT4XdHz76d9p9Iqes7dhOKq3OKpk6kg-bfIKiEOiA1mY49fn5h8pNShTqV7QBhh4CE4EDT3Szl~WsLdrlHUKJufSi7erEMh3coF7HORpF1wah2Xw7q470t~b8dKGKi7N7xQsqhGruDm66PH9oE9Kt9WBVBq2zORdPRtRM61I7EnrwDlbOkL0y~XpvQ3JKUQKdBQ3QsOJt8CHlhHHXMMbvqhntR61RSDBQAEAAcAAA==") static final Destination UPDATE_SERVER = new Destination("pSWieSRB3czCl3Zz4WpKp4Z8tjv-05zbogRDS7SEnKcSdWOupVwjzQ92GsgQh1VqgoSRk1F8dpZOnHxxz5HFy9D7ri0uFdkMyXdSKoB7IgkkvCfTAyEmeaPwSYnurF3Zk7u286E7YG2rZkQZgJ77tow7ZS0mxFB7Z0Ti-VkZ9~GeGePW~howwNm4iSQACZA0DyTpI8iv5j4I0itPCQRgaGziob~Vfvjk49nd8N4jtaDGo9cEcafikVzQ2OgBgYWL6LRbrrItwuGqsDvITUHWaElUYIDhRQYUq8gYiUA6rwAJputfhFU0J7lIxFR9vVY7YzRvcFckfr0DNI4VQVVlPnRPkUxQa--BlldMaCIppWugjgKLwqiSiHywKpSMlBWgY2z1ry4ueEBo1WEP-mEf88wRk4cFQBCKtctCQnIG2GsnATqTl-VGUAsuzeNWZiFSwXiTy~gQ094yWx-K06fFZUDt4CMiLZVhGlixiInD~34FCRC9LVMtFcqiFB2M-Ql2AAAA")
} }

View File

@@ -2,5 +2,4 @@ package com.muwire.core.upload
class ContentRequest extends Request { class ContentRequest extends Request {
Range range Range range
int have
} }

View File

@@ -5,58 +5,34 @@ import java.nio.channels.FileChannel
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.StandardOpenOption import java.nio.file.StandardOpenOption
import java.util.stream.Collectors
import com.muwire.core.Persona
import com.muwire.core.connection.Endpoint import com.muwire.core.connection.Endpoint
import com.muwire.core.mesh.Mesh
import com.muwire.core.util.DataUtil
import net.i2p.data.Destination
class ContentUploader extends Uploader { class ContentUploader extends Uploader {
private final File file private final File file
private final ContentRequest request private final ContentRequest request
private final Mesh mesh
private final int pieceSize
ContentUploader(File file, ContentRequest request, Endpoint endpoint, Mesh mesh, int pieceSize) { ContentUploader(File file, ContentRequest request, Endpoint endpoint) {
super(endpoint) super(endpoint)
this.file = file this.file = file
this.request = request this.request = request
this.mesh = mesh
this.pieceSize = pieceSize
} }
@Override @Override
void respond() { void respond() {
OutputStream os = endpoint.getOutputStream() OutputStream os = endpoint.getOutputStream()
Range range = request.getRange() Range range = request.getRange()
boolean satisfiable = true if (range.start >= file.length() || range.end >= file.length()) {
final long length = file.length() os.write("416 Range Not Satisfiable\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
if (range.start >= length || range.end >= length)
satisfiable = false
if (satisfiable) {
int startPiece = range.start / (0x1 << pieceSize)
int endPiece = range.end / (0x1 << pieceSize)
for (int i = startPiece; i <= endPiece; i++)
satisfiable &= mesh.pieces.isDownloaded(i)
}
if (!satisfiable) {
os.write("416 Range Not Satisfiable\r\n".getBytes(StandardCharsets.US_ASCII))
writeMesh(request.downloader)
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
os.flush() os.flush()
return return
} }
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII)) os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("Content-Range: $range.start-$range.end\r\n".getBytes(StandardCharsets.US_ASCII)) os.write("Content-Range: $range.start-$range.end\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
writeMesh(request.downloader)
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
FileChannel channel = null FileChannel channel
try { try {
channel = Files.newByteChannel(file.toPath(), EnumSet.of(StandardOpenOption.READ)) channel = Files.newByteChannel(file.toPath(), EnumSet.of(StandardOpenOption.READ))
mapped = channel.map(FileChannel.MapMode.READ_ONLY, range.start, range.end - range.start + 1) mapped = channel.map(FileChannel.MapMode.READ_ONLY, range.start, range.end - range.start + 1)
@@ -72,55 +48,7 @@ class ContentUploader extends Uploader {
} finally { } finally {
try {channel?.close() } catch (IOException ignored) {} try {channel?.close() } catch (IOException ignored) {}
endpoint.getOutputStream().flush() endpoint.getOutputStream().flush()
synchronized(this) {
DataUtil.tryUnmap(mapped)
mapped = null
}
}
}
private void writeMesh(Persona toExclude) {
String xHave = DataUtil.encodeXHave(mesh.pieces.getDownloaded(), mesh.pieces.nPieces)
endpoint.getOutputStream().write("X-Have: $xHave\r\n".getBytes(StandardCharsets.US_ASCII))
Set<Persona> sources = mesh.getRandom(9, toExclude)
if (!sources.isEmpty()) {
String xAlts = sources.stream().map({ it.toBase64() }).collect(Collectors.joining(","))
endpoint.getOutputStream().write("X-Alt: $xAlts\r\n".getBytes(StandardCharsets.US_ASCII))
} }
} }
@Override
public String getName() {
return file.getName();
}
@Override
public synchronized int getProgress() {
if (mapped == null)
return 0
int position = mapped.position()
int total = request.getRange().end - request.getRange().start
(int)(position * 100.0 / total)
}
@Override
public String getDownloader() {
request.downloader.getHumanReadableName()
}
@Override
public int getDonePieces() {
return request.have;
}
@Override
public int getTotalPieces() {
return mesh.pieces.nPieces;
}
@Override
public long getTotalSize() {
return file.length();
}
} }

View File

@@ -6,8 +6,6 @@ import java.nio.charset.StandardCharsets
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
import com.muwire.core.connection.Endpoint import com.muwire.core.connection.Endpoint
import net.i2p.data.Base64
class HashListUploader extends Uploader { class HashListUploader extends Uploader {
private final InfoHash infoHash private final InfoHash infoHash
private final HashListRequest request private final HashListRequest request
@@ -16,7 +14,6 @@ class HashListUploader extends Uploader {
super(endpoint) super(endpoint)
this.infoHash = infoHash this.infoHash = infoHash
mapped = ByteBuffer.wrap(infoHash.getHashList()) mapped = ByteBuffer.wrap(infoHash.getHashList())
this.request = request
} }
void respond() { void respond() {
@@ -35,34 +32,4 @@ class HashListUploader extends Uploader {
} }
endpoint.getOutputStream().flush() endpoint.getOutputStream().flush()
} }
@Override
public String getName() {
return "Hash list for " + Base64.encode(infoHash.getRoot());
}
@Override
public synchronized int getProgress() {
(int)(mapped.position() * 100.0 / mapped.capacity())
}
@Override
public String getDownloader() {
request.downloader.getHumanReadableName()
}
@Override
public int getDonePieces() {
return 0;
}
@Override
public int getTotalPieces() {
return 1;
}
@Override
public long getTotalSize() {
return -1;
}
} }

View File

@@ -5,7 +5,6 @@ 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 com.muwire.core.Persona
import com.muwire.core.util.DataUtil
import groovy.util.logging.Log import groovy.util.logging.Log
import net.i2p.data.Base64 import net.i2p.data.Base64
@@ -49,14 +48,8 @@ class Request {
def decoded = Base64.decode(encoded) def decoded = Base64.decode(encoded)
downloader = new Persona(new ByteArrayInputStream(decoded)) downloader = new Persona(new ByteArrayInputStream(decoded))
} }
int have = 0
if (headers.containsKey("X-Have")) {
def encoded = headers["X-Have"].trim()
have = DataUtil.decodeXHave(encoded).size()
}
new ContentRequest( infoHash : infoHash, range : new Range(start, end), new ContentRequest( infoHash : infoHash, range : new Range(start, end),
headers : headers, downloader : downloader, have : have) headers : headers, downloader : downloader)
} }
static Request parseHashListRequest(InfoHash infoHash, InputStream is) throws IOException { static Request parseHashListRequest(InfoHash infoHash, InputStream is) throws IOException {

View File

@@ -6,12 +6,7 @@ import com.muwire.core.EventBus
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
import com.muwire.core.SharedFile import com.muwire.core.SharedFile
import com.muwire.core.connection.Endpoint import com.muwire.core.connection.Endpoint
import com.muwire.core.download.DownloadManager
import com.muwire.core.download.Downloader
import com.muwire.core.download.SourceDiscoveredEvent
import com.muwire.core.files.FileManager import com.muwire.core.files.FileManager
import com.muwire.core.mesh.Mesh
import com.muwire.core.mesh.MeshManager
import groovy.util.logging.Log import groovy.util.logging.Log
import net.i2p.data.Base64 import net.i2p.data.Base64
@@ -20,17 +15,12 @@ import net.i2p.data.Base64
public class UploadManager { public class UploadManager {
private final EventBus eventBus private final EventBus eventBus
private final FileManager fileManager private final FileManager fileManager
private final MeshManager meshManager
private final DownloadManager downloadManager
public UploadManager() {} public UploadManager() {}
public UploadManager(EventBus eventBus, FileManager fileManager, public UploadManager(EventBus eventBus, FileManager fileManager) {
MeshManager meshManager, DownloadManager downloadManager) {
this.eventBus = eventBus this.eventBus = eventBus
this.fileManager = fileManager this.fileManager = fileManager
this.meshManager = meshManager
this.downloadManager = downloadManager
} }
public void processGET(Endpoint e) throws IOException { public void processGET(Endpoint e) throws IOException {
@@ -54,10 +44,8 @@ public class UploadManager {
log.info("Responding to upload request for root $infoHashString") log.info("Responding to upload request for root $infoHashString")
byte [] infoHashRoot = Base64.decode(infoHashString) byte [] infoHashRoot = Base64.decode(infoHashString)
InfoHash infoHash = new InfoHash(infoHashRoot)
Set<SharedFile> sharedFiles = fileManager.getSharedFiles(infoHashRoot) Set<SharedFile> sharedFiles = fileManager.getSharedFiles(infoHashRoot)
Downloader downloader = downloadManager.downloaders.get(infoHash) if (sharedFiles == null || sharedFiles.isEmpty()) {
if (downloader == null && (sharedFiles == null || sharedFiles.isEmpty())) {
log.info "file not found" log.info "file not found"
e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII)) e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
e.getOutputStream().flush() e.getOutputStream().flush()
@@ -73,31 +61,13 @@ public class UploadManager {
return return
} }
ContentRequest request = Request.parseContentRequest(infoHash, e.getInputStream()) Request request = Request.parseContentRequest(new InfoHash(infoHashRoot), e.getInputStream())
if (request.downloader != null && request.downloader.destination != e.destination) { if (request.downloader != null && request.downloader.destination != e.destination) {
log.info("Downloader persona doesn't match their destination") log.info("Downloader persona doesn't match their destination")
e.close() e.close()
return return
} }
Uploader uploader = new ContentUploader(sharedFiles.iterator().next().file, request, e)
if (request.have > 0)
eventBus.publish(new SourceDiscoveredEvent(infoHash : request.infoHash, source : request.downloader))
Mesh mesh
File file
int pieceSize
if (downloader != null) {
mesh = meshManager.get(infoHash)
file = downloader.incompleteFile
pieceSize = downloader.pieceSizePow2
} else {
SharedFile sharedFile = sharedFiles.iterator().next();
mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces, false)
file = sharedFile.file
pieceSize = sharedFile.pieceSize
}
Uploader uploader = new ContentUploader(file, request, e, mesh, pieceSize)
eventBus.publish(new UploadEvent(uploader : uploader)) eventBus.publish(new UploadEvent(uploader : uploader))
try { try {
uploader.respond() uploader.respond()
@@ -115,10 +85,8 @@ public class UploadManager {
log.info("Responding to hashlist request for root $infoHashString") log.info("Responding to hashlist request for root $infoHashString")
byte [] infoHashRoot = Base64.decode(infoHashString) byte [] infoHashRoot = Base64.decode(infoHashString)
InfoHash infoHash = new InfoHash(infoHashRoot)
Downloader downloader = downloadManager.downloaders.get(infoHash)
Set<SharedFile> sharedFiles = fileManager.getSharedFiles(infoHashRoot) Set<SharedFile> sharedFiles = fileManager.getSharedFiles(infoHashRoot)
if (downloader == null && (sharedFiles == null || sharedFiles.isEmpty())) { if (sharedFiles == null || sharedFiles.isEmpty()) {
log.info "file not found" log.info "file not found"
e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII)) e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
e.getOutputStream().flush() e.getOutputStream().flush()
@@ -134,30 +102,13 @@ public class UploadManager {
return return
} }
Request request = Request.parseHashListRequest(infoHash, e.getInputStream()) Request request = Request.parseHashListRequest(new InfoHash(infoHashRoot), e.getInputStream())
if (request.downloader != null && request.downloader.destination != e.destination) { if (request.downloader != null && request.downloader.destination != e.destination) {
log.info("Downloader persona doesn't match their destination") log.info("Downloader persona doesn't match their destination")
e.close() e.close()
return return
} }
Uploader uploader = new HashListUploader(e, sharedFiles.iterator().next().infoHash, request)
InfoHash fullInfoHash
if (downloader == null) {
fullInfoHash = sharedFiles.iterator().next().infoHash
} else {
byte [] hashList = downloader.getInfoHash().getHashList()
if (hashList != null && hashList.length > 0)
fullInfoHash = downloader.getInfoHash()
else {
log.info("infohash not found in downloader")
e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
e.getOutputStream().flush()
e.close()
return
}
}
Uploader uploader = new HashListUploader(e, fullInfoHash, request)
eventBus.publish(new UploadEvent(uploader : uploader)) eventBus.publish(new UploadEvent(uploader : uploader))
try { try {
uploader.respond() uploader.respond()
@@ -179,10 +130,8 @@ public class UploadManager {
log.info("Responding to upload request for root $infoHashString") log.info("Responding to upload request for root $infoHashString")
infoHashRoot = Base64.decode(infoHashString) infoHashRoot = Base64.decode(infoHashString)
infoHash = new InfoHash(infoHashRoot)
sharedFiles = fileManager.getSharedFiles(infoHashRoot) sharedFiles = fileManager.getSharedFiles(infoHashRoot)
downloader = downloadManager.downloaders.get(infoHash) if (sharedFiles == null || sharedFiles.isEmpty()) {
if (downloader == null && (sharedFiles == null || sharedFiles.isEmpty())) {
log.info "file not found" log.info "file not found"
e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII)) e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
e.getOutputStream().flush() e.getOutputStream().flush()
@@ -204,25 +153,7 @@ public class UploadManager {
e.close() e.close()
return return
} }
uploader = new ContentUploader(sharedFiles.iterator().next().file, request, e)
if (request.have > 0)
eventBus.publish(new SourceDiscoveredEvent(infoHash : request.infoHash, source : request.downloader))
Mesh mesh
File file
int pieceSize
if (downloader != null) {
mesh = meshManager.get(infoHash)
file = downloader.incompleteFile
pieceSize = downloader.pieceSizePow2
} else {
SharedFile sharedFile = sharedFiles.iterator().next();
mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces, false)
file = sharedFile.file
pieceSize = sharedFile.pieceSize
}
uploader = new ContentUploader(file, request, e, mesh, pieceSize)
eventBus.publish(new UploadEvent(uploader : uploader)) eventBus.publish(new UploadEvent(uploader : uploader))
try { try {
uploader.respond() uploader.respond()

View File

@@ -23,19 +23,4 @@ abstract class Uploader {
return -1 return -1
mapped.position() mapped.position()
} }
abstract String getName();
/**
* @return an integer between 0 and 100
*/
abstract int getProgress();
abstract String getDownloader();
abstract int getDonePieces();
abstract int getTotalPieces();
abstract long getTotalSize();
} }

View File

@@ -0,0 +1,82 @@
package com.muwire.core.util
import java.nio.charset.StandardCharsets
import com.muwire.core.Constants
class DataUtil {
private final static int MAX_SHORT = (0x1 << 16) - 1
static void writeUnsignedShort(int value, OutputStream os) {
if (value > MAX_SHORT || value < 0)
throw new IllegalArgumentException("$value invalid")
byte lsb = (byte) (value & 0xFF)
byte msb = (byte) (value >> 8)
os.write(msb)
os.write(lsb)
}
private final static int MAX_HEADER = 0x7FFFFF
static void packHeader(int length, byte [] header) {
if (header.length != 3)
throw new IllegalArgumentException("header length $header.length")
if (length < 0 || length > MAX_HEADER)
throw new IllegalArgumentException("length $length")
header[2] = (byte) (length & 0xFF)
header[1] = (byte) ((length >> 8) & 0xFF)
header[0] = (byte) ((length >> 16) & 0x7F)
}
static int readLength(byte [] header) {
if (header.length != 3)
throw new IllegalArgumentException("header length $header.length")
return (((int)(header[0] & 0x7F)) << 16) |
(((int)(header[1] & 0xFF) << 8)) |
((int)header[2] & 0xFF)
}
static String readi18nString(byte [] encoded) {
if (encoded.length < 2)
throw new IllegalArgumentException("encoding too short $encoded.length")
int length = ((encoded[0] & 0xFF) << 8) | (encoded[1] & 0xFF)
if (encoded.length != length + 2)
throw new IllegalArgumentException("encoding doesn't match length, expected $length found $encoded.length")
byte [] string = new byte[length]
System.arraycopy(encoded, 2, string, 0, length)
new String(string, StandardCharsets.UTF_8)
}
static byte[] encodei18nString(String string) {
byte [] utf8 = string.getBytes(StandardCharsets.UTF_8)
if (utf8.length > Short.MAX_VALUE)
throw new IllegalArgumentException("String in utf8 too long $utf8.length")
def baos = new ByteArrayOutputStream()
def daos = new DataOutputStream(baos)
daos.writeShort((short) utf8.length)
daos.write(utf8)
daos.close()
baos.toByteArray()
}
public static String readTillRN(InputStream is) {
def baos = new ByteArrayOutputStream()
while(baos.size() < (Constants.MAX_HEADER_SIZE)) {
byte read = is.read()
if (read == -1)
throw new IOException()
if (read == '\r') {
if (is.read() != '\n')
throw new IOException("invalid header")
break
}
baos.write(read)
}
new String(baos.toByteArray(), StandardCharsets.US_ASCII)
}
}

View File

@@ -1,13 +0,0 @@
package com.muwire.core;
import net.i2p.crypto.SigType;
public class Constants {
public static final byte PERSONA_VERSION = (byte)1;
public static final SigType SIG_TYPE = SigType.EdDSA_SHA512_Ed25519;
public static final int MAX_HEADER_SIZE = 0x1 << 14;
public static final int MAX_HEADERS = 16;
public static final int MAX_RESULTS = 0x1 << 16;
}

View File

@@ -1,7 +1,6 @@
package com.muwire.core; package com.muwire.core;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.util.Set; import java.util.Set;
import net.i2p.data.Destination; import net.i2p.data.Destination;
@@ -10,8 +9,7 @@ public class DownloadedFile extends SharedFile {
private final Set<Destination> sources; private final Set<Destination> sources;
public DownloadedFile(File file, InfoHash infoHash, int pieceSize, Set<Destination> sources) public DownloadedFile(File file, InfoHash infoHash, int pieceSize, Set<Destination> sources) {
throws IOException {
super(file, infoHash, pieceSize); super(file, infoHash, pieceSize);
this.sources = sources; this.sources = sources;
} }

View File

@@ -1,13 +1,6 @@
package com.muwire.core; package com.muwire.core;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import com.muwire.core.util.DataUtil;
import net.i2p.data.Base64;
public class SharedFile { public class SharedFile {
@@ -15,31 +8,10 @@ public class SharedFile {
private final InfoHash infoHash; private final InfoHash infoHash;
private final int pieceSize; private final int pieceSize;
private final String cachedPath; public SharedFile(File file, InfoHash infoHash, int pieceSize) {
private final long cachedLength;
private final String b64EncodedFileName;
private final String b64EncodedHashRoot;
private final List<String> b64EncodedHashList;
private volatile String comment;
public SharedFile(File file, InfoHash infoHash, int pieceSize) throws IOException {
this.file = file; this.file = file;
this.infoHash = infoHash; this.infoHash = infoHash;
this.pieceSize = pieceSize; this.pieceSize = pieceSize;
this.cachedPath = file.getAbsolutePath();
this.cachedLength = file.length();
this.b64EncodedFileName = Base64.encode(DataUtil.encodei18nString(file.toString()));
this.b64EncodedHashRoot = Base64.encode(infoHash.getRoot());
List<String> b64List = new ArrayList<String>();
byte[] tmp = new byte[32];
for (int i = 0; i < infoHash.getHashList().length / 32; i++) {
System.arraycopy(infoHash.getHashList(), i * 32, tmp, 0, 32);
b64List.add(Base64.encode(tmp));
}
this.b64EncodedHashList = b64List;
} }
public File getFile() { public File getFile() {
@@ -53,54 +25,4 @@ public class SharedFile {
public int getPieceSize() { public int getPieceSize() {
return pieceSize; return pieceSize;
} }
public int getNPieces() {
long length = file.length();
int rawPieceSize = 0x1 << pieceSize;
int rv = (int) (length / rawPieceSize);
if (length % rawPieceSize != 0)
rv++;
return rv;
}
public String getB64EncodedFileName() {
return b64EncodedFileName;
}
public String getB64EncodedHashRoot() {
return b64EncodedHashRoot;
}
public List<String> getB64EncodedHashList() {
return b64EncodedHashList;
}
public String getCachedPath() {
return cachedPath;
}
public long getCachedLength() {
return cachedLength;
}
public void setComment(String comment) {
this.comment = comment;
}
public String getComment() {
return comment;
}
@Override
public int hashCode() {
return file.hashCode() ^ infoHash.hashCode();
}
@Override
public boolean equals(Object o) {
if (!(o instanceof SharedFile))
return false;
SharedFile other = (SharedFile)o;
return file.equals(other.file) && infoHash.equals(other.infoHash);
}
} }

View File

@@ -1,168 +0,0 @@
package com.muwire.core.util;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import com.muwire.core.Constants;
import net.i2p.data.Base64;
public class DataUtil {
private final static int MAX_SHORT = (0x1 << 16) - 1;
static void writeUnsignedShort(int value, OutputStream os) throws IOException {
if (value > MAX_SHORT || value < 0)
throw new IllegalArgumentException("$value invalid");
byte lsb = (byte) (value & 0xFF);
byte msb = (byte) (value >> 8);
os.write(msb);
os.write(lsb);
}
private final static int MAX_HEADER = 0x7FFFFF;
static void packHeader(int length, byte [] header) {
if (header.length != 3)
throw new IllegalArgumentException("header length $header.length");
if (length < 0 || length > MAX_HEADER)
throw new IllegalArgumentException("length $length");
header[2] = (byte) (length & 0xFF);
header[1] = (byte) ((length >> 8) & 0xFF);
header[0] = (byte) ((length >> 16) & 0x7F);
}
static int readLength(byte [] header) {
if (header.length != 3)
throw new IllegalArgumentException("header length $header.length");
return (((int)(header[0] & 0x7F)) << 16) |
(((int)(header[1] & 0xFF) << 8)) |
((int)header[2] & 0xFF);
}
static String readi18nString(byte [] encoded) {
if (encoded.length < 2)
throw new IllegalArgumentException("encoding too short $encoded.length");
int length = ((encoded[0] & 0xFF) << 8) | (encoded[1] & 0xFF);
if (encoded.length != length + 2)
throw new IllegalArgumentException("encoding doesn't match length, expected $length found $encoded.length");
byte [] string = new byte[length];
System.arraycopy(encoded, 2, string, 0, length);
return new String(string, StandardCharsets.UTF_8);
}
public static byte[] encodei18nString(String string) {
byte [] utf8 = string.getBytes(StandardCharsets.UTF_8);
if (utf8.length > Short.MAX_VALUE)
throw new IllegalArgumentException("String in utf8 too long $utf8.length");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream daos = new DataOutputStream(baos);
try {
daos.writeShort((short) utf8.length);
daos.write(utf8);
daos.close();
} catch (IOException impossible) {
throw new IllegalStateException(impossible);
}
return baos.toByteArray();
}
public static String readTillRN(InputStream is) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while(baos.size() < (Constants.MAX_HEADER_SIZE)) {
int read = is.read();
if (read == -1)
throw new IOException();
if (read == '\r') {
if (is.read() != '\n')
throw new IOException("invalid header");
break;
}
baos.write(read);
}
return new String(baos.toByteArray(), StandardCharsets.US_ASCII);
}
public static String encodeXHave(List<Integer> pieces, int totalPieces) {
int bytes = totalPieces / 8;
if (totalPieces % 8 != 0)
bytes++;
byte[] raw = new byte[bytes];
for (int it : pieces) {
int byteIdx = it / 8;
int offset = it % 8;
int mask = 0x80 >>> offset;
raw[byteIdx] |= mask;
}
return Base64.encode(raw);
}
public static List<Integer> decodeXHave(String xHave) {
byte [] availablePieces = Base64.decode(xHave);
List<Integer> available = new ArrayList<>();
for (int i = 0; i < availablePieces.length; i ++) {
byte b = availablePieces[i];
for (int j = 0; j < 8 ; j++) {
byte mask = (byte) (0x80 >>> j);
if ((b & mask) == mask) {
available.add(i * 8 + j);
}
}
}
return available;
}
public static Throwable findRoot(Throwable e) {
while(e.getCause() != null)
e = e.getCause();
return e;
}
public static void tryUnmap(ByteBuffer cb) {
if (cb==null || !cb.isDirect()) return;
// we could use this type cast and call functions without reflection code,
// but static import from sun.* package is risky for non-SUN virtual machine.
//try { ((sun.nio.ch.DirectBuffer)cb).cleaner().clean(); } catch (Exception ex) { }
// JavaSpecVer: 1.6, 1.7, 1.8, 9, 10
boolean isOldJDK = System.getProperty("java.specification.version","99").startsWith("1.");
try {
if (isOldJDK) {
Method cleaner = cb.getClass().getMethod("cleaner");
cleaner.setAccessible(true);
Method clean = Class.forName("sun.misc.Cleaner").getMethod("clean");
clean.setAccessible(true);
clean.invoke(cleaner.invoke(cb));
} else {
Class unsafeClass;
try {
unsafeClass = Class.forName("sun.misc.Unsafe");
} catch(Exception ex) {
// jdk.internal.misc.Unsafe doesn't yet have an invokeCleaner() method,
// but that method should be added if sun.misc.Unsafe is removed.
unsafeClass = Class.forName("jdk.internal.misc.Unsafe");
}
Method clean = unsafeClass.getMethod("invokeCleaner", ByteBuffer.class);
clean.setAccessible(true);
Field theUnsafeField = unsafeClass.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
Object theUnsafe = theUnsafeField.get(null);
clean.invoke(theUnsafe, cb);
}
} catch(Exception ex) { }
cb = null;
}
}

View File

@@ -1,31 +1,21 @@
package com.muwire.core.download package com.muwire.core.download
import static org.junit.Assert.fail
import org.junit.After import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Test import org.junit.Test
import com.muwire.core.EventBus
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
import com.muwire.core.Persona
import com.muwire.core.Personas
import com.muwire.core.connection.Endpoint import com.muwire.core.connection.Endpoint
import com.muwire.core.files.FileHasher import com.muwire.core.files.FileHasher
import static com.muwire.core.util.DataUtil.readTillRN import static com.muwire.core.util.DataUtil.readTillRN
import static com.muwire.core.util.DataUtil.encodeXHave
import net.i2p.data.Base64 import net.i2p.data.Base64
import net.i2p.util.ConcurrentHashSet
class DownloadSessionTest { class DownloadSessionTest {
private EventBus eventBus
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
@@ -34,16 +24,6 @@ class DownloadSessionTest {
private InputStream fromDownloader, fromUploader private InputStream fromDownloader, fromUploader
private OutputStream toDownloader, toUploader private OutputStream toDownloader, toUploader
private volatile boolean performed
private Set<Integer> available = new ConcurrentHashSet<>()
private volatile IOException thrown
@Before
public void setUp() {
eventBus = new EventBus()
}
private void initSession(int size, def claimedPieces = []) { 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]
@@ -68,7 +48,8 @@ class DownloadSessionTest {
else else
nPieces = size / pieceSize + 1 nPieces = size / pieceSize + 1
pieces = new Pieces(nPieces) pieces = new Pieces(nPieces)
claimedPieces.each {pieces.claimed.set(it)} claimed = new Pieces(nPieces)
claimedPieces.each {claimed.markDownloaded(it)}
fromDownloader = new PipedInputStream() fromDownloader = new PipedInputStream()
fromUploader = new PipedInputStream() fromUploader = new PipedInputStream()
@@ -76,20 +57,12 @@ 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(eventBus, "",pieces, infoHash, endpoint, target, pieceSize, size, available) session = new DownloadSession("",pieces, claimed, infoHash, endpoint, target, pieceSize, size)
downloadThread = new Thread( { perform() } as Runnable) downloadThread = new Thread( { session.request() } as Runnable)
downloadThread.setDaemon(true) downloadThread.setDaemon(true)
downloadThread.start() downloadThread.start()
} }
private void perform() {
try {
performed = session.request()
} catch (IOException e) {
thrown = e
}
}
@After @After
public void teardown() { public void teardown() {
source?.delete() source?.delete()
@@ -104,7 +77,6 @@ class DownloadSessionTest {
assert "GET $rootBase64" == readTillRN(fromDownloader) assert "GET $rootBase64" == readTillRN(fromDownloader)
assert "Range: 0-19" == readTillRN(fromDownloader) assert "Range: 0-19" == 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)
@@ -116,9 +88,6 @@ class DownloadSessionTest {
assert pieces.isComplete() assert pieces.isComplete()
assert target.bytes == source.bytes assert target.bytes == source.bytes
assert performed
assert available.isEmpty()
assert thrown == null
} }
@Test @Test
@@ -130,7 +99,6 @@ class DownloadSessionTest {
assert "GET $rootBase64" == readTillRN(fromDownloader) assert "GET $rootBase64" == readTillRN(fromDownloader)
readTillRN(fromDownloader) 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)
@@ -141,9 +109,6 @@ class DownloadSessionTest {
Thread.sleep(150) Thread.sleep(150)
assert pieces.isComplete() assert pieces.isComplete()
assert target.bytes == source.bytes assert target.bytes == source.bytes
assert performed
assert available.isEmpty()
assert thrown == null
} }
@Test @Test
@@ -161,7 +126,6 @@ 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)
readTillRN(fromDownloader) readTillRN(fromDownloader)
assert "" == readTillRN(fromDownloader) assert "" == readTillRN(fromDownloader)
@@ -175,21 +139,14 @@ class DownloadSessionTest {
Thread.sleep(150) Thread.sleep(150)
assert !pieces.isComplete() assert !pieces.isComplete()
assert 1 == pieces.donePieces() assert 1 == pieces.donePieces()
assert performed
assert available.isEmpty()
assert thrown == null
} }
@Test @Test
@Ignore // this needs to be rewritten with stealing in mind
public void testSmallFileClaimed() { public void testSmallFileClaimed() {
initSession(20, [0]) initSession(20, [0])
long now = System.currentTimeMillis() long now = System.currentTimeMillis()
downloadThread.join(150) downloadThread.join(100)
assert 100 >= (System.currentTimeMillis() - now) assert 100 > (System.currentTimeMillis() - now)
assert !performed
assert available.isEmpty()
assert thrown == null
} }
@Test @Test
@@ -197,7 +154,7 @@ class DownloadSessionTest {
int pieceSize = FileHasher.getPieceSize(1) int pieceSize = FileHasher.getPieceSize(1)
int size = (1 << pieceSize) * 10 int size = (1 << pieceSize) * 10
initSession(size, [1,2,3,4,5,6,7,8,9]) initSession(size, [1,2,3,4,5,6,7,8,9])
assert !pieces.claimed.get(0) assert !claimed.isMarked(0)
assert "GET $rootBase64" == readTillRN(fromDownloader) assert "GET $rootBase64" == readTillRN(fromDownloader)
String range = readTillRN(fromDownloader) String range = readTillRN(fromDownloader)
@@ -205,131 +162,7 @@ class DownloadSessionTest {
int start = Integer.parseInt(matcher[0][1]) int start = Integer.parseInt(matcher[0][1])
int end = Integer.parseInt(matcher[0][2]) int end = Integer.parseInt(matcher[0][2])
assert pieces.claimed.get(0) assert claimed.isMarked(0)
assert start == 0 && end == (1 << pieceSize) - 1 assert start == 0 && end == (1 << pieceSize) - 1
} }
@Test
public void test416NoHave() {
initSession(20)
readAllHeaders(fromDownloader)
toDownloader.write("416 don't have it\r\n\r\n".bytes)
toDownloader.flush()
Thread.sleep(150)
assert !performed
assert available.isEmpty()
assert thrown != null
}
@Test
public void test416Have() {
initSession(20)
readAllHeaders(fromDownloader)
toDownloader.write("416 don't have it\r\n".bytes)
toDownloader.write("X-Have: ${encodeXHave([0], 1)}\r\n\r\n".bytes)
toDownloader.flush()
Thread.sleep(150)
assert performed
assert available.contains(0)
assert thrown == null
}
@Test
public void test416Have2Pieces() {
int pieceSize = FileHasher.getPieceSize(1)
int size = (1 << pieceSize) + 1
initSession(size)
readAllHeaders(fromDownloader)
toDownloader.write("416 don't have it\r\n".bytes)
toDownloader.write("X-Have: ${encodeXHave([1], 2)}\r\n\r\n".bytes)
toDownloader.flush()
Thread.sleep(150)
assert performed
assert available.contains(1)
assert thrown == null
}
@Test
public void test200TwoPieces1Available() {
int pieceSize = FileHasher.getPieceSize(1)
int size = (1 << pieceSize) * 9 + 1
initSession(size)
Set<String> headers = readAllHeaders(fromDownloader)
def matcher = null
headers.each {
if (it.startsWith("Range"))
matcher = (it =~ /^Range: (\d+)-(\d+)$/)
}
assert matcher.groupCount() > 0
int start = Integer.parseInt(matcher[0][1])
int end = Integer.parseInt(matcher[0][2])
if (start == 0)
fail("inconlcusive")
toDownloader.write("416 don't have it \r\n".bytes)
toDownloader.write("X-Have: ${encodeXHave([0],2)}\r\n\r\n".bytes)
toDownloader.flush()
downloadThread.join()
assert performed
performed = false
assert available.contains(0)
assert thrown == null
// request same session
downloadThread = new Thread( { perform() } as Runnable)
downloadThread.setDaemon(true)
downloadThread.start()
Thread.sleep(150)
headers = readAllHeaders(fromDownloader)
matcher = null
headers.each {
if (it.startsWith("Range"))
matcher = (it =~ /^Range: (\d+)-(\d+)$/)
}
assert matcher.groupCount() > 0
start = Integer.parseInt(matcher[0][1])
end = Integer.parseInt(matcher[0][2])
assert start == 0
}
@Test
public void testXAlt() throws Exception {
Personas personas = new Personas()
def sources = []
def listener = new Object() {
public void onSourceDiscoveredEvent(SourceDiscoveredEvent e) {
sources << e.source
}
}
eventBus.register(SourceDiscoveredEvent.class, listener)
initSession(20)
readAllHeaders(fromDownloader)
toDownloader.write("416 don't have it\r\n".bytes)
toDownloader.write("X-Alt: ${personas.persona1.toBase64()},${personas.persona2.toBase64()}\r\n\r\n".bytes)
toDownloader.flush()
Thread.sleep(150)
assert sources.contains(personas.persona1)
assert sources.contains(personas.persona2)
assert 2 == sources.size()
}
private static Set<String> readAllHeaders(InputStream is) {
Set<String> rv = new HashSet<>()
String header
while((header = readTillRN(is)) != "")
rv.add(header)
rv
}
} }

View File

@@ -16,7 +16,7 @@ class PiecesTest {
public void testSinglePiece() { public void testSinglePiece() {
pieces = new Pieces(1) pieces = new Pieces(1)
assert !pieces.isComplete() assert !pieces.isComplete()
assert pieces.claim() == [0,0,0] assert pieces.getRandomPiece() == 0
pieces.markDownloaded(0) pieces.markDownloaded(0)
assert pieces.isComplete() assert pieces.isComplete()
} }
@@ -25,28 +25,13 @@ class PiecesTest {
public void testTwoPieces() { public void testTwoPieces() {
pieces = new Pieces(2) pieces = new Pieces(2)
assert !pieces.isComplete() assert !pieces.isComplete()
int[] piece = pieces.claim() int piece = pieces.getRandomPiece()
assert piece[0] == 0 || piece[0] == 1 assert piece == 0 || piece == 1
pieces.markDownloaded(piece[0]) pieces.markDownloaded(piece)
assert !pieces.isComplete() assert !pieces.isComplete()
int[] piece2 = pieces.claim() int piece2 = pieces.getRandomPiece()
assert piece[0] != piece2[0] assert piece != piece2
pieces.markDownloaded(piece2[0]) pieces.markDownloaded(piece2)
assert pieces.isComplete() assert pieces.isComplete()
} }
@Test
public void testClaimAvailable() {
pieces = new Pieces(2)
int[] claimed = pieces.claim([0].toSet())
assert claimed == [0,0,0]
assert [0,0,1] == pieces.claim([0].toSet())
}
@Test
public void testClaimNoneAvailable() {
pieces = new Pieces(20)
int[] claimed = pieces.claim()
assert [0,0,0] == pieces.claim(claimed.toSet())
}
} }

View File

@@ -26,7 +26,7 @@ class FileHasherTest extends GroovyTestCase {
void testPieceSize() { void testPieceSize() {
assert 17 == FileHasher.getPieceSize(1000000) assert 17 == FileHasher.getPieceSize(1000000)
assert 17 == FileHasher.getPieceSize(100000000) assert 17 == FileHasher.getPieceSize(100000000)
assert 24 == FileHasher.getPieceSize(FileHasher.MAX_SIZE) assert 27 == FileHasher.getPieceSize(FileHasher.MAX_SIZE)
shouldFail IllegalArgumentException, { shouldFail IllegalArgumentException, {
FileHasher.getPieceSize(Long.MAX_VALUE) FileHasher.getPieceSize(Long.MAX_VALUE)
} }

View File

@@ -25,10 +25,8 @@ class HasherServiceTest {
void before() { void before() {
eventBus = new EventBus() eventBus = new EventBus()
hasher = new FileHasher() hasher = new FileHasher()
def props = new MuWireSettings() service = new HasherService(hasher, eventBus, new FileManager(eventBus, new MuWireSettings()))
service = new HasherService(hasher, eventBus, new FileManager(eventBus, props), props)
eventBus.register(FileHashedEvent.class, listener) eventBus.register(FileHashedEvent.class, listener)
eventBus.register(FileSharedEvent.class, service)
service.start() service.start()
} }

View File

@@ -78,7 +78,7 @@ class PersisterServiceLoadingTest {
persisted.write json persisted.write json
PersisterService ps = new PersisterService(persisted, eventBus, 100, null) PersisterService ps = new PersisterService(persisted, eventBus, 100, null)
ps.onUILoadedEvent(null) ps.start()
Thread.sleep(2000) Thread.sleep(2000)
assert listener.publishedFiles.size() == 1 assert listener.publishedFiles.size() == 1
@@ -121,7 +121,7 @@ class PersisterServiceLoadingTest {
persisted.write json persisted.write json
PersisterService ps = new PersisterService(persisted, eventBus, 100, null) PersisterService ps = new PersisterService(persisted, eventBus, 100, null)
ps.onUILoadedEvent(null) ps.start()
Thread.sleep(2000) Thread.sleep(2000)
assert listener.publishedFiles.size() == 1 assert listener.publishedFiles.size() == 1
@@ -163,7 +163,7 @@ class PersisterServiceLoadingTest {
persisted.append "$json2\n" persisted.append "$json2\n"
PersisterService ps = new PersisterService(persisted, eventBus, 100, null) PersisterService ps = new PersisterService(persisted, eventBus, 100, null)
ps.onUILoadedEvent(null) ps.start()
Thread.sleep(2000) Thread.sleep(2000)
assert listener.publishedFiles.size() == 2 assert listener.publishedFiles.size() == 2
@@ -195,7 +195,7 @@ class PersisterServiceLoadingTest {
persisted.write json1 persisted.write json1
PersisterService ps = new PersisterService(persisted, eventBus, 100, null) PersisterService ps = new PersisterService(persisted, eventBus, 100, null)
ps.onUILoadedEvent(null) ps.start()
Thread.sleep(2000) Thread.sleep(2000)
assert listener.publishedFiles.size() == 1 assert listener.publishedFiles.size() == 1

View File

@@ -58,7 +58,7 @@ class PersisterServiceSavingTest {
sf = new SharedFile(f, ih, 0) sf = new SharedFile(f, ih, 0)
ps = new PersisterService(persisted, eventBus, 100, fileSource) ps = new PersisterService(persisted, eventBus, 100, fileSource)
ps.onUILoadedEvent(null) ps.start()
Thread.sleep(1500) Thread.sleep(1500)
JsonSlurper jsonSlurper = new JsonSlurper() JsonSlurper jsonSlurper = new JsonSlurper()
@@ -77,7 +77,7 @@ class PersisterServiceSavingTest {
sf = new DownloadedFile(f, ih, 0, new HashSet([dests.dest1, dests.dest2])) sf = new DownloadedFile(f, ih, 0, new HashSet([dests.dest1, dests.dest2]))
ps = new PersisterService(persisted, eventBus, 100, fileSource) ps = new PersisterService(persisted, eventBus, 100, fileSource)
ps.onUILoadedEvent(null) ps.start()
Thread.sleep(1500) Thread.sleep(1500)
JsonSlurper jsonSlurper = new JsonSlurper() JsonSlurper jsonSlurper = new JsonSlurper()

View File

@@ -72,9 +72,6 @@ class HostCacheTest {
TrustLevel.NEUTRAL TrustLevel.NEUTRAL
} }
settingsMock.ignore.allowUntrusted { true } settingsMock.ignore.allowUntrusted { true }
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks() initMocks()
@@ -94,10 +91,6 @@ class HostCacheTest {
TrustLevel.DISTRUSTED TrustLevel.DISTRUSTED
} }
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks() initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1)) cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
@@ -111,9 +104,6 @@ class HostCacheTest {
TrustLevel.NEUTRAL TrustLevel.NEUTRAL
} }
settingsMock.ignore.allowUntrusted { false } settingsMock.ignore.allowUntrusted { false }
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks() initMocks()
@@ -133,9 +123,6 @@ class HostCacheTest {
} }
trustMock.demand.getLevel{ d -> TrustLevel.TRUSTED } trustMock.demand.getLevel{ d -> TrustLevel.TRUSTED }
trustMock.demand.getLevel{ d -> TrustLevel.TRUSTED } trustMock.demand.getLevel{ d -> TrustLevel.TRUSTED }
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks() initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1)) cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
@@ -152,14 +139,6 @@ class HostCacheTest {
assert d == destinations.dest1 assert d == destinations.dest1
TrustLevel.TRUSTED TrustLevel.TRUSTED
} }
trustMock.demand.getLevel { d ->
assert d == destinations.dest1
TrustLevel.TRUSTED
}
settingsMock.ignore.getHostClearInterval { 100 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks() initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1)) cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
@@ -179,10 +158,6 @@ class HostCacheTest {
TrustLevel.TRUSTED TrustLevel.TRUSTED
} }
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks() initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1)) cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
@@ -208,10 +183,6 @@ class HostCacheTest {
TrustLevel.TRUSTED TrustLevel.TRUSTED
} }
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks() initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1)) cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
@@ -243,10 +214,6 @@ class HostCacheTest {
TrustLevel.TRUSTED TrustLevel.TRUSTED
} }
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks() initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1)) cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1)) cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
@@ -262,11 +229,6 @@ class HostCacheTest {
assert d == destinations.dest1 assert d == destinations.dest1
TrustLevel.TRUSTED TrustLevel.TRUSTED
} }
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks() initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1)) cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
Thread.sleep(150) Thread.sleep(150)
@@ -298,10 +260,6 @@ class HostCacheTest {
TrustLevel.TRUSTED TrustLevel.TRUSTED
} }
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks() initMocks()
def rv = cache.getHosts(5) def rv = cache.getHosts(5)
assert rv.size() == 1 assert rv.size() == 1

View File

@@ -83,11 +83,4 @@ class SearchIndexTest {
assert found.size() == 1 assert found.size() == 1
assert found.contains("b c.d") assert found.contains("b c.d")
} }
@Test
void testDuplicateTerm() {
initIndex(["MuWire-0.3.3.jar"])
def found = index.search(["muwire", "0", "3", "jar"])
assert found.size() == 1
}
} }

View File

@@ -9,18 +9,18 @@ import com.muwire.core.InfoHash
class RequestParsingTest { class RequestParsingTest {
ContentRequest request Request request
private void fromString(String requestString) { private void fromString(String requestString) {
def is = new ByteArrayInputStream(requestString.getBytes(StandardCharsets.US_ASCII)) def is = new ByteArrayInputStream(requestString.getBytes(StandardCharsets.US_ASCII))
request = Request.parseContentRequest(new InfoHash(new byte[InfoHash.SIZE]), is) request = Request.parse(new InfoHash(new byte[InfoHash.SIZE]), is)
} }
private static void failed(String requestString) { private static void failed(String requestString) {
try { try {
def is = new ByteArrayInputStream(requestString.getBytes(StandardCharsets.US_ASCII)) def is = new ByteArrayInputStream(requestString.getBytes(StandardCharsets.US_ASCII))
Request.parseContentRequest(new InfoHash(new byte[InfoHash.SIZE]), is) Request.parse(new InfoHash(new byte[InfoHash.SIZE]), is)
assert false assert false
} catch (IOException expected) {} } catch (IOException expected) {}
} }

View File

@@ -9,9 +9,6 @@ import org.junit.Test
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
import com.muwire.core.connection.Endpoint import com.muwire.core.connection.Endpoint
import com.muwire.core.download.Pieces
import com.muwire.core.files.FileHasher
import com.muwire.core.mesh.Mesh
class UploaderTest { class UploaderTest {
@@ -22,7 +19,7 @@ class UploaderTest {
InputStream is InputStream is
OutputStream os OutputStream os
ContentRequest request Request request
Uploader uploader Uploader uploader
byte[] inFile byte[] inFile
@@ -55,13 +52,7 @@ class UploaderTest {
} }
private void startUpload() { private void startUpload() {
def hasher = new FileHasher() uploader = new Uploader(file, request, endpoint)
InfoHash infoHash = hasher.hashFile(file)
Pieces pieces = new Pieces(FileHasher.getPieceSize(file.length()))
for (int i = 0; i < pieces.nPieces; i++)
pieces.markDownloaded(i)
Mesh mesh = new Mesh(infoHash, pieces)
uploader = new ContentUploader(file, request, endpoint, mesh, FileHasher.getPieceSize(file.length()))
uploadThread = new Thread(uploader.respond() as Runnable) uploadThread = new Thread(uploader.respond() as Runnable)
uploadThread.setDaemon(true) uploadThread.setDaemon(true)
uploadThread.start() uploadThread.start()
@@ -86,11 +77,10 @@ class UploaderTest {
@Test @Test
public void testSmallFile() { public void testSmallFile() {
fillFile(20) fillFile(20)
request = new ContentRequest(range : new Range(0,19)) request = new Request(range : new Range(0,19))
startUpload() startUpload()
assert "200 OK" == readUntilRN() assert "200 OK" == readUntilRN()
assert "Content-Range: 0-19" == readUntilRN() assert "Content-Range: 0-19" == readUntilRN()
assert readUntilRN().startsWith("X-Have")
assert "" == readUntilRN() assert "" == readUntilRN()
byte [] data = new byte[20] byte [] data = new byte[20]
@@ -102,11 +92,10 @@ class UploaderTest {
@Test @Test
public void testRequestMiddle() { public void testRequestMiddle() {
fillFile(20) fillFile(20)
request = new ContentRequest(range : new Range(5,15)) request = new Request(range : new Range(5,15))
startUpload() startUpload()
assert "200 OK" == readUntilRN() assert "200 OK" == readUntilRN()
assert "Content-Range: 5-15" == readUntilRN() assert "Content-Range: 5-15" == readUntilRN()
assert readUntilRN().startsWith("X-Have")
assert "" == readUntilRN() assert "" == readUntilRN()
byte [] data = new byte[11] byte [] data = new byte[11]
@@ -119,10 +108,9 @@ class UploaderTest {
@Test @Test
public void testOutOfRange() { public void testOutOfRange() {
fillFile(20) fillFile(20)
request = new ContentRequest(range : new Range(0,20)) request = new Request(range : new Range(0,20))
startUpload() startUpload()
assert "416 Range Not Satisfiable" == readUntilRN() assert "416 Range Not Satisfiable" == readUntilRN()
assert readUntilRN().startsWith("X-Have")
assert "" == readUntilRN() assert "" == readUntilRN()
} }
@@ -130,12 +118,11 @@ class UploaderTest {
public void testLargeFile() { public void testLargeFile() {
final int length = 0x1 << 14 final int length = 0x1 << 14
fillFile(length) fillFile(length)
request = new ContentRequest(range : new Range(0, length - 1)) request = new Request(range : new Range(0, length - 1))
startUpload() startUpload()
readUntilRN() readUntilRN()
readUntilRN() readUntilRN()
readUntilRN() readUntilRN()
readUntilRN()
byte [] data = new byte[length] byte [] data = new byte[length]
DataInputStream dis = new DataInputStream(is) DataInputStream dis = new DataInputStream(is)

View File

@@ -49,7 +49,7 @@ Files are transferred over HTTP1.1 protocol with some custom headers added for d
### Mesh management ### Mesh management
Download mesh management is a simplified version of Gnutella's "Alternate Location" system. For more information see the "download-mesh" document. Download mesh management is identical to Gnutella, except instead of ip addresses MuWire personas are used. [More information](http://rfc-gnutella.sourceforge.net/developer/tmp/download-mesh.html)
### In-Network updates ### In-Network updates

View File

@@ -1,15 +0,0 @@
# Download Mesh / Partial Sharing
MuWire uses a system similar to Gnutella's "Alternate Location" download mesh management system, however it is simplified to account for I2P's strengths and borrows a bit from BitTorrent's "Have" message.
### "X-Have" header
With every request a downloader makes it sends an "X-Have" header containing the Base64-encoded representation of a bitfield where bits set to 1 represent pieces of the file that the downloader already has. To make partial file sharing possible, if the uploader does not have the complete file it also sends this header in every response. If the header is missing it is assumed the uploader has the complete file.
### "X-Alt" header
The uploader can recommend other uploaders to the downloader via the "X-Alt" header. The format of this header is a comma-separated list of Base64-encoded Personas that have previously reported having at least one piece of the file to the uploader via the "X-Have" header.
### Differences from Gnutella
Unlike Gnutella the uploader is the sole repository where possible sources of the file are tracked. There is no negative "X-Nalt" header to prevent attacking the download mesh by mass downvoting of sources.

View File

@@ -1,20 +1,5 @@
group = com.muwire group = com.muwire
version = 0.5.2 version = 0.2.1
i2pVersion = 0.9.42
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
grailsVersion=4.0.0
gorm.version=7.0.2.RELEASE
sourceCompatibility=1.8
targetCompatibility=1.8
# plugin properties
author = zab@mail.i2p
signer = zab@mail.i2p
keystorePassword=changeit
websiteURL=http://muwire.i2p
updateURLsu3=http://muwire.i2p/MuWire.su3
pack200=true

34
gradlew vendored
View File

@@ -11,21 +11,21 @@
PRG="$0" PRG="$0"
# Need this for relative symlinks. # Need this for relative symlinks.
while [ -h "$PRG" ] ; do while [ -h "$PRG" ] ; do
ls=$(ls -ld "$PRG") ls=`ls -ld "$PRG"`
link=$(expr "$ls" : '.*-> \(.*\)$') link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then if expr "$link" : '/.*' > /dev/null; then
PRG="$link" PRG="$link"
else else
PRG=$(dirname "$PRG")"/$link" PRG=`dirname "$PRG"`"/$link"
fi fi
done done
SAVED="$(pwd)" SAVED="`pwd`"
cd "$(dirname "$PRG")/" >/dev/null cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="$(pwd -P)" APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null cd "$SAVED" >/dev/null
APP_NAME="Gradle" APP_NAME="Gradle"
APP_BASE_NAME=$(basename "$0") APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS="" DEFAULT_JVM_OPTS=""
@@ -49,7 +49,7 @@ cygwin=false
msys=false msys=false
darwin=false darwin=false
nonstop=false nonstop=false
case "$(uname)" in case "`uname`" in
CYGWIN* ) CYGWIN* )
cygwin=true cygwin=true
;; ;;
@@ -90,7 +90,7 @@ fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=$(ulimit -H -n) MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT" MAX_FD="$MAX_FD_LIMIT"
@@ -111,12 +111,12 @@ fi
# For Cygwin, switch paths to Windows format before running java # For Cygwin, switch paths to Windows format before running java
if $cygwin ; then if $cygwin ; then
APP_HOME=$(cygpath --path --mixed "$APP_HOME") APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=$(cygpath --path --mixed "$CLASSPATH") CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=$(cygpath --unix "$JAVACMD") JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath # We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=$(find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null) ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP="" SEP=""
for dir in $ROOTDIRSRAW ; do for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir" ROOTDIRS="$ROOTDIRS$SEP$dir"
@@ -130,13 +130,13 @@ if $cygwin ; then
# Now convert the arguments - kludge to limit ourselves to /bin/sh # Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0 i=0
for arg in "$@" ; do for arg in "$@" ; do
CHECK=$(echo "$arg"|egrep -c "$OURCYGPATTERN" -) CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=$(echo "$arg"|egrep -c "^-") ### Determine if an option CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval $(echo args$i)=$(cygpath --path --ignore --mixed "$arg") eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else else
eval $(echo args$i)="\"$arg\"" eval `echo args$i`="\"$arg\""
fi fi
i=$((i+1)) i=$((i+1))
done done

View File

@@ -44,9 +44,9 @@ mainClassName = 'com.muwire.gui.Launcher'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties'] 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'
// apply from: 'gradle/code-quality.gradle' apply from: 'gradle/code-quality.gradle'
// apply from: 'gradle/integration-test.gradle' apply from: 'gradle/integration-test.gradle'
// apply from: 'gradle/package.gradle' // apply from: 'gradle/package.gradle'
apply from: 'gradle/docs.gradle' apply from: 'gradle/docs.gradle'
apply plugin: 'com.github.johnrengelman.shadow' apply plugin: 'com.github.johnrengelman.shadow'
@@ -58,11 +58,7 @@ dependencies {
compile project(":core") compile project(":core")
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 group: 'org.slf4j', name: 'slf4j-jdk14', version: "${slf4jVersion}"
runtime group: 'org.slf4j', name: 'slf4j-api', version: "${slf4jVersion}"
runtime group: 'org.slf4j', name: 'jul-to-slf4j', version: "${slf4jVersion}"
runtime "javax.annotation:javax.annotation-api:1.3.2" 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}"
@@ -123,7 +119,6 @@ if (hasProperty('debugRun') && ((project.debugRun as boolean))) {
} }
} }
/*
task jacocoRootMerge(type: org.gradle.testing.jacoco.tasks.JacocoMerge, dependsOn: [test, jacocoTestReport, jacocoIntegrationTestReport]) { task jacocoRootMerge(type: org.gradle.testing.jacoco.tasks.JacocoMerge, dependsOn: [test, jacocoTestReport, jacocoIntegrationTestReport]) {
executionData = files(jacocoTestReport.executionData, jacocoIntegrationTestReport.executionData) executionData = files(jacocoTestReport.executionData, jacocoIntegrationTestReport.executionData)
destinationFile = file("${buildDir}/jacoco/root.exec") destinationFile = file("${buildDir}/jacoco/root.exec")
@@ -143,5 +138,4 @@ task jacocoRootReport(dependsOn: jacocoRootMerge, type: JacocoReport) {
xml.destination = file("${buildDir}/reports/jacoco/root/root.xml") xml.destination = file("${buildDir}/reports/jacoco/root/root.xml")
} }
} }
*/

View File

@@ -26,39 +26,4 @@ mvcGroups {
view = 'com.muwire.gui.OptionsView' view = 'com.muwire.gui.OptionsView'
controller = 'com.muwire.gui.OptionsController' controller = 'com.muwire.gui.OptionsController'
} }
"mu-wire-status" {
model = 'com.muwire.gui.MuWireStatusModel'
view = 'com.muwire.gui.MuWireStatusView'
controller = 'com.muwire.gui.MuWireStatusController'
}
'i-2-p-status' {
model = 'com.muwire.gui.I2PStatusModel'
view = 'com.muwire.gui.I2PStatusView'
controller = 'com.muwire.gui.I2PStatusController'
}
'trust-list' {
model = 'com.muwire.gui.TrustListModel'
view = 'com.muwire.gui.TrustListView'
controller = 'com.muwire.gui.TrustListController'
}
'content-panel' {
model = 'com.muwire.gui.ContentPanelModel'
view = 'com.muwire.gui.ContentPanelView'
controller = 'com.muwire.gui.ContentPanelController'
}
'show-comment' {
model = 'com.muwire.gui.ShowCommentModel'
view = 'com.muwire.gui.ShowCommentView'
controller = 'com.muwire.gui.ShowCommentController'
}
'add-comment' {
model = 'com.muwire.gui.AddCommentModel'
view = 'com.muwire.gui.AddCommentView'
controller = 'com.muwire.gui.AddCommentController'
}
'browse' {
model = 'com.muwire.gui.BrowseModel'
view = 'com.muwire.gui.BrowseView'
controller = 'com.muwire.gui.BrowseController'
}
} }

View File

@@ -1,45 +0,0 @@
package com.muwire.gui
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import net.i2p.data.Base64
import javax.annotation.Nonnull
import com.muwire.core.Core
import com.muwire.core.files.UICommentEvent
import com.muwire.core.util.DataUtil
@ArtifactProviderFor(GriffonController)
class AddCommentController {
@MVCMember @Nonnull
AddCommentModel model
@MVCMember @Nonnull
AddCommentView view
Core core
@ControllerAction
void save() {
String comment = view.textarea.getText()
if (comment.trim().length() == 0)
comment = null
else
comment = Base64.encode(DataUtil.encodei18nString(comment))
model.selectedFiles.each {
def event = new UICommentEvent(sharedFile : it, oldComment : it.getComment())
it.setComment(comment)
core.eventBus.publish(event)
}
mvcGroup.parentGroup.view.refreshSharedFiles()
cancel()
}
@ControllerAction
void cancel() {
view.dialog.setVisible(false)
mvcGroup.destroy()
}
}

View File

@@ -1,99 +0,0 @@
package com.muwire.gui
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import net.i2p.data.Base64
import javax.annotation.Nonnull
import com.muwire.core.EventBus
import com.muwire.core.download.UIDownloadEvent
import com.muwire.core.search.BrowseStatus
import com.muwire.core.search.BrowseStatusEvent
import com.muwire.core.search.UIBrowseEvent
import com.muwire.core.search.UIResultEvent
@ArtifactProviderFor(GriffonController)
class BrowseController {
@MVCMember @Nonnull
BrowseModel model
@MVCMember @Nonnull
BrowseView view
EventBus eventBus
void register() {
eventBus.register(BrowseStatusEvent.class, this)
eventBus.register(UIResultEvent.class, this)
eventBus.publish(new UIBrowseEvent(host : model.host))
}
void mvcGroupDestroy() {
eventBus.unregister(BrowseStatusEvent.class, this)
eventBus.unregister(UIResultEvent.class, this)
}
void onBrowseStatusEvent(BrowseStatusEvent e) {
runInsideUIAsync {
model.status = e.status
if (e.status == BrowseStatus.FETCHING)
model.totalResults = e.totalResults
}
}
void onUIResultEvent(UIResultEvent e) {
runInsideUIAsync {
model.results << e
model.resultCount = model.results.size()
view.resultsTable.model.fireTableDataChanged()
}
}
@ControllerAction
void dismiss() {
view.dialog.setVisible(false)
mvcGroup.destroy()
}
@ControllerAction
void download() {
def selectedResults = view.selectedResults()
if (selectedResults == null || selectedResults.isEmpty())
return
selectedResults.removeAll {
!mvcGroup.parentGroup.parentGroup.model.canDownload(it.infohash)
}
selectedResults.each { result ->
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
eventBus.publish(new UIDownloadEvent(
result : [result],
sources : [model.host.destination],
target : file,
sequential : mvcGroup.parentGroup.view.sequentialDownloadCheckbox.model.isSelected()
))
}
mvcGroup.parentGroup.parentGroup.view.showDownloadsWindow.call()
dismiss()
}
@ControllerAction
void viewComment() {
def selectedResults = view.selectedResults()
if (selectedResults == null || selectedResults.size() != 1)
return
def result = selectedResults[0]
if (result.comment == null)
return
String groupId = Base64.encode(result.infohash.getRoot())
Map<String,Object> params = new HashMap<>()
params['result'] = result
mvcGroup.createMVCGroup("show-comment", groupId, params)
}
}

View File

@@ -1,105 +0,0 @@
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
import com.muwire.core.EventBus
import com.muwire.core.content.ContentControlEvent
import com.muwire.core.content.Match
import com.muwire.core.content.Matcher
import com.muwire.core.content.RegexMatcher
import com.muwire.core.trust.TrustEvent
import com.muwire.core.trust.TrustLevel
@ArtifactProviderFor(GriffonController)
class ContentPanelController {
@MVCMember @Nonnull
ContentPanelModel model
@MVCMember @Nonnull
ContentPanelView view
Core core
@ControllerAction
void addRule() {
def term = view.ruleTextField.text
if (model.regex)
core.muOptions.watchedRegexes.add(term)
else
core.muOptions.watchedKeywords.add(term)
saveMuWireSettings()
core.eventBus.publish(new ContentControlEvent(term : term, regex : model.regex, add:true))
}
@ControllerAction
void deleteRule() {
int rule = view.getSelectedRule()
if (rule < 0)
return
Matcher matcher = model.rules[rule]
String term = matcher.getTerm()
if (matcher instanceof RegexMatcher)
core.muOptions.watchedRegexes.remove(term)
else
core.muOptions.watchedKeywords.remove(term)
saveMuWireSettings()
core.eventBus.publish(new ContentControlEvent(term : term, regex : (matcher instanceof RegexMatcher), add: false))
}
@ControllerAction
void keyword() {
model.regex = false
}
@ControllerAction
void regex() {
model.regex = true
}
@ControllerAction
void refresh() {
model.refresh()
}
@ControllerAction
void clearHits() {
int selectedRule = view.getSelectedRule()
if (selectedRule < 0)
return
Matcher matcher = model.rules[selectedRule]
matcher.matches.clear()
model.refresh()
}
@ControllerAction
void trust() {
int selectedHit = view.getSelectedHit()
if (selectedHit < 0)
return
Match m = model.hits[selectedHit]
core.eventBus.publish(new TrustEvent(persona : m.persona, level : TrustLevel.TRUSTED))
}
@ControllerAction
void distrust() {
int selectedHit = view.getSelectedHit()
if (selectedHit < 0)
return
Match m = model.hits[selectedHit]
core.eventBus.publish(new TrustEvent(persona : m.persona, level : TrustLevel.DISTRUSTED))
}
void saveMuWireSettings() {
File f = new File(core.home, "MuWire.properties")
f.withOutputStream {
core.muOptions.write(it)
}
}
}

View File

@@ -1,41 +0,0 @@
package com.muwire.gui
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import net.i2p.router.Router
import javax.annotation.Nonnull
import com.muwire.core.Core
@ArtifactProviderFor(GriffonController)
class I2PStatusController {
@MVCMember @Nonnull
I2PStatusModel model
@MVCMember @Nonnull
I2PStatusView view
@ControllerAction
void refresh() {
Core core = application.context.get("core")
Router router = core.router
model.networkStatus = router._context.commSystem().status.toStatusString()
model.floodfill = router._context.netDb().floodfillEnabled()
model.ntcpConnections = router._context.commSystem().getTransports()["NTCP"].countPeers()
model.ssuConnections = router._context.commSystem().getTransports()["SSU"].countPeers()
model.participatingTunnels = router._context.tunnelManager().getParticipatingCount()
model.activePeers = router._context.profileOrganizer().countActivePeers()
model.receiveBps = router._context.bandwidthLimiter().getReceiveBps15s()
model.sendBps = router._context.bandwidthLimiter().getSendBps15s()
model.participatingBW = router._context.bandwidthLimiter().getCurrentParticipatingBandwidth()
}
@ControllerAction
void close() {
view.dialog.setVisible(false)
mvcGroup.destroy()
}
}

View File

@@ -11,27 +11,16 @@ import net.i2p.data.Base64
import javax.annotation.Nonnull import javax.annotation.Nonnull
import javax.inject.Inject import javax.inject.Inject
import javax.swing.JTable
import com.muwire.core.Constants
import com.muwire.core.Core import com.muwire.core.Core
import com.muwire.core.Persona
import com.muwire.core.SharedFile
import com.muwire.core.SplitPattern
import com.muwire.core.download.Downloader
import com.muwire.core.download.DownloadStartedEvent import com.muwire.core.download.DownloadStartedEvent
import com.muwire.core.download.UIDownloadCancelledEvent import com.muwire.core.download.UIDownloadCancelledEvent
import com.muwire.core.download.UIDownloadEvent import com.muwire.core.download.UIDownloadEvent
import com.muwire.core.download.UIDownloadPausedEvent
import com.muwire.core.download.UIDownloadResumedEvent
import com.muwire.core.files.DirectoryUnsharedEvent
import com.muwire.core.files.FileUnsharedEvent
import com.muwire.core.files.UIPersistFilesEvent
import com.muwire.core.search.QueryEvent import com.muwire.core.search.QueryEvent
import com.muwire.core.search.SearchEvent import com.muwire.core.search.SearchEvent
import com.muwire.core.trust.RemoteTrustList
import com.muwire.core.trust.TrustEvent import com.muwire.core.trust.TrustEvent
import com.muwire.core.trust.TrustLevel import com.muwire.core.trust.TrustLevel
import com.muwire.core.trust.TrustSubscriptionEvent
@ArtifactProviderFor(GriffonController) @ArtifactProviderFor(GriffonController)
class MainFrameController { class MainFrameController {
@@ -41,8 +30,6 @@ class MainFrameController {
@MVCMember @Nonnull @MVCMember @Nonnull
MainFrameModel model MainFrameModel model
@MVCMember @Nonnull
MainFrameView view
private volatile Core core private volatile Core core
@@ -55,13 +42,10 @@ class MainFrameController {
search = search.trim() search = search.trim()
if (search.length() == 0) if (search.length() == 0)
return return
if (search.length() > 128)
search = search.substring(0,128)
def uuid = UUID.randomUUID() def uuid = UUID.randomUUID()
Map<String, Object> params = new HashMap<>() Map<String, Object> params = new HashMap<>()
params["search-terms"] = search params["search-terms"] = search
params["uuid"] = uuid.toString() params["uuid"] = uuid.toString()
params["core"] = core
def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params) def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params)
model.results[uuid.toString()] = group model.results[uuid.toString()] = group
@@ -78,18 +62,14 @@ class MainFrameController {
def searchEvent def searchEvent
if (hashSearch) { if (hashSearch) {
searchEvent = new SearchEvent(searchHash : root, uuid : uuid, oobInfohash: true, compressedResults : true) searchEvent = new SearchEvent(searchHash : root, uuid : uuid)
} else { } else {
// this can be improved a lot // this can be improved a lot
def replaced = search.toLowerCase().trim().replaceAll(SplitPattern.SPLIT_PATTERN, " ") def replaced = search.toLowerCase().trim().replaceAll(Constants.SPLIT_PATTERN, " ")
def terms = replaced.split(" ") def terms = replaced.split(" ")
def nonEmpty = [] searchEvent = new SearchEvent(searchTerms : terms, uuid : uuid, oobInfohash: true)
terms.each { if (it.length() > 0) nonEmpty << it }
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : uuid, oobInfohash: true,
searchComments : core.muOptions.searchComments, compressedResults : true)
} }
boolean firstHop = core.muOptions.allowUntrusted || core.muOptions.searchExtraHop core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true,
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : firstHop,
replyTo: core.me.destination, receivedOn: core.me.destination, replyTo: core.me.destination, receivedOn: core.me.destination,
originator : core.me)) originator : core.me))
} }
@@ -101,7 +81,6 @@ class MainFrameController {
Map<String, Object> params = new HashMap<>() Map<String, Object> params = new HashMap<>()
params["search-terms"] = tabTitle params["search-terms"] = tabTitle
params["uuid"] = uuid.toString() params["uuid"] = uuid.toString()
params["core"] = core
def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params) def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params)
model.results[uuid.toString()] = group model.results[uuid.toString()] = group
@@ -112,6 +91,20 @@ class MainFrameController {
originator : core.me)) originator : core.me))
} }
private def selectedResult() {
def selected = builder.getVariable("result-tabs").getSelectedComponent()
def group = selected.getClientProperty("mvc-group")
def table = selected.getClientProperty("results-table")
int row = table.getSelectedRow()
if (row == -1)
return
def sortEvt = group.view.lastSortEvent
if (sortEvt != null) {
row = group.view.resultsTable.rowSorter.convertRowIndexToModel(row)
}
group.model.results[row]
}
private int selectedDownload() { private int selectedDownload() {
def downloadsTable = builder.getVariable("downloads-table") def downloadsTable = builder.getVariable("downloads-table")
def selected = downloadsTable.getSelectedRow() def selected = downloadsTable.getSelectedRow()
@@ -122,28 +115,41 @@ class MainFrameController {
} }
@ControllerAction @ControllerAction
void trustPersonaFromSearch() { void download() {
int selected = builder.getVariable("searches-table").getSelectedRow() def result = selectedResult()
if (selected < 0) if (result == null)
return return // TODO disable button
Persona p = model.searches[selected].originator
core.eventBus.publish( new TrustEvent(persona : p, level : TrustLevel.TRUSTED) ) 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 distrustPersonaFromSearch() { void trust() {
int selected = builder.getVariable("searches-table").getSelectedRow() def result = selectedResult()
if (selected < 0) if (result == null)
return return // TODO disable button
Persona p = model.searches[selected].originator core.eventBus.publish( new TrustEvent(persona : result.sender, level : TrustLevel.TRUSTED))
core.eventBus.publish( new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED) ) }
@ControllerAction
void distrust() {
def result = selectedResult()
if (result == null)
return // TODO disable button
core.eventBus.publish( new TrustEvent(persona : result.sender, level : TrustLevel.DISTRUSTED))
} }
@ControllerAction @ControllerAction
void cancel() { void cancel() {
def downloader = model.downloads[selectedDownload()].downloader def downloader = model.downloads[selectedDownload()].downloader
downloader.cancel() downloader.cancel()
model.downloadInfoHashes.remove(downloader.getInfoHash())
core.eventBus.publish(new UIDownloadCancelledEvent(downloader : downloader)) core.eventBus.publish(new UIDownloadCancelledEvent(downloader : downloader))
} }
@@ -151,153 +157,37 @@ class MainFrameController {
void resume() { void resume() {
def downloader = model.downloads[selectedDownload()].downloader def downloader = model.downloads[selectedDownload()].downloader
downloader.resume() downloader.resume()
core.eventBus.publish(new UIDownloadResumedEvent())
}
@ControllerAction
void pause() {
def downloader = model.downloads[selectedDownload()].downloader
downloader.pause()
core.eventBus.publish(new UIDownloadPausedEvent())
}
@ControllerAction
void clear() {
def toRemove = []
model.downloads.each {
if (it.downloader.getCurrentState() == Downloader.DownloadState.CANCELLED) {
toRemove << it
} else if (it.downloader.getCurrentState() == Downloader.DownloadState.FINISHED) {
toRemove << it
}
}
toRemove.each {
model.downloads.remove(it)
}
model.clearButtonEnabled = false
} }
private void markTrust(String tableName, TrustLevel level, def list) { private void markTrust(String tableName, TrustLevel level, def list) {
int row = view.getSelectedTrustTablesRow(tableName) int row = builder.getVariable(tableName).getSelectedRow()
if (row < 0) if (row < 0)
return return
builder.getVariable(tableName).model.fireTableDataChanged()
core.eventBus.publish(new TrustEvent(persona : list[row], level : level)) core.eventBus.publish(new TrustEvent(persona : list[row], level : level))
} }
@ControllerAction @ControllerAction
void markTrusted() { void markTrusted() {
markTrust("distrusted-table", TrustLevel.TRUSTED, model.distrusted) markTrust("distrusted-table", TrustLevel.TRUSTED, model.distrusted)
model.markTrustedButtonEnabled = false
model.markNeutralFromDistrustedButtonEnabled = false
} }
@ControllerAction @ControllerAction
void markNeutralFromDistrusted() { void markNeutralFromDistrusted() {
markTrust("distrusted-table", TrustLevel.NEUTRAL, model.distrusted) markTrust("distrusted-table", TrustLevel.NEUTRAL, model.distrusted)
model.markTrustedButtonEnabled = false
model.markNeutralFromDistrustedButtonEnabled = false
} }
@ControllerAction @ControllerAction
void markDistrusted() { void markDistrusted() {
markTrust("trusted-table", TrustLevel.DISTRUSTED, model.trusted) markTrust("trusted-table", TrustLevel.DISTRUSTED, model.trusted)
model.subscribeButtonEnabled = false
model.markDistrustedButtonEnabled = false
model.markNeutralFromTrustedButtonEnabled = false
} }
@ControllerAction @ControllerAction
void markNeutralFromTrusted() { void markNeutralFromTrusted() {
markTrust("trusted-table", TrustLevel.NEUTRAL, model.trusted) markTrust("trusted-table", TrustLevel.NEUTRAL, model.trusted)
model.subscribeButtonEnabled = false
model.markDistrustedButtonEnabled = false
model.markNeutralFromTrustedButtonEnabled = false
} }
@ControllerAction void unshareSelectedFiles() {
void subscribe() { println "unsharing selected files"
int row = view.getSelectedTrustTablesRow("trusted-table")
if (row < 0)
return
Persona p = model.trusted[row]
core.muOptions.trustSubscriptions.add(p)
saveMuWireSettings()
core.eventBus.publish(new TrustSubscriptionEvent(persona : p, subscribe : true))
model.subscribeButtonEnabled = false
model.markDistrustedButtonEnabled = false
model.markNeutralFromTrustedButtonEnabled = false
}
@ControllerAction
void review() {
RemoteTrustList list = getSelectedTrustList()
if (list == null)
return
Map<String,Object> env = new HashMap<>()
env["trustList"] = list
env["trustService"] = core.trustService
env["eventBus"] = core.eventBus
mvcGroup.createMVCGroup("trust-list", env)
}
@ControllerAction
void update() {
RemoteTrustList list = getSelectedTrustList()
if (list == null)
return
core.eventBus.publish(new TrustSubscriptionEvent(persona : list.persona, subscribe : true))
}
@ControllerAction
void unsubscribe() {
RemoteTrustList list = getSelectedTrustList()
if (list == null)
return
core.muOptions.trustSubscriptions.remove(list.persona)
saveMuWireSettings()
model.subscriptions.remove(list)
JTable table = builder.getVariable("subscription-table")
table.model.fireTableDataChanged()
core.eventBus.publish(new TrustSubscriptionEvent(persona : list.persona, subscribe : false))
}
private RemoteTrustList getSelectedTrustList() {
int row = view.getSelectedTrustTablesRow("subscription-table")
if (row < 0)
return null
model.subscriptions[row]
}
void unshareSelectedFile() {
def sf = view.selectedSharedFiles()
if (sf == null)
return
sf.each {
core.eventBus.publish(new FileUnsharedEvent(unsharedFile : it))
}
core.eventBus.publish(new UIPersistFilesEvent())
}
@ControllerAction
void addComment() {
def selectedFiles = view.selectedSharedFiles()
if (selectedFiles == null || selectedFiles.isEmpty())
return
Map<String, Object> params = new HashMap<>()
params['selectedFiles'] = selectedFiles
params['core'] = core
mvcGroup.createMVCGroup("add-comment", "Add Comment", params)
}
void saveMuWireSettings() {
File f = new File(core.home, "MuWire.properties")
f.withOutputStream {
core.muOptions.write(it)
}
} }
void mvcGroupInit(Map<String, String> args) { void mvcGroupInit(Map<String, String> args) {

View File

@@ -1,48 +0,0 @@
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 MuWireStatusController {
@MVCMember @Nonnull
MuWireStatusModel model
@MVCMember @Nonnull
MuWireStatusView view
@ControllerAction
void refresh() {
Core core = application.context.get("core")
int incoming = 0
int outgoing = 0
core.connectionManager.getConnections().each {
if (it.incoming)
incoming++
else
outgoing++
}
model.incomingConnections = incoming
model.outgoingConnections = outgoing
model.knownHosts = core.hostCache.hosts.size()
model.failingHosts = core.hostCache.countFailingHosts()
model.hopelessHosts = core.hostCache.countHopelessHosts()
model.sharedFiles = core.fileManager.fileToSharedFile.size()
model.downloads = core.downloadManager.downloaders.size()
}
@ControllerAction
void close() {
view.dialog.setVisible(false)
mvcGroup.destroy()
}
}

View File

@@ -4,15 +4,9 @@ import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor import griffon.metadata.ArtifactProviderFor
import groovy.util.logging.Log
import java.util.logging.Level
import javax.annotation.Nonnull import javax.annotation.Nonnull
import javax.swing.JFileChooser
import com.muwire.core.Core import com.muwire.core.Core
import com.muwire.core.MuWireSettings
@ArtifactProviderFor(GriffonController) @ArtifactProviderFor(GriffonController)
class OptionsController { class OptionsController {
@@ -25,7 +19,6 @@ class OptionsController {
void save() { void save() {
String text String text
Core core = application.context.get("core") Core core = application.context.get("core")
MuWireSettings settings = application.context.get("muwire-settings")
def i2pProps = core.i2pOptions def i2pProps = core.i2pOptions
@@ -45,17 +38,6 @@ class OptionsController {
model.outboundLength = text model.outboundLength = text
i2pProps["outbound.length"] = text i2pProps["outbound.length"] = text
if (settings.embeddedRouter) {
text = view.i2pNTCPPortField.text
model.i2pNTCPPort = text
i2pProps["i2np.ntcp.port"] = text
text = view.i2pUDPPortField.text
model.i2pUDPPort = text
i2pProps["i2np.udp.port"] = text
}
File i2pSettingsFile = new File(core.home, "i2p.properties") File i2pSettingsFile = new File(core.home, "i2p.properties")
i2pSettingsFile.withOutputStream { i2pSettingsFile.withOutputStream {
i2pProps.store(it,"") i2pProps.store(it,"")
@@ -64,68 +46,20 @@ class OptionsController {
text = view.retryField.text text = view.retryField.text
model.downloadRetryInterval = text model.downloadRetryInterval = text
def settings = application.context.get("muwire-settings")
settings.downloadRetryInterval = Integer.valueOf(text) settings.downloadRetryInterval = Integer.valueOf(text)
text = view.updateField.text text = view.updateField.text
model.updateCheckInterval = text model.updateCheckInterval = text
settings.updateCheckInterval = Integer.valueOf(text) settings.updateCheckInterval = Integer.valueOf(text)
boolean searchComments = view.searchCommentsCheckbox.model.isSelected()
model.searchComments = searchComments
settings.searchComments = searchComments
boolean autoDownloadUpdate = view.autoDownloadUpdateCheckbox.model.isSelected()
model.autoDownloadUpdate = autoDownloadUpdate
settings.autoDownloadUpdate = autoDownloadUpdate
boolean shareDownloaded = view.shareDownloadedCheckbox.model.isSelected()
model.shareDownloadedFiles = shareDownloaded
settings.shareDownloadedFiles = shareDownloaded
boolean shareHidden = view.shareHiddenCheckbox.model.isSelected()
model.shareHiddenFiles = shareHidden
settings.shareHiddenFiles = shareHidden
boolean browseFiles = view.browseFilesCheckbox.model.isSelected()
model.browseFiles = browseFiles
settings.browseFiles = browseFiles
text = view.speedSmoothSecondsField.text
model.speedSmoothSeconds = Integer.valueOf(text)
settings.speedSmoothSeconds = Integer.valueOf(text)
String downloadLocation = model.downloadLocation
settings.downloadLocation = new File(downloadLocation)
String incompleteLocation = model.incompleteLocation
settings.incompleteLocation = new File(incompleteLocation)
if (settings.embeddedRouter) {
text = view.inBwField.text
model.inBw = text
settings.inBw = Integer.valueOf(text)
text = view.outBwField.text
model.outBw = text
settings.outBw = Integer.valueOf(text)
}
boolean onlyTrusted = view.allowUntrustedCheckbox.model.isSelected() boolean onlyTrusted = view.allowUntrustedCheckbox.model.isSelected()
model.onlyTrusted = onlyTrusted model.onlyTrusted = onlyTrusted
settings.setAllowUntrusted(!onlyTrusted) settings.setAllowUntrusted(!onlyTrusted)
boolean searchExtraHop = view.searchExtraHopCheckbox.model.isSelected() boolean shareDownloaded = view.shareDownloadedCheckbox.model.isSelected()
model.searchExtraHop = searchExtraHop model.shareDownloadedFiles = shareDownloaded
settings.searchExtraHop = searchExtraHop settings.shareDownloadedFiles = shareDownloaded
boolean trustLists = view.allowTrustListsCheckbox.model.isSelected()
model.trustLists = trustLists
settings.allowTrustLists = trustLists
String trustListInterval = view.trustListIntervalField.text
model.trustListInterval = trustListInterval
settings.trustListInterval = Integer.parseInt(trustListInterval)
File settingsFile = new File(core.home, "MuWire.properties") File settingsFile = new File(core.home, "MuWire.properties")
settingsFile.withOutputStream { settingsFile.withOutputStream {
@@ -143,8 +77,9 @@ class OptionsController {
model.font = text model.font = text
uiSettings.font = text uiSettings.font = text
uiSettings.autoFontSize = model.automaticFontSize boolean showMonitor = view.monitorCheckbox.model.isSelected()
uiSettings.fontSize = Integer.parseInt(view.fontSizeField.text) model.showMonitor = showMonitor
uiSettings.showMonitor = showMonitor
boolean clearCancelledDownloads = view.clearCancelledDownloadsCheckbox.model.isSelected() boolean clearCancelledDownloads = view.clearCancelledDownloadsCheckbox.model.isSelected()
model.clearCancelledDownloads = clearCancelledDownloads model.clearCancelledDownloads = clearCancelledDownloads
@@ -158,6 +93,10 @@ class OptionsController {
model.excludeLocalResult = excludeLocalResult model.excludeLocalResult = excludeLocalResult
uiSettings.excludeLocalResult = excludeLocalResult uiSettings.excludeLocalResult = excludeLocalResult
boolean showSearchHashes = view.showSearchHashesCheckbox.model.isSelected()
model.showSearchHashes = showSearchHashes
uiSettings.showSearchHashes = showSearchHashes
File uiSettingsFile = new File(core.home, "gui.properties") File uiSettingsFile = new File(core.home, "gui.properties")
uiSettingsFile.withOutputStream { uiSettingsFile.withOutputStream {
uiSettings.write(it) uiSettings.write(it)
@@ -171,37 +110,4 @@ class OptionsController {
view.d.setVisible(false) view.d.setVisible(false)
mvcGroup.destroy() mvcGroup.destroy()
} }
@ControllerAction
void downloadLocation() {
def chooser = new JFileChooser()
chooser.setFileHidingEnabled(false)
chooser.setDialogTitle("Select location for downloaded files")
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY)
int rv = chooser.showOpenDialog(null)
if (rv == JFileChooser.APPROVE_OPTION)
model.downloadLocation = chooser.getSelectedFile().getAbsolutePath()
}
@ControllerAction
void incompleteLocation() {
def chooser = new JFileChooser()
chooser.setFileHidingEnabled(false)
chooser.setDialogTitle("Select location for downloaded files")
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY)
int rv = chooser.showOpenDialog(null)
if (rv == JFileChooser.APPROVE_OPTION)
model.incompleteLocation = chooser.getSelectedFile().getAbsolutePath()
}
@ControllerAction
void automaticFontAction() {
model.automaticFontSize = true
model.customFontSize = 12
}
@ControllerAction
void customFontAction() {
model.automaticFontSize = false
}
} }

Some files were not shown because too many files have changed in this diff Show More