Compare commits
1 Commits
muwire-0.5
...
fix-possib
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7718dc0821 |
28
README.md
28
README.md
@@ -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
26
TODO.md
@@ -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)
|
||||||
|
@@ -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'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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
|
||||||
@@ -16,66 +15,66 @@ import com.muwire.core.upload.UploadEvent
|
|||||||
import com.muwire.core.upload.UploadFinishedEvent
|
import com.muwire.core.upload.UploadFinishedEvent
|
||||||
|
|
||||||
class Cli {
|
class Cli {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
def home = System.getProperty("user.home") + File.separator + ".MuWire"
|
def home = System.getProperty("user.home") + File.separator + ".MuWire"
|
||||||
home = new File(home)
|
home = new File(home)
|
||||||
if (!home.exists())
|
if (!home.exists())
|
||||||
home.mkdirs()
|
home.mkdirs()
|
||||||
|
|
||||||
def propsFile = new File(home,"MuWire.properties")
|
def propsFile = new File(home,"MuWire.properties")
|
||||||
if (!propsFile.exists()) {
|
if (!propsFile.exists()) {
|
||||||
println "create props file ${propsFile.getAbsoluteFile()} before launching MuWire"
|
println "create props file ${propsFile.getAbsoluteFile()} before launching MuWire"
|
||||||
System.exit(1)
|
System.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
def props = new Properties()
|
def props = new Properties()
|
||||||
propsFile.withInputStream { props.load(it) }
|
propsFile.withInputStream { props.load(it) }
|
||||||
props = new MuWireSettings(props)
|
props = new MuWireSettings(props)
|
||||||
|
|
||||||
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"
|
||||||
System.exit(1)
|
System.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def filesList
|
def filesList
|
||||||
if (args.length == 0) {
|
if (args.length == 0) {
|
||||||
println "Enter a file containing list of files to share"
|
println "Enter a file containing list of files to share"
|
||||||
def reader = new BufferedReader(new InputStreamReader(System.in))
|
def reader = new BufferedReader(new InputStreamReader(System.in))
|
||||||
filesList = reader.readLine()
|
filesList = reader.readLine()
|
||||||
} else
|
} else
|
||||||
filesList = args[0]
|
filesList = args[0]
|
||||||
|
|
||||||
Thread.sleep(1000)
|
Thread.sleep(1000)
|
||||||
println "loading shared files from $filesList"
|
println "loading shared files from $filesList"
|
||||||
|
|
||||||
// listener for shared files
|
// listener for shared files
|
||||||
def sharedListener = new SharedListener()
|
def sharedListener = new SharedListener()
|
||||||
core.eventBus.register(FileHashedEvent.class, sharedListener)
|
core.eventBus.register(FileHashedEvent.class, sharedListener)
|
||||||
core.eventBus.register(FileLoadedEvent.class, sharedListener)
|
core.eventBus.register(FileLoadedEvent.class, sharedListener)
|
||||||
|
|
||||||
// for connections
|
// for connections
|
||||||
def connectionsListener = new ConnectionListener()
|
def connectionsListener = new ConnectionListener()
|
||||||
core.eventBus.register(ConnectionEvent.class, connectionsListener)
|
core.eventBus.register(ConnectionEvent.class, connectionsListener)
|
||||||
core.eventBus.register(DisconnectionEvent.class, connectionsListener)
|
core.eventBus.register(DisconnectionEvent.class, connectionsListener)
|
||||||
|
|
||||||
// for uploads
|
// for uploads
|
||||||
def uploadsListener = new UploadsListener()
|
def uploadsListener = new UploadsListener()
|
||||||
core.eventBus.register(UploadEvent.class, uploadsListener)
|
core.eventBus.register(UploadEvent.class, uploadsListener)
|
||||||
core.eventBus.register(UploadFinishedEvent.class, uploadsListener)
|
core.eventBus.register(UploadFinishedEvent.class, uploadsListener)
|
||||||
|
|
||||||
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)
|
||||||
def fileLoader = new Object() {
|
def fileLoader = new Object() {
|
||||||
public void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
|
public void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
|
||||||
@@ -84,15 +83,14 @@ 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
|
||||||
println "MuWire is ready"
|
println "MuWire is ready"
|
||||||
|
|
||||||
filesList = new File(filesList)
|
filesList = new File(filesList)
|
||||||
filesList.withReader {
|
filesList.withReader {
|
||||||
def toShare = it.readLine()
|
def toShare = it.readLine()
|
||||||
core.eventBus.publish(new FileSharedEvent(file : new File(toShare)))
|
core.eventBus.publish(new FileSharedEvent(file : new File(toShare)))
|
||||||
}
|
}
|
||||||
@@ -103,7 +101,7 @@ class Cli {
|
|||||||
})
|
})
|
||||||
Thread.sleep(Integer.MAX_VALUE)
|
Thread.sleep(Integer.MAX_VALUE)
|
||||||
}
|
}
|
||||||
|
|
||||||
static class ConnectionListener {
|
static class ConnectionListener {
|
||||||
volatile int connections
|
volatile int connections
|
||||||
public void onConnectionEvent(ConnectionEvent e) {
|
public void onConnectionEvent(ConnectionEvent e) {
|
||||||
@@ -114,19 +112,19 @@ class Cli {
|
|||||||
connections--
|
connections--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static class UploadsListener {
|
static class UploadsListener {
|
||||||
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()}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static class SharedListener {
|
static class SharedListener {
|
||||||
volatile int shared
|
volatile int shared
|
||||||
void onFileHashedEvent(FileHashedEvent e) {
|
void onFileHashedEvent(FileHashedEvent e) {
|
||||||
|
@@ -17,31 +17,31 @@ import com.muwire.core.search.UIResultEvent
|
|||||||
import net.i2p.data.Base64
|
import net.i2p.data.Base64
|
||||||
|
|
||||||
class CliDownloader {
|
class CliDownloader {
|
||||||
|
|
||||||
private static final List<Downloader> downloaders = Collections.synchronizedList(new ArrayList<>())
|
private static final List<Downloader> downloaders = Collections.synchronizedList(new ArrayList<>())
|
||||||
private static final Map<UUID,ResultsHolder> resultsListeners = new ConcurrentHashMap<>()
|
private static final Map<UUID,ResultsHolder> resultsListeners = new ConcurrentHashMap<>()
|
||||||
|
|
||||||
public static void main(String []args) {
|
public static void main(String []args) {
|
||||||
def home = System.getProperty("user.home") + File.separator + ".MuWire"
|
def home = System.getProperty("user.home") + File.separator + ".MuWire"
|
||||||
home = new File(home)
|
home = new File(home)
|
||||||
if (!home.exists())
|
if (!home.exists())
|
||||||
home.mkdirs()
|
home.mkdirs()
|
||||||
|
|
||||||
def propsFile = new File(home,"MuWire.properties")
|
def propsFile = new File(home,"MuWire.properties")
|
||||||
if (!propsFile.exists()) {
|
if (!propsFile.exists()) {
|
||||||
println "create props file ${propsFile.getAbsoluteFile()} before launching MuWire"
|
println "create props file ${propsFile.getAbsoluteFile()} before launching MuWire"
|
||||||
System.exit(1)
|
System.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
def props = new Properties()
|
def props = new Properties()
|
||||||
propsFile.withInputStream { props.load(it) }
|
propsFile.withInputStream { props.load(it) }
|
||||||
props = new MuWireSettings(props)
|
props = new MuWireSettings(props)
|
||||||
|
|
||||||
def filesList
|
def filesList
|
||||||
int connections
|
int connections
|
||||||
int resultWait
|
int resultWait
|
||||||
if (args.length != 3) {
|
if (args.length != 3) {
|
||||||
println "Enter a file containing list of hashes of files to download, " +
|
println "Enter a file containing list of hashes of files to download, " +
|
||||||
"how many connections you want before searching" +
|
"how many connections you want before searching" +
|
||||||
"and how long to wait for results to arrive"
|
"and how long to wait for results to arrive"
|
||||||
System.exit(1)
|
System.exit(1)
|
||||||
@@ -53,24 +53,24 @@ 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"
|
||||||
System.exit(1)
|
System.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def latch = new CountDownLatch(connections)
|
def latch = new CountDownLatch(connections)
|
||||||
def connectionListener = new ConnectionWaiter(latch : latch)
|
def connectionListener = new ConnectionWaiter(latch : latch)
|
||||||
core.eventBus.register(ConnectionEvent.class, connectionListener)
|
core.eventBus.register(ConnectionEvent.class, connectionListener)
|
||||||
|
|
||||||
core.startServices()
|
core.startServices()
|
||||||
println "starting to wait until there are $connections connections"
|
println "starting to wait until there are $connections connections"
|
||||||
latch.await()
|
latch.await()
|
||||||
|
|
||||||
println "connected, searching for files"
|
println "connected, searching for files"
|
||||||
|
|
||||||
def file = new File(filesList)
|
def file = new File(filesList)
|
||||||
file.eachLine {
|
file.eachLine {
|
||||||
String[] split = it.split(",")
|
String[] split = it.split(",")
|
||||||
@@ -79,22 +79,22 @@ class CliDownloader {
|
|||||||
def hash = Base64.decode(split[0])
|
def hash = Base64.decode(split[0])
|
||||||
def searchEvent = new SearchEvent(searchHash : hash, uuid : uuid)
|
def searchEvent = new SearchEvent(searchHash : hash, uuid : uuid)
|
||||||
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop:true,
|
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop:true,
|
||||||
replyTo: core.me.destination, receivedOn : core.me.destination, originator: core.me))
|
replyTo: core.me.destination, receivedOn : core.me.destination, originator: core.me))
|
||||||
}
|
}
|
||||||
|
|
||||||
println "waiting for results to arrive"
|
println "waiting for results to arrive"
|
||||||
Thread.sleep(resultWait * 1000)
|
Thread.sleep(resultWait * 1000)
|
||||||
|
|
||||||
core.eventBus.register(DownloadStartedEvent.class, new DownloadListener())
|
core.eventBus.register(DownloadStartedEvent.class, new DownloadListener())
|
||||||
resultsListeners.each { uuid, resultsListener ->
|
resultsListeners.each { uuid, resultsListener ->
|
||||||
println "starting download of $resultsListener.fileName from ${resultsListener.getResults().size()} hosts"
|
println "starting download of $resultsListener.fileName from ${resultsListener.getResults().size()} hosts"
|
||||||
File target = new File(resultsListener.fileName)
|
File target = new File(resultsListener.fileName)
|
||||||
|
|
||||||
core.eventBus.publish(new UIDownloadEvent(target : target, result : resultsListener.getResults()))
|
core.eventBus.publish(new UIDownloadEvent(target : target, result : resultsListener.getResults()))
|
||||||
}
|
}
|
||||||
|
|
||||||
Thread.sleep(1000)
|
Thread.sleep(1000)
|
||||||
|
|
||||||
Timer timer = new Timer("stats-printer")
|
Timer timer = new Timer("stats-printer")
|
||||||
timer.schedule({
|
timer.schedule({
|
||||||
println "==== STATUS UPDATE ==="
|
println "==== STATUS UPDATE ==="
|
||||||
@@ -109,7 +109,7 @@ class CliDownloader {
|
|||||||
}
|
}
|
||||||
println "==== END ==="
|
println "==== END ==="
|
||||||
} as TimerTask, 60000, 60000)
|
} as TimerTask, 60000, 60000)
|
||||||
|
|
||||||
println "waiting for downloads to finish"
|
println "waiting for downloads to finish"
|
||||||
while(true) {
|
while(true) {
|
||||||
boolean allFinished = true
|
boolean allFinished = true
|
||||||
@@ -120,10 +120,10 @@ class CliDownloader {
|
|||||||
break
|
break
|
||||||
Thread.sleep(1000)
|
Thread.sleep(1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
println "all downloads finished"
|
println "all downloads finished"
|
||||||
}
|
}
|
||||||
|
|
||||||
static class ResultsHolder {
|
static class ResultsHolder {
|
||||||
final List<UIResultEvent> results = Collections.synchronizedList(new ArrayList<>())
|
final List<UIResultEvent> results = Collections.synchronizedList(new ArrayList<>())
|
||||||
String fileName
|
String fileName
|
||||||
@@ -134,7 +134,7 @@ class CliDownloader {
|
|||||||
results
|
results
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static class ResultsListener {
|
static class ResultsListener {
|
||||||
UUID uuid
|
UUID uuid
|
||||||
String fileName
|
String fileName
|
||||||
@@ -148,7 +148,7 @@ class CliDownloader {
|
|||||||
listener.add(e)
|
listener.add(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static class ConnectionWaiter {
|
static class ConnectionWaiter {
|
||||||
CountDownLatch latch
|
CountDownLatch latch
|
||||||
public void onConnectionEvent(ConnectionEvent e) {
|
public void onConnectionEvent(ConnectionEvent e) {
|
||||||
@@ -156,7 +156,7 @@ class CliDownloader {
|
|||||||
latch.countDown()
|
latch.countDown()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static class DownloadListener {
|
static class DownloadListener {
|
||||||
public void onDownloadStartedEvent(DownloadStartedEvent e) {
|
public void onDownloadStartedEvent(DownloadStartedEvent e) {
|
||||||
|
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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'
|
||||||
|
15
core/src/main/groovy/com/muwire/core/Constants.groovy
Normal file
15
core/src/main/groovy/com/muwire/core/Constants.groovy
Normal 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 = "[\\.,_-]"
|
||||||
|
}
|
@@ -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,20 +51,16 @@ 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 {
|
||||||
|
|
||||||
final EventBus eventBus
|
final EventBus eventBus
|
||||||
final Persona me
|
final Persona me
|
||||||
final File home
|
final File home
|
||||||
final Properties i2pOptions
|
final Properties i2pOptions
|
||||||
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,19 +70,24 @@ 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")
|
||||||
if (i2pOptionsFile.exists()) {
|
if (i2pOptionsFile.exists()) {
|
||||||
@@ -113,70 +95,29 @@ public class Core {
|
|||||||
|
|
||||||
if (!i2pOptions.containsKey("inbound.nickname"))
|
if (!i2pOptions.containsKey("inbound.nickname"))
|
||||||
i2pOptions["inbound.nickname"] = "MuWire"
|
i2pOptions["inbound.nickname"] = "MuWire"
|
||||||
if (!i2pOptions.containsKey("outbound.nickname"))
|
if (!i2pOptions.containsKey("outbound.nickname"))
|
||||||
i2pOptions["outbound.nickname"] = "MuWire"
|
i2pOptions["outbound.nickname"] = "MuWire"
|
||||||
} else {
|
} else {
|
||||||
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
|
||||||
keyDat.withInputStream {
|
keyDat.withInputStream {
|
||||||
socketManager = new I2PSocketManagerFactory().createManager(it, i2pOptions["i2cp.tcp.host"], i2pOptions["i2cp.tcp.port"].toInteger(), i2pOptions)
|
socketManager = new I2PSocketManagerFactory().createManager(it, i2pOptions["i2cp.tcp.host"], i2pOptions["i2cp.tcp.port"].toInteger(), i2pOptions)
|
||||||
}
|
}
|
||||||
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()
|
||||||
def spk = new SigningPrivateKey(Constants.SIG_TYPE)
|
def spk = new SigningPrivateKey(Constants.SIG_TYPE)
|
||||||
keyDat.withInputStream {
|
keyDat.withInputStream {
|
||||||
@@ -184,8 +125,8 @@ public class Core {
|
|||||||
def privateKey = new PrivateKey()
|
def privateKey = new PrivateKey()
|
||||||
privateKey.readBytes(it)
|
privateKey.readBytes(it)
|
||||||
spk.readBytes(it)
|
spk.readBytes(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
def baos = new ByteArrayOutputStream()
|
def baos = new ByteArrayOutputStream()
|
||||||
def daos = new DataOutputStream(baos)
|
def daos = new DataOutputStream(baos)
|
||||||
daos.write(Constants.PERSONA_VERSION)
|
daos.write(Constants.PERSONA_VERSION)
|
||||||
@@ -202,121 +143,87 @@ public class Core {
|
|||||||
me = new Persona(new ByteArrayInputStream(baos.toByteArray()))
|
me = new Persona(new ByteArrayInputStream(baos.toByteArray()))
|
||||||
log.info("Loaded myself as "+me.getHumanReadableName())
|
log.info("Loaded myself as "+me.getHumanReadableName())
|
||||||
|
|
||||||
eventBus = new EventBus()
|
eventBus = new EventBus()
|
||||||
|
|
||||||
log.info("initializing trust service")
|
log.info("initializing trust service")
|
||||||
File goodTrust = new File(home, "trusted")
|
File goodTrust = new File(home, "trusted")
|
||||||
File badTrust = new File(home, "distrusted")
|
File badTrust = new File(home, "distrusted")
|
||||||
trustService = new TrustService(goodTrust, badTrust, 5000)
|
trustService = new TrustService(goodTrust, badTrust, 5000)
|
||||||
eventBus.register(TrustEvent.class, trustService)
|
eventBus.register(TrustEvent.class, trustService)
|
||||||
|
|
||||||
|
|
||||||
log.info "initializing file manager"
|
log.info "initializing file manager"
|
||||||
fileManager = 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 persistence service"
|
||||||
|
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 15000, fileManager)
|
||||||
log.info("initializing mesh manager")
|
|
||||||
MeshManager meshManager = new MeshManager(fileManager, home, props)
|
log.info("initializing host cache")
|
||||||
eventBus.register(SourceDiscoveredEvent.class, meshManager)
|
File hostStorage = new File(home, "hosts.json")
|
||||||
|
|
||||||
log.info "initializing persistence service"
|
|
||||||
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 60000, fileManager)
|
|
||||||
eventBus.register(UILoadedEvent.class, persisterService)
|
|
||||||
eventBus.register(UIPersistFilesEvent.class, persisterService)
|
|
||||||
|
|
||||||
log.info("initializing host cache")
|
|
||||||
File hostStorage = new File(home, "hosts.json")
|
|
||||||
hostCache = new HostCache(trustService,hostStorage, 30000, props, i2pSession.getMyDestination())
|
hostCache = new HostCache(trustService,hostStorage, 30000, props, i2pSession.getMyDestination())
|
||||||
eventBus.register(HostDiscoveredEvent.class, hostCache)
|
eventBus.register(HostDiscoveredEvent.class, hostCache)
|
||||||
eventBus.register(ConnectionEvent.class, hostCache)
|
eventBus.register(ConnectionEvent.class, hostCache)
|
||||||
|
|
||||||
log.info("initializing connection manager")
|
log.info("initializing connection manager")
|
||||||
connectionManager = props.isLeaf() ?
|
connectionManager = props.isLeaf() ?
|
||||||
new LeafConnectionManager(eventBus, me, 3, hostCache, props) :
|
new LeafConnectionManager(eventBus, me, 3, hostCache, props) :
|
||||||
new UltrapeerConnectionManager(eventBus, me, 512, 512, hostCache, trustService, props)
|
new UltrapeerConnectionManager(eventBus, me, 512, 512, hostCache, trustService, props)
|
||||||
eventBus.register(TrustEvent.class, connectionManager)
|
eventBus.register(TrustEvent.class, connectionManager)
|
||||||
eventBus.register(ConnectionEvent.class, connectionManager)
|
eventBus.register(ConnectionEvent.class, connectionManager)
|
||||||
eventBus.register(DisconnectionEvent.class, connectionManager)
|
eventBus.register(DisconnectionEvent.class, connectionManager)
|
||||||
eventBus.register(QueryEvent.class, connectionManager)
|
eventBus.register(QueryEvent.class, connectionManager)
|
||||||
|
|
||||||
log.info("initializing cache client")
|
log.info("initializing cache client")
|
||||||
cacheClient = new CacheClient(eventBus,hostCache, connectionManager, i2pSession, props, 10000)
|
cacheClient = new CacheClient(eventBus,hostCache, connectionManager, i2pSession, props, 10000)
|
||||||
|
|
||||||
log.info("initializing update client")
|
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")
|
||||||
|
I2PConnector i2pConnector = new I2PConnector(socketManager)
|
||||||
log.info("initializing connector")
|
|
||||||
I2PConnector i2pConnector = new I2PConnector(socketManager)
|
log.info "initializing results sender"
|
||||||
|
ResultsSender resultsSender = new ResultsSender(eventBus, i2pConnector, me)
|
||||||
log.info "initializing results sender"
|
|
||||||
ResultsSender resultsSender = new ResultsSender(eventBus, i2pConnector, me, props)
|
log.info "initializing search manager"
|
||||||
|
SearchManager searchManager = new SearchManager(eventBus, me, resultsSender)
|
||||||
log.info "initializing search manager"
|
eventBus.register(QueryEvent.class, searchManager)
|
||||||
SearchManager searchManager = new SearchManager(eventBus, me, resultsSender)
|
eventBus.register(ResultsEvent.class, searchManager)
|
||||||
eventBus.register(QueryEvent.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)
|
||||||
|
|
||||||
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()
|
||||||
@@ -325,30 +232,16 @@ public class Core {
|
|||||||
hostCache.waitForLoad()
|
hostCache.waitForLoad()
|
||||||
updateClient.start()
|
updateClient.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
@@ -358,7 +251,7 @@ public class Core {
|
|||||||
log.info("creating home dir")
|
log.info("creating home dir")
|
||||||
home.mkdir()
|
home.mkdir()
|
||||||
}
|
}
|
||||||
|
|
||||||
def props = new Properties()
|
def props = new Properties()
|
||||||
def propsFile = new File(home, "MuWire.properties")
|
def propsFile = new File(home, "MuWire.properties")
|
||||||
if (propsFile.exists()) {
|
if (propsFile.exists()) {
|
||||||
@@ -374,10 +267,10 @@ public class Core {
|
|||||||
props.write(it)
|
props.write(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
if (args.length == 0) {
|
if (args.length == 0) {
|
||||||
log.info("initialized everything, sleeping")
|
log.info("initialized everything, sleeping")
|
||||||
|
@@ -4,17 +4,17 @@ import java.util.concurrent.atomic.AtomicLong
|
|||||||
|
|
||||||
class Event {
|
class Event {
|
||||||
|
|
||||||
private static final AtomicLong SEQ_NO = new AtomicLong();
|
private static final AtomicLong SEQ_NO = new AtomicLong();
|
||||||
final long seqNo
|
final long seqNo
|
||||||
final long timestamp
|
final long timestamp
|
||||||
|
|
||||||
Event() {
|
Event() {
|
||||||
seqNo = SEQ_NO.getAndIncrement()
|
seqNo = SEQ_NO.getAndIncrement()
|
||||||
timestamp = System.currentTimeMillis()
|
timestamp = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
"seqNo $seqNo timestamp $timestamp"
|
"seqNo $seqNo timestamp $timestamp"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -10,47 +10,42 @@ import com.muwire.core.files.FileSharedEvent
|
|||||||
import groovy.util.logging.Log
|
import groovy.util.logging.Log
|
||||||
@Log
|
@Log
|
||||||
class EventBus {
|
class EventBus {
|
||||||
|
|
||||||
|
private Map handlers = new HashMap()
|
||||||
|
private final Executor executor = Executors.newSingleThreadExecutor {r ->
|
||||||
|
def rv = new Thread(r)
|
||||||
|
rv.setDaemon(true)
|
||||||
|
rv.setName("event-bus")
|
||||||
|
rv
|
||||||
|
}
|
||||||
|
|
||||||
private Map handlers = new HashMap()
|
void publish(Event e) {
|
||||||
private final Executor executor = Executors.newSingleThreadExecutor {r ->
|
executor.execute({publishInternal(e)} as Runnable)
|
||||||
def rv = new Thread(r)
|
}
|
||||||
rv.setDaemon(true)
|
|
||||||
rv.setName("event-bus")
|
private void publishInternal(Event e) {
|
||||||
rv
|
log.fine "publishing event $e of type ${e.getClass().getSimpleName()} event $e"
|
||||||
}
|
def currentHandlers
|
||||||
|
final def clazz = e.getClass()
|
||||||
void publish(Event e) {
|
synchronized(this) {
|
||||||
executor.execute({publishInternal(e)} as Runnable)
|
currentHandlers = handlers.getOrDefault(clazz, [])
|
||||||
}
|
}
|
||||||
|
currentHandlers.each {
|
||||||
private void publishInternal(Event e) {
|
|
||||||
log.fine "publishing event $e of type ${e.getClass().getSimpleName()} event $e"
|
|
||||||
def currentHandlers
|
|
||||||
final def clazz = e.getClass()
|
|
||||||
synchronized(this) {
|
|
||||||
currentHandlers = handlers.getOrDefault(clazz, [])
|
|
||||||
}
|
|
||||||
currentHandlers.each {
|
|
||||||
try {
|
try {
|
||||||
it."on${clazz.getSimpleName()}"(e)
|
it."on${clazz.getSimpleName()}"(e)
|
||||||
} catch (Exception bad) {
|
} catch (Exception bad) {
|
||||||
log.log(Level.SEVERE, "exception dispatching event",bad)
|
log.log(Level.SEVERE, "exception dispatching event",bad)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized void register(Class<? extends Event> eventType, def handler) {
|
synchronized void register(Class<? extends Event> eventType, def handler) {
|
||||||
log.info "Registering $handler for type $eventType"
|
log.info "Registering $handler for type $eventType"
|
||||||
def currentHandlers = handlers.get(eventType)
|
def currentHandlers = handlers.get(eventType)
|
||||||
if (currentHandlers == null) {
|
if (currentHandlers == null) {
|
||||||
currentHandlers = new CopyOnWriteArrayList()
|
currentHandlers = new CopyOnWriteArrayList()
|
||||||
handlers.put(eventType, currentHandlers)
|
handlers.put(eventType, currentHandlers)
|
||||||
}
|
}
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -13,5 +13,5 @@ class InvalidSignatureException extends Exception {
|
|||||||
public InvalidSignatureException(Throwable cause) {
|
public InvalidSignatureException(Throwable cause) {
|
||||||
super(cause);
|
super(cause);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,174 +1,74 @@
|
|||||||
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
|
MuWireSettings() {
|
||||||
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() {
|
|
||||||
this(new Properties())
|
this(new Properties())
|
||||||
}
|
}
|
||||||
|
|
||||||
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"))
|
crawlerResponse = CrawlerResponse.valueOf(props.get("crawlerResponse","REGISTERED"))
|
||||||
allowTrustLists = Boolean.valueOf(props.getProperty("allowTrustLists","true"))
|
|
||||||
trustListInterval = Integer.valueOf(props.getProperty("trustListInterval","1"))
|
|
||||||
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, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean isLeaf() {
|
||||||
|
isLeaf
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean allowUntrusted() {
|
||||||
|
allowUntrusted
|
||||||
|
}
|
||||||
|
|
||||||
|
void setAllowUntrusted(boolean allowUntrusted) {
|
||||||
|
this.allowUntrusted = allowUntrusted
|
||||||
|
}
|
||||||
|
|
||||||
|
CrawlerResponse getCrawlerResponse() {
|
||||||
|
crawlerResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
void setCrawlerResponse(CrawlerResponse crawlerResponse) {
|
||||||
|
this.crawlerResponse = crawlerResponse
|
||||||
|
}
|
||||||
|
|
||||||
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() {
|
|
||||||
isLeaf
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean allowUntrusted() {
|
|
||||||
allowUntrusted
|
|
||||||
}
|
|
||||||
|
|
||||||
void setAllowUntrusted(boolean allowUntrusted) {
|
|
||||||
this.allowUntrusted = allowUntrusted
|
|
||||||
}
|
|
||||||
|
|
||||||
CrawlerResponse getCrawlerResponse() {
|
|
||||||
crawlerResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
void setCrawlerResponse(CrawlerResponse crawlerResponse) {
|
|
||||||
this.crawlerResponse = crawlerResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
String getNickname() {
|
String getNickname() {
|
||||||
nickname
|
nickname
|
||||||
}
|
}
|
||||||
|
@@ -7,11 +7,11 @@ import java.nio.charset.StandardCharsets
|
|||||||
*/
|
*/
|
||||||
public class Name {
|
public class Name {
|
||||||
final String name
|
final String name
|
||||||
|
|
||||||
Name(String name) {
|
Name(String name) {
|
||||||
this.name = name
|
this.name = name
|
||||||
}
|
}
|
||||||
|
|
||||||
Name(InputStream nameStream) throws IOException {
|
Name(InputStream nameStream) throws IOException {
|
||||||
DataInputStream dis = new DataInputStream(nameStream)
|
DataInputStream dis = new DataInputStream(nameStream)
|
||||||
int length = dis.readUnsignedShort()
|
int length = dis.readUnsignedShort()
|
||||||
@@ -19,22 +19,22 @@ public class Name {
|
|||||||
dis.readFully(nameBytes)
|
dis.readFully(nameBytes)
|
||||||
this.name = new String(nameBytes, StandardCharsets.UTF_8)
|
this.name = new String(nameBytes, StandardCharsets.UTF_8)
|
||||||
}
|
}
|
||||||
|
|
||||||
public void write(OutputStream out) throws IOException {
|
public void write(OutputStream out) throws IOException {
|
||||||
DataOutputStream dos = new DataOutputStream(out)
|
DataOutputStream dos = new DataOutputStream(out)
|
||||||
dos.writeShort(name.length())
|
dos.writeShort(name.length())
|
||||||
dos.write(name.getBytes(StandardCharsets.UTF_8))
|
dos.write(name.getBytes(StandardCharsets.UTF_8))
|
||||||
}
|
}
|
||||||
|
|
||||||
public getName() {
|
public getName() {
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
name.hashCode()
|
name.hashCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
if (!(o instanceof Name))
|
if (!(o instanceof Name))
|
||||||
|
@@ -9,7 +9,7 @@ import net.i2p.data.SigningPublicKey
|
|||||||
|
|
||||||
public class Persona {
|
public class Persona {
|
||||||
private static final int SIG_LEN = Constants.SIG_TYPE.getSigLen()
|
private static final int SIG_LEN = Constants.SIG_TYPE.getSigLen()
|
||||||
|
|
||||||
private final byte version
|
private final byte version
|
||||||
private final Name name
|
private final Name name
|
||||||
private final Destination destination
|
private final Destination destination
|
||||||
@@ -17,12 +17,12 @@ public class Persona {
|
|||||||
private volatile String humanReadableName
|
private volatile String humanReadableName
|
||||||
private volatile String base64
|
private volatile String base64
|
||||||
private volatile byte[] payload
|
private volatile byte[] payload
|
||||||
|
|
||||||
public Persona(InputStream personaStream) throws IOException, InvalidSignatureException {
|
public Persona(InputStream personaStream) throws IOException, InvalidSignatureException {
|
||||||
version = (byte) (personaStream.read() & 0xFF)
|
version = (byte) (personaStream.read() & 0xFF)
|
||||||
if (version != Constants.PERSONA_VERSION)
|
if (version != Constants.PERSONA_VERSION)
|
||||||
throw new IOException("Unknown version "+version)
|
throw new IOException("Unknown version "+version)
|
||||||
|
|
||||||
name = new Name(personaStream)
|
name = new Name(personaStream)
|
||||||
destination = Destination.create(personaStream)
|
destination = Destination.create(personaStream)
|
||||||
sig = new byte[SIG_LEN]
|
sig = new byte[SIG_LEN]
|
||||||
@@ -31,7 +31,7 @@ public class Persona {
|
|||||||
if (!verify(version, name, destination, sig))
|
if (!verify(version, name, destination, sig))
|
||||||
throw new InvalidSignatureException(getHumanReadableName() + " didn't verify")
|
throw new InvalidSignatureException(getHumanReadableName() + " didn't verify")
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean verify(byte version, Name name, Destination destination, byte [] sig) {
|
private static boolean verify(byte version, Name name, Destination destination, byte [] sig) {
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
||||||
baos.write(version)
|
baos.write(version)
|
||||||
@@ -42,7 +42,7 @@ public class Persona {
|
|||||||
Signature signature = new Signature(Constants.SIG_TYPE, sig)
|
Signature signature = new Signature(Constants.SIG_TYPE, sig)
|
||||||
DSAEngine.getInstance().verifySignature(signature, payload, spk)
|
DSAEngine.getInstance().verifySignature(signature, payload, spk)
|
||||||
}
|
}
|
||||||
|
|
||||||
public void write(OutputStream out) throws IOException {
|
public void write(OutputStream out) throws IOException {
|
||||||
if (payload == null) {
|
if (payload == null) {
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
||||||
@@ -54,13 +54,13 @@ public class Persona {
|
|||||||
}
|
}
|
||||||
out.write(payload)
|
out.write(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getHumanReadableName() {
|
public String getHumanReadableName() {
|
||||||
if (humanReadableName == null)
|
if (humanReadableName == null)
|
||||||
humanReadableName = name.getName() + "@" + destination.toBase32().substring(0,32)
|
humanReadableName = name.getName() + "@" + destination.toBase32().substring(0,32)
|
||||||
humanReadableName
|
humanReadableName
|
||||||
}
|
}
|
||||||
|
|
||||||
public String toBase64() {
|
public String toBase64() {
|
||||||
if (base64 == null) {
|
if (base64 == null) {
|
||||||
def baos = new ByteArrayOutputStream()
|
def baos = new ByteArrayOutputStream()
|
||||||
@@ -69,12 +69,12 @@ public class Persona {
|
|||||||
}
|
}
|
||||||
base64
|
base64
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
name.hashCode() ^ destination.hashCode()
|
name.hashCode() ^ destination.hashCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
if (!(o instanceof Persona))
|
if (!(o instanceof Persona))
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +0,0 @@
|
|||||||
package com.muwire.core
|
|
||||||
|
|
||||||
class RouterDisconnectedEvent extends Event {
|
|
||||||
}
|
|
@@ -2,12 +2,12 @@ package com.muwire.core
|
|||||||
|
|
||||||
abstract class Service {
|
abstract class Service {
|
||||||
|
|
||||||
volatile boolean loaded
|
volatile boolean loaded
|
||||||
|
|
||||||
abstract void load()
|
abstract void load()
|
||||||
|
|
||||||
void waitForLoad() {
|
void waitForLoad() {
|
||||||
while (!loaded)
|
while (!loaded)
|
||||||
Thread.sleep(10)
|
Thread.sleep(10)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +0,0 @@
|
|||||||
package com.muwire.core
|
|
||||||
|
|
||||||
class SplitPattern {
|
|
||||||
|
|
||||||
public static final String SPLIT_PATTERN = "[\\*\\+\\-,\\.:;\\(\\)=_/\\\\\\!\\\"\\\'\\\$%\\|\\[\\]\\{\\}\\?]";
|
|
||||||
|
|
||||||
}
|
|
@@ -22,108 +22,104 @@ import net.i2p.data.Destination
|
|||||||
@Log
|
@Log
|
||||||
abstract class Connection implements Closeable {
|
abstract class Connection implements Closeable {
|
||||||
|
|
||||||
private static final int SEARCHES = 10
|
final EventBus eventBus
|
||||||
private static final long INTERVAL = 1000
|
final Endpoint endpoint
|
||||||
|
final boolean incoming
|
||||||
final EventBus eventBus
|
final HostCache hostCache
|
||||||
final Endpoint endpoint
|
|
||||||
final boolean incoming
|
|
||||||
final HostCache hostCache
|
|
||||||
final TrustService trustService
|
final TrustService trustService
|
||||||
final MuWireSettings settings
|
final MuWireSettings settings
|
||||||
|
|
||||||
private final AtomicBoolean running = new AtomicBoolean()
|
private final AtomicBoolean running = new AtomicBoolean()
|
||||||
private final BlockingQueue messages = new LinkedBlockingQueue()
|
private final BlockingQueue messages = new LinkedBlockingQueue()
|
||||||
private final Thread reader, writer
|
private final Thread reader, writer
|
||||||
private final LinkedList<Long> searchTimestamps = new LinkedList<>()
|
|
||||||
|
protected final String name
|
||||||
protected final String name
|
|
||||||
|
long lastPingSentTime, lastPongReceivedTime
|
||||||
long lastPingSentTime, lastPongReceivedTime
|
|
||||||
|
Connection(EventBus eventBus, Endpoint endpoint, boolean incoming,
|
||||||
Connection(EventBus eventBus, Endpoint endpoint, boolean incoming,
|
|
||||||
HostCache hostCache, TrustService trustService, MuWireSettings settings) {
|
HostCache hostCache, TrustService trustService, MuWireSettings settings) {
|
||||||
this.eventBus = eventBus
|
this.eventBus = eventBus
|
||||||
this.incoming = incoming
|
this.incoming = incoming
|
||||||
this.endpoint = endpoint
|
this.endpoint = endpoint
|
||||||
this.hostCache = hostCache
|
this.hostCache = hostCache
|
||||||
this.trustService = trustService
|
this.trustService = trustService
|
||||||
this.settings = settings
|
this.settings = settings
|
||||||
|
|
||||||
this.name = endpoint.destination.toBase32().substring(0,8)
|
this.name = endpoint.destination.toBase32().substring(0,8)
|
||||||
|
|
||||||
this.reader = new Thread({readLoop()} as Runnable)
|
this.reader = new Thread({readLoop()} as Runnable)
|
||||||
this.reader.setName("reader-$name")
|
this.reader.setName("reader-$name")
|
||||||
this.reader.setDaemon(true)
|
this.reader.setDaemon(true)
|
||||||
|
|
||||||
this.writer = new Thread({writeLoop()} as Runnable)
|
this.writer = new Thread({writeLoop()} as Runnable)
|
||||||
this.writer.setName("writer-$name")
|
this.writer.setName("writer-$name")
|
||||||
this.writer.setDaemon(true)
|
this.writer.setDaemon(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* starts the connection threads
|
* starts the connection threads
|
||||||
*/
|
*/
|
||||||
void start() {
|
void start() {
|
||||||
if (!running.compareAndSet(false, true)) {
|
if (!running.compareAndSet(false, true)) {
|
||||||
log.log(Level.WARNING,"$name already running", new Exception())
|
log.log(Level.WARNING,"$name already running", new Exception())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
reader.start()
|
reader.start()
|
||||||
writer.start()
|
writer.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
if (!running.compareAndSet(true, false)) {
|
if (!running.compareAndSet(true, false)) {
|
||||||
log.log(Level.WARNING, "$name already closed", new Exception() )
|
log.log(Level.WARNING, "$name already closed", new Exception() )
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.info("closing $name")
|
log.info("closing $name")
|
||||||
reader.interrupt()
|
reader.interrupt()
|
||||||
writer.interrupt()
|
writer.interrupt()
|
||||||
endpoint.close()
|
endpoint.close()
|
||||||
eventBus.publish(new DisconnectionEvent(destination: endpoint.destination))
|
eventBus.publish(new DisconnectionEvent(destination: endpoint.destination))
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void readLoop() {
|
protected void readLoop() {
|
||||||
try {
|
try {
|
||||||
while(running.get()) {
|
while(running.get()) {
|
||||||
read()
|
read()
|
||||||
}
|
}
|
||||||
} catch (SocketTimeoutException e) {
|
} catch (SocketTimeoutException e) {
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.log(Level.WARNING,"unhandled exception in reader",e)
|
log.log(Level.WARNING,"unhandled exception in reader",e)
|
||||||
} finally {
|
} finally {
|
||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract void read()
|
protected abstract void read()
|
||||||
|
|
||||||
protected void writeLoop() {
|
protected void writeLoop() {
|
||||||
try {
|
try {
|
||||||
while(running.get()) {
|
while(running.get()) {
|
||||||
def message = messages.take()
|
def message = messages.take()
|
||||||
write(message)
|
write(message)
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.log(Level.WARNING, "unhandled exception in writer",e)
|
log.log(Level.WARNING, "unhandled exception in writer",e)
|
||||||
} finally {
|
} finally {
|
||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract void write(def message);
|
protected abstract void write(def message);
|
||||||
|
|
||||||
void sendPing() {
|
void sendPing() {
|
||||||
def ping = [:]
|
def ping = [:]
|
||||||
ping.type = "Ping"
|
ping.type = "Ping"
|
||||||
ping.version = 1
|
ping.version = 1
|
||||||
messages.put(ping)
|
messages.put(ping)
|
||||||
lastPingSentTime = System.currentTimeMillis()
|
lastPingSentTime = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
|
|
||||||
void sendQuery(QueryEvent e) {
|
void sendQuery(QueryEvent e) {
|
||||||
def query = [:]
|
def query = [:]
|
||||||
query.type = "Search"
|
query.type = "Search"
|
||||||
@@ -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()
|
||||||
@@ -141,53 +135,35 @@ abstract class Connection implements Closeable {
|
|||||||
query.originator = e.originator.toBase64()
|
query.originator = e.originator.toBase64()
|
||||||
messages.put(query)
|
messages.put(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void handlePing() {
|
protected void handlePing() {
|
||||||
log.fine("$name received ping")
|
log.fine("$name received ping")
|
||||||
def pong = [:]
|
def pong = [:]
|
||||||
pong.type = "Pong"
|
pong.type = "Pong"
|
||||||
pong.version = 1
|
pong.version = 1
|
||||||
pong.pongs = hostCache.getGoodHosts(10).collect { d -> d.toBase64() }
|
pong.pongs = hostCache.getGoodHosts(10).collect { d -> d.toBase64() }
|
||||||
messages.put(pong)
|
messages.put(pong)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void handlePong(def pong) {
|
protected void handlePong(def pong) {
|
||||||
log.fine("$name received pong")
|
log.fine("$name received pong")
|
||||||
lastPongReceivedTime = System.currentTimeMillis()
|
lastPongReceivedTime = System.currentTimeMillis()
|
||||||
if (pong.pongs == null)
|
if (pong.pongs == null)
|
||||||
throw new Exception("Pong doesn't have pongs")
|
throw new Exception("Pong doesn't have pongs")
|
||||||
pong.pongs.each {
|
pong.pongs.each {
|
||||||
def dest = new Destination(it)
|
def dest = new Destination(it)
|
||||||
eventBus.publish(new HostDiscoveredEvent(destination: dest))
|
eventBus.publish(new HostDiscoveredEvent(destination: dest))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
search.keywords = null
|
search.keywords = null
|
||||||
infohash = Base64.decode(search.infohash)
|
infohash = Base64.decode(search.infohash)
|
||||||
}
|
}
|
||||||
|
|
||||||
Destination replyTo = new Destination(search.replyTo)
|
Destination replyTo = new Destination(search.replyTo)
|
||||||
TrustLevel trustLevel = trustService.getLevel(replyTo)
|
TrustLevel trustLevel = trustService.getLevel(replyTo)
|
||||||
if (trustLevel == TrustLevel.DISTRUSTED) {
|
if (trustLevel == TrustLevel.DISTRUSTED) {
|
||||||
@@ -198,7 +174,7 @@ abstract class Connection implements Closeable {
|
|||||||
log.info("dropping search from neutral peer")
|
log.info("dropping search from neutral peer")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Persona originator = null
|
Persona originator = null
|
||||||
if (search.originator != null) {
|
if (search.originator != null) {
|
||||||
originator = new Persona(new ByteArrayInputStream(Base64.decode(search.originator)))
|
originator = new Persona(new ByteArrayInputStream(Base64.decode(search.originator)))
|
||||||
@@ -207,29 +183,21 @@ abstract class Connection implements Closeable {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
receivedOn : endpoint.destination,
|
receivedOn : endpoint.destination,
|
||||||
firstHop : search.firstHop )
|
firstHop : search.firstHop )
|
||||||
eventBus.publish(event)
|
eventBus.publish(event)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,214 +5,194 @@ 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 {
|
||||||
|
|
||||||
final EventBus eventBus
|
final EventBus eventBus
|
||||||
final UltrapeerConnectionManager manager
|
final UltrapeerConnectionManager manager
|
||||||
final MuWireSettings settings
|
final MuWireSettings settings
|
||||||
final I2PAcceptor acceptor
|
final I2PAcceptor acceptor
|
||||||
final HostCache hostCache
|
final HostCache hostCache
|
||||||
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
|
||||||
final ExecutorService handshakerThreads
|
final ExecutorService handshakerThreads
|
||||||
|
|
||||||
private volatile shutdown
|
private volatile shutdown
|
||||||
|
|
||||||
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
|
||||||
this.acceptor = acceptor
|
this.acceptor = acceptor
|
||||||
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
|
||||||
|
|
||||||
acceptorThread = Executors.newSingleThreadExecutor { r ->
|
acceptorThread = Executors.newSingleThreadExecutor { r ->
|
||||||
def rv = new Thread(r)
|
def rv = new Thread(r)
|
||||||
rv.setDaemon(true)
|
rv.setDaemon(true)
|
||||||
rv.setName("acceptor")
|
rv.setName("acceptor")
|
||||||
rv
|
rv
|
||||||
}
|
}
|
||||||
|
|
||||||
handshakerThreads = Executors.newCachedThreadPool { r ->
|
handshakerThreads = Executors.newCachedThreadPool { r ->
|
||||||
def rv = new Thread(r)
|
def rv = new Thread(r)
|
||||||
rv.setDaemon(true)
|
rv.setDaemon(true)
|
||||||
rv.setName("acceptor-processor-${System.currentTimeMillis()}")
|
rv.setName("acceptor-processor-${System.currentTimeMillis()}")
|
||||||
rv
|
rv
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void start() {
|
void start() {
|
||||||
acceptorThread.execute({acceptLoop()} as Runnable)
|
acceptorThread.execute({acceptLoop()} as Runnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
void stop() {
|
void stop() {
|
||||||
shutdown = true
|
shutdown = true
|
||||||
acceptorThread.shutdownNow()
|
acceptorThread.shutdownNow()
|
||||||
handshakerThreads.shutdownNow()
|
handshakerThreads.shutdownNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
private void acceptLoop() {
|
private void acceptLoop() {
|
||||||
try {
|
try {
|
||||||
while(true) {
|
while(true) {
|
||||||
def incoming = acceptor.accept()
|
def incoming = acceptor.accept()
|
||||||
log.info("accepted connection from ${incoming.destination.toBase32()}")
|
log.info("accepted connection from ${incoming.destination.toBase32()}")
|
||||||
switch(trustService.getLevel(incoming.destination)) {
|
switch(trustService.getLevel(incoming.destination)) {
|
||||||
case TrustLevel.TRUSTED : break
|
case TrustLevel.TRUSTED : break
|
||||||
case TrustLevel.NEUTRAL :
|
case TrustLevel.NEUTRAL :
|
||||||
if (settings.allowUntrusted())
|
if (settings.allowUntrusted())
|
||||||
break
|
break
|
||||||
case TrustLevel.DISTRUSTED :
|
case TrustLevel.DISTRUSTED :
|
||||||
log.info("Disallowing distrusted connection")
|
log.info("Disallowing distrusted connection")
|
||||||
incoming.close()
|
incoming.close()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
handshakerThreads.execute({processIncoming(incoming)} as Runnable)
|
handshakerThreads.execute({processIncoming(incoming)} as Runnable)
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.log(Level.WARNING, "exception in accept loop",e)
|
log.log(Level.WARNING, "exception in accept loop",e)
|
||||||
if (!shutdown)
|
if (!shutdown)
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processIncoming(Endpoint e) {
|
private void processIncoming(Endpoint e) {
|
||||||
InputStream is = e.inputStream
|
InputStream is = e.inputStream
|
||||||
try {
|
try {
|
||||||
int read = is.read()
|
int read = is.read()
|
||||||
switch(read) {
|
switch(read) {
|
||||||
case (byte)'M':
|
case (byte)'M':
|
||||||
if (settings.isLeaf())
|
if (settings.isLeaf())
|
||||||
throw new IOException("Incoming connection as leaf")
|
throw new IOException("Incoming connection as leaf")
|
||||||
processMuWire(e)
|
processMuWire(e)
|
||||||
break
|
break
|
||||||
case (byte)'G':
|
case (byte)'G':
|
||||||
processGET(e)
|
processGET(e)
|
||||||
break
|
break
|
||||||
case (byte)'H':
|
case (byte)'H':
|
||||||
processHashList(e)
|
processHashList(e)
|
||||||
break
|
break
|
||||||
case (byte)'P':
|
case (byte)'P':
|
||||||
processPOST(e)
|
processPOST(e)
|
||||||
break
|
break
|
||||||
case (byte)'R':
|
default:
|
||||||
processRESULTS(e)
|
throw new Exception("Invalid read $read")
|
||||||
break
|
}
|
||||||
case (byte)'T':
|
} catch (Exception ex) {
|
||||||
processTRUST(e)
|
log.log(Level.WARNING, "incoming connection failed",ex)
|
||||||
break
|
e.close()
|
||||||
case (byte)'B':
|
eventBus.publish new ConnectionEvent(endpoint: e, incoming: true, leaf: null, status: ConnectionAttemptStatus.FAILED)
|
||||||
processBROWSE(e)
|
}
|
||||||
break
|
}
|
||||||
default:
|
|
||||||
throw new Exception("Invalid read $read")
|
private void processMuWire(Endpoint e) {
|
||||||
}
|
byte[] uWire = "uWire ".bytes
|
||||||
} catch (Exception ex) {
|
for (int i = 0; i < uWire.length; i++) {
|
||||||
log.log(Level.WARNING, "incoming connection failed",ex)
|
int read = e.inputStream.read()
|
||||||
e.close()
|
if (read != uWire[i]) {
|
||||||
eventBus.publish new ConnectionEvent(endpoint: e, incoming: true, leaf: null, status: ConnectionAttemptStatus.FAILED)
|
throw new IOException("unexpected value $read at position $i")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processMuWire(Endpoint e) {
|
byte[] type = new byte[4]
|
||||||
byte[] uWire = "uWire ".bytes
|
DataInputStream dis = new DataInputStream(e.inputStream)
|
||||||
for (int i = 0; i < uWire.length; i++) {
|
dis.readFully(type)
|
||||||
int read = e.inputStream.read()
|
|
||||||
if (read != uWire[i]) {
|
|
||||||
throw new IOException("unexpected value $read at position $i")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] type = new byte[4]
|
|
||||||
DataInputStream dis = new DataInputStream(e.inputStream)
|
|
||||||
dis.readFully(type)
|
|
||||||
|
|
||||||
if (type == "leaf".bytes)
|
if (type == "leaf".bytes)
|
||||||
handleIncoming(e, true)
|
handleIncoming(e, true)
|
||||||
else if (type == "peer".bytes)
|
else if (type == "peer".bytes)
|
||||||
handleIncoming(e, false)
|
handleIncoming(e, false)
|
||||||
else
|
else
|
||||||
throw new IOException("unknown connection type $type")
|
throw new IOException("unknown connection type $type")
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleIncoming(Endpoint e, boolean leaf) {
|
private void handleIncoming(Endpoint e, boolean leaf) {
|
||||||
boolean accept = !manager.isConnected(e.destination) &&
|
boolean accept = !manager.isConnected(e.destination) &&
|
||||||
!establisher.isInProgress(e.destination) &&
|
!establisher.isInProgress(e.destination) &&
|
||||||
(leaf ? manager.hasLeafSlots() : manager.hasPeerSlots())
|
(leaf ? manager.hasLeafSlots() : manager.hasPeerSlots())
|
||||||
if (accept) {
|
if (accept) {
|
||||||
log.info("accepting connection, leaf:$leaf")
|
log.info("accepting connection, leaf:$leaf")
|
||||||
e.outputStream.write("OK".bytes)
|
e.outputStream.write("OK".bytes)
|
||||||
e.outputStream.flush()
|
e.outputStream.flush()
|
||||||
def wrapped = new Endpoint(e.destination, new InflaterInputStream(e.inputStream), new DeflaterOutputStream(e.outputStream, true), e.toClose)
|
def wrapped = new Endpoint(e.destination, new InflaterInputStream(e.inputStream), new DeflaterOutputStream(e.outputStream, true), e.toClose)
|
||||||
eventBus.publish(new ConnectionEvent(endpoint: wrapped, incoming: true, leaf: leaf, status: ConnectionAttemptStatus.SUCCESSFUL))
|
eventBus.publish(new ConnectionEvent(endpoint: wrapped, incoming: true, leaf: leaf, status: ConnectionAttemptStatus.SUCCESSFUL))
|
||||||
} else {
|
} else {
|
||||||
log.info("rejecting connection, leaf:$leaf")
|
log.info("rejecting connection, leaf:$leaf")
|
||||||
e.outputStream.write("REJECT".bytes)
|
e.outputStream.write("REJECT".bytes)
|
||||||
def hosts = hostCache.getGoodHosts(10)
|
def hosts = hostCache.getGoodHosts(10)
|
||||||
if (!hosts.isEmpty()) {
|
if (!hosts.isEmpty()) {
|
||||||
def json = [:]
|
def json = [:]
|
||||||
json.tryHosts = hosts.collect { d -> d.toBase64() }
|
json.tryHosts = hosts.collect { d -> d.toBase64() }
|
||||||
json = JsonOutput.toJson(json)
|
json = JsonOutput.toJson(json)
|
||||||
def os = new DataOutputStream(e.outputStream)
|
def os = new DataOutputStream(e.outputStream)
|
||||||
os.writeShort(json.bytes.length)
|
os.writeShort(json.bytes.length)
|
||||||
os.write(json.bytes)
|
os.write(json.bytes)
|
||||||
}
|
}
|
||||||
e.outputStream.flush()
|
e.outputStream.flush()
|
||||||
e.close()
|
e.close()
|
||||||
eventBus.publish(new ConnectionEvent(endpoint: e, incoming: true, leaf: leaf, status: ConnectionAttemptStatus.REJECTED))
|
eventBus.publish(new ConnectionEvent(endpoint: e, incoming: true, leaf: leaf, status: ConnectionAttemptStatus.REJECTED))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private void processGET(Endpoint e) {
|
private void processGET(Endpoint e) {
|
||||||
byte[] et = new byte[3]
|
byte[] et = new byte[3]
|
||||||
final DataInputStream dis = new DataInputStream(e.getInputStream())
|
final DataInputStream dis = new DataInputStream(e.getInputStream())
|
||||||
dis.readFully(et)
|
dis.readFully(et)
|
||||||
if (et != "ET ".getBytes(StandardCharsets.US_ASCII))
|
if (et != "ET ".getBytes(StandardCharsets.US_ASCII))
|
||||||
throw new IOException("Invalid GET connection")
|
throw new IOException("Invalid GET connection")
|
||||||
uploadManager.processGET(e)
|
uploadManager.processGET(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processHashList(Endpoint e) {
|
private void processHashList(Endpoint e) {
|
||||||
byte[] ashList = new byte[8]
|
byte[] ashList = new byte[8]
|
||||||
final DataInputStream dis = new DataInputStream(e.getInputStream())
|
final DataInputStream dis = new DataInputStream(e.getInputStream())
|
||||||
@@ -221,7 +201,7 @@ class ConnectionAcceptor {
|
|||||||
throw new IOException("Invalid HASHLIST connection")
|
throw new IOException("Invalid HASHLIST connection")
|
||||||
uploadManager.processHashList(e)
|
uploadManager.processHashList(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processPOST(final Endpoint e) throws IOException {
|
private void processPOST(final Endpoint e) throws IOException {
|
||||||
byte [] ost = new byte[4]
|
byte [] ost = new byte[4]
|
||||||
final DataInputStream dis = new DataInputStream(e.getInputStream())
|
final DataInputStream dis = new DataInputStream(e.getInputStream())
|
||||||
@@ -243,164 +223,20 @@ 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 {
|
||||||
e.close()
|
e.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -21,169 +21,164 @@ import net.i2p.util.ConcurrentHashSet
|
|||||||
|
|
||||||
@Log
|
@Log
|
||||||
class ConnectionEstablisher {
|
class ConnectionEstablisher {
|
||||||
|
|
||||||
|
private static final int CONCURRENT = 4
|
||||||
|
|
||||||
private static final int CONCURRENT = 4
|
final EventBus eventBus
|
||||||
|
final I2PConnector i2pConnector
|
||||||
final EventBus eventBus
|
final MuWireSettings settings
|
||||||
final I2PConnector i2pConnector
|
final ConnectionManager connectionManager
|
||||||
final MuWireSettings settings
|
final HostCache hostCache
|
||||||
final ConnectionManager connectionManager
|
|
||||||
final HostCache hostCache
|
final Timer timer
|
||||||
|
final ExecutorService executor
|
||||||
final Timer timer
|
|
||||||
final ExecutorService executor, closer
|
final Set inProgress = new ConcurrentHashSet()
|
||||||
|
|
||||||
final Set inProgress = new ConcurrentHashSet()
|
|
||||||
|
|
||||||
ConnectionEstablisher(){}
|
ConnectionEstablisher(){}
|
||||||
|
|
||||||
|
ConnectionEstablisher(EventBus eventBus, I2PConnector i2pConnector, MuWireSettings settings,
|
||||||
|
ConnectionManager connectionManager, HostCache hostCache) {
|
||||||
|
this.eventBus = eventBus
|
||||||
|
this.i2pConnector = i2pConnector
|
||||||
|
this.settings = settings
|
||||||
|
this.connectionManager = connectionManager
|
||||||
|
this.hostCache = hostCache
|
||||||
|
timer = new Timer("connection-timer",true)
|
||||||
|
executor = Executors.newFixedThreadPool(CONCURRENT, { r ->
|
||||||
|
def rv = new Thread(r)
|
||||||
|
rv.setDaemon(true)
|
||||||
|
rv.setName("connector-${System.currentTimeMillis()}")
|
||||||
|
rv
|
||||||
|
} as ThreadFactory)
|
||||||
|
}
|
||||||
|
|
||||||
|
void start() {
|
||||||
|
timer.schedule({connectIfNeeded()} as TimerTask, 100, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop() {
|
||||||
|
timer.cancel()
|
||||||
|
executor.shutdownNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
private void connectIfNeeded() {
|
||||||
|
if (!connectionManager.needsConnections())
|
||||||
|
return
|
||||||
|
if (inProgress.size() >= CONCURRENT)
|
||||||
|
return
|
||||||
|
|
||||||
ConnectionEstablisher(EventBus eventBus, I2PConnector i2pConnector, MuWireSettings settings,
|
def toTry = null
|
||||||
ConnectionManager connectionManager, HostCache hostCache) {
|
for (int i = 0; i < 5; i++) {
|
||||||
this.eventBus = eventBus
|
toTry = hostCache.getHosts(1)
|
||||||
this.i2pConnector = i2pConnector
|
if (toTry.isEmpty())
|
||||||
this.settings = settings
|
return
|
||||||
this.connectionManager = connectionManager
|
toTry = toTry[0]
|
||||||
this.hostCache = hostCache
|
if (!connectionManager.isConnected(toTry) &&
|
||||||
timer = new Timer("connection-timer",true)
|
!inProgress.contains(toTry)) {
|
||||||
executor = Executors.newFixedThreadPool(CONCURRENT, { r ->
|
break
|
||||||
def rv = new Thread(r)
|
}
|
||||||
rv.setDaemon(true)
|
}
|
||||||
rv.setName("connector-${System.currentTimeMillis()}")
|
if (toTry == null)
|
||||||
rv
|
return
|
||||||
} as ThreadFactory)
|
if (!connectionManager.isConnected(toTry) && inProgress.add(toTry))
|
||||||
|
executor.execute({connect(toTry)} as Runnable)
|
||||||
closer = Executors.newSingleThreadExecutor()
|
}
|
||||||
}
|
|
||||||
|
private void connect(Destination toTry) {
|
||||||
|
log.info("starting connect to ${toTry.toBase32()}")
|
||||||
|
try {
|
||||||
|
def endpoint = i2pConnector.connect(toTry)
|
||||||
|
log.info("successful transport connect to ${toTry.toBase32()}")
|
||||||
|
|
||||||
|
// outgoing handshake
|
||||||
|
endpoint.outputStream.write("MuWire ".bytes)
|
||||||
|
def type = settings.isLeaf() ? "leaf" : "peer"
|
||||||
|
endpoint.outputStream.write(type.bytes)
|
||||||
|
endpoint.outputStream.flush()
|
||||||
|
|
||||||
|
InputStream is = endpoint.inputStream
|
||||||
|
int read = is.read()
|
||||||
|
if (read == -1) {
|
||||||
|
fail endpoint
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch(read) {
|
||||||
|
case (byte)'O': readK(endpoint); break
|
||||||
|
case (byte)'R': readEJECT(endpoint); break
|
||||||
|
default :
|
||||||
|
log.warning("unknown response $read")
|
||||||
|
fail endpoint
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.log(Level.WARNING, "Couldn't connect to ${toTry.toBase32()}", e)
|
||||||
|
def endpoint = new Endpoint(toTry, null, null, null)
|
||||||
|
fail(endpoint)
|
||||||
|
} finally {
|
||||||
|
inProgress.remove(toTry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fail(Endpoint endpoint) {
|
||||||
|
endpoint.close()
|
||||||
|
eventBus.publish(new ConnectionEvent(endpoint: endpoint, incoming: false, leaf: false, status: ConnectionAttemptStatus.FAILED))
|
||||||
|
}
|
||||||
|
|
||||||
|
private void readK(Endpoint e) {
|
||||||
|
int read = e.inputStream.read()
|
||||||
|
if (read != 'K') {
|
||||||
|
log.warning("unknown response after O: $read")
|
||||||
|
fail e
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("connection to ${e.destination.toBase32()} established")
|
||||||
|
|
||||||
|
// wrap into deflater / inflater streams and publish
|
||||||
|
def wrapped = new Endpoint(e.destination, new InflaterInputStream(e.inputStream), new DeflaterOutputStream(e.outputStream, true), e.toClose)
|
||||||
|
eventBus.publish(new ConnectionEvent(endpoint: wrapped, incoming: false, leaf: false, status: ConnectionAttemptStatus.SUCCESSFUL))
|
||||||
|
}
|
||||||
|
|
||||||
|
private void readEJECT(Endpoint e) {
|
||||||
|
byte[] eject = "EJECT".bytes
|
||||||
|
for (int i = 0; i < eject.length; i++) {
|
||||||
|
int read = e.inputStream.read()
|
||||||
|
if (read != eject[i]) {
|
||||||
|
log.warning("Unknown response after R at position $i")
|
||||||
|
fail e
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.info("connection to ${e.destination.toBase32()} rejected")
|
||||||
|
|
||||||
|
|
||||||
|
eventBus.publish(new ConnectionEvent(endpoint: e, incoming: false, leaf: false, status: ConnectionAttemptStatus.REJECTED))
|
||||||
|
try {
|
||||||
|
DataInputStream dais = new DataInputStream(e.inputStream)
|
||||||
|
int payloadSize = dais.readUnsignedShort()
|
||||||
|
byte[] payload = new byte[payloadSize]
|
||||||
|
dais.readFully(payload)
|
||||||
|
|
||||||
void start() {
|
def json = new JsonSlurper()
|
||||||
timer.schedule({connectIfNeeded()} as TimerTask, 100, 1000)
|
json = json.parse(payload)
|
||||||
}
|
|
||||||
|
|
||||||
void stop() {
|
if (json.tryHosts == null) {
|
||||||
timer.cancel()
|
log.warning("post-rejection json didn't contain hosts to try")
|
||||||
executor.shutdownNow()
|
return
|
||||||
closer.shutdown()
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void connectIfNeeded() {
|
|
||||||
if (!connectionManager.needsConnections())
|
|
||||||
return
|
|
||||||
if (inProgress.size() >= CONCURRENT)
|
|
||||||
return
|
|
||||||
|
|
||||||
def toTry = null
|
|
||||||
for (int i = 0; i < 5; i++) {
|
|
||||||
toTry = hostCache.getHosts(1)
|
|
||||||
if (toTry.isEmpty())
|
|
||||||
return
|
|
||||||
toTry = toTry[0]
|
|
||||||
if (!connectionManager.isConnected(toTry) &&
|
|
||||||
!inProgress.contains(toTry)) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (toTry == null)
|
|
||||||
return
|
|
||||||
if (!connectionManager.isConnected(toTry) && inProgress.add(toTry))
|
|
||||||
executor.execute({connect(toTry)} as Runnable)
|
|
||||||
}
|
|
||||||
|
|
||||||
private void connect(Destination toTry) {
|
|
||||||
log.info("starting connect to ${toTry.toBase32()}")
|
|
||||||
try {
|
|
||||||
def endpoint = i2pConnector.connect(toTry)
|
|
||||||
log.info("successful transport connect to ${toTry.toBase32()}")
|
|
||||||
|
|
||||||
// outgoing handshake
|
|
||||||
endpoint.outputStream.write("MuWire ".bytes)
|
|
||||||
def type = settings.isLeaf() ? "leaf" : "peer"
|
|
||||||
endpoint.outputStream.write(type.bytes)
|
|
||||||
endpoint.outputStream.flush()
|
|
||||||
|
|
||||||
InputStream is = endpoint.inputStream
|
|
||||||
int read = is.read()
|
|
||||||
if (read == -1) {
|
|
||||||
fail endpoint
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch(read) {
|
|
||||||
case (byte)'O': readK(endpoint); break
|
|
||||||
case (byte)'R': readEJECT(endpoint); break
|
|
||||||
default :
|
|
||||||
log.warning("unknown response $read")
|
|
||||||
fail endpoint
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.log(Level.WARNING, "Couldn't connect to ${toTry.toBase32()}", e)
|
|
||||||
def endpoint = new Endpoint(toTry, null, null, null)
|
|
||||||
fail(endpoint)
|
|
||||||
} finally {
|
|
||||||
inProgress.remove(toTry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void fail(Endpoint endpoint) {
|
|
||||||
closer.execute {
|
|
||||||
endpoint.close()
|
|
||||||
eventBus.publish(new ConnectionEvent(endpoint: endpoint, incoming: false, leaf: false, status: ConnectionAttemptStatus.FAILED))
|
|
||||||
} as Runnable
|
|
||||||
}
|
|
||||||
|
|
||||||
private void readK(Endpoint e) {
|
|
||||||
int read = e.inputStream.read()
|
|
||||||
if (read != 'K') {
|
|
||||||
log.warning("unknown response after O: $read")
|
|
||||||
fail e
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("connection to ${e.destination.toBase32()} established")
|
|
||||||
|
|
||||||
// wrap into deflater / inflater streams and publish
|
|
||||||
def wrapped = new Endpoint(e.destination, new InflaterInputStream(e.inputStream), new DeflaterOutputStream(e.outputStream, true), e.toClose)
|
|
||||||
eventBus.publish(new ConnectionEvent(endpoint: wrapped, incoming: false, leaf: false, status: ConnectionAttemptStatus.SUCCESSFUL))
|
|
||||||
}
|
|
||||||
|
|
||||||
private void readEJECT(Endpoint e) {
|
|
||||||
byte[] eject = "EJECT".bytes
|
|
||||||
for (int i = 0; i < eject.length; i++) {
|
|
||||||
int read = e.inputStream.read()
|
|
||||||
if (read != eject[i]) {
|
|
||||||
log.warning("Unknown response after R at position $i")
|
|
||||||
fail e
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.info("connection to ${e.destination.toBase32()} rejected")
|
|
||||||
|
|
||||||
|
|
||||||
eventBus.publish(new ConnectionEvent(endpoint: e, incoming: false, leaf: false, status: ConnectionAttemptStatus.REJECTED))
|
|
||||||
try {
|
|
||||||
DataInputStream dais = new DataInputStream(e.inputStream)
|
|
||||||
int payloadSize = dais.readUnsignedShort()
|
|
||||||
byte[] payload = new byte[payloadSize]
|
|
||||||
dais.readFully(payload)
|
|
||||||
|
|
||||||
def json = new JsonSlurper()
|
|
||||||
json = json.parse(payload)
|
|
||||||
|
|
||||||
if (json.tryHosts == null) {
|
|
||||||
log.warning("post-rejection json didn't contain hosts to try")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
json.tryHosts.asList().each {
|
|
||||||
Destination suggested = new Destination(it)
|
|
||||||
eventBus.publish(new HostDiscoveredEvent(destination: suggested))
|
|
||||||
}
|
|
||||||
} catch (Exception ignore) {
|
|
||||||
log.log(Level.WARNING,"Problem parsing post-rejection payload",ignore)
|
|
||||||
} finally {
|
|
||||||
// the end
|
|
||||||
closer.execute({e.close()} as Runnable)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
json.tryHosts.asList().each {
|
||||||
|
Destination suggested = new Destination(it)
|
||||||
|
eventBus.publish(new HostDiscoveredEvent(destination: suggested))
|
||||||
|
}
|
||||||
|
} catch (Exception ignore) {
|
||||||
|
log.log(Level.WARNING,"Problem parsing post-rejection payload",ignore)
|
||||||
|
} finally {
|
||||||
|
// the end
|
||||||
|
e.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isInProgress(Destination d) {
|
public boolean isInProgress(Destination d) {
|
||||||
inProgress.contains(d)
|
inProgress.contains(d)
|
||||||
}
|
}
|
||||||
|
@@ -6,14 +6,14 @@ import net.i2p.data.Destination
|
|||||||
|
|
||||||
class ConnectionEvent extends Event {
|
class ConnectionEvent extends Event {
|
||||||
|
|
||||||
Endpoint endpoint
|
Endpoint endpoint
|
||||||
boolean incoming
|
boolean incoming
|
||||||
Boolean leaf // can be null if uknown
|
Boolean leaf // can be null if uknown
|
||||||
ConnectionAttemptStatus status
|
ConnectionAttemptStatus status
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
"ConnectionEvent ${super.toString()} endpoint: $endpoint incoming: $incoming leaf : $leaf status : $status"
|
"ConnectionEvent ${super.toString()} endpoint: $endpoint incoming: $incoming leaf : $leaf status : $status"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -11,64 +11,64 @@ import com.muwire.core.trust.TrustLevel
|
|||||||
import net.i2p.data.Destination
|
import net.i2p.data.Destination
|
||||||
|
|
||||||
abstract class ConnectionManager {
|
abstract class ConnectionManager {
|
||||||
|
|
||||||
|
private static final int PING_TIME = 20000
|
||||||
|
|
||||||
private static final int PING_TIME = 20000
|
final EventBus eventBus
|
||||||
|
|
||||||
final EventBus eventBus
|
private final Timer timer
|
||||||
|
|
||||||
private final Timer timer
|
protected final HostCache hostCache
|
||||||
|
|
||||||
protected final HostCache hostCache
|
|
||||||
protected final Persona me
|
protected final Persona me
|
||||||
protected final MuWireSettings settings
|
protected final MuWireSettings settings
|
||||||
|
|
||||||
ConnectionManager() {}
|
ConnectionManager() {}
|
||||||
|
|
||||||
ConnectionManager(EventBus eventBus, Persona me, HostCache hostCache, MuWireSettings settings) {
|
ConnectionManager(EventBus eventBus, Persona me, HostCache hostCache, MuWireSettings settings) {
|
||||||
this.eventBus = eventBus
|
this.eventBus = eventBus
|
||||||
this.me = me
|
this.me = me
|
||||||
this.hostCache = hostCache
|
this.hostCache = hostCache
|
||||||
this.settings = settings
|
this.settings = settings
|
||||||
this.timer = new Timer("connections-pinger",true)
|
this.timer = new Timer("connections-pinger",true)
|
||||||
}
|
}
|
||||||
|
|
||||||
void start() {
|
void start() {
|
||||||
timer.schedule({sendPings()} as TimerTask, 1000,1000)
|
timer.schedule({sendPings()} as TimerTask, 1000,1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
void stop() {
|
void stop() {
|
||||||
timer.cancel()
|
timer.cancel()
|
||||||
getConnections().each { it.close() }
|
getConnections().each { it.close() }
|
||||||
}
|
}
|
||||||
|
|
||||||
void onTrustEvent(TrustEvent e) {
|
void onTrustEvent(TrustEvent e) {
|
||||||
if (e.level == TrustLevel.DISTRUSTED)
|
if (e.level == TrustLevel.DISTRUSTED)
|
||||||
drop(e.persona.destination)
|
drop(e.persona.destination)
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract void drop(Destination d)
|
abstract void drop(Destination d)
|
||||||
|
|
||||||
abstract Collection<Connection> getConnections()
|
abstract Collection<Connection> getConnections()
|
||||||
|
|
||||||
protected abstract int getDesiredConnections()
|
protected abstract int getDesiredConnections()
|
||||||
|
|
||||||
boolean needsConnections() {
|
boolean needsConnections() {
|
||||||
return getConnections().size() < getDesiredConnections()
|
return getConnections().size() < getDesiredConnections()
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract boolean isConnected(Destination d)
|
abstract boolean isConnected(Destination d)
|
||||||
|
|
||||||
abstract void onConnectionEvent(ConnectionEvent e)
|
abstract void onConnectionEvent(ConnectionEvent e)
|
||||||
|
|
||||||
abstract void onDisconnectionEvent(DisconnectionEvent e)
|
abstract void onDisconnectionEvent(DisconnectionEvent e)
|
||||||
|
|
||||||
abstract void shutdown()
|
abstract void shutdown()
|
||||||
|
|
||||||
protected void sendPings() {
|
protected void sendPings() {
|
||||||
final long now = System.currentTimeMillis()
|
final long now = System.currentTimeMillis()
|
||||||
getConnections().each {
|
getConnections().each {
|
||||||
if (now - it.lastPingSentTime > PING_TIME)
|
if (now - it.lastPingSentTime > PING_TIME)
|
||||||
it.sendPing()
|
it.sendPing()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,11 +5,11 @@ import com.muwire.core.Event
|
|||||||
import net.i2p.data.Destination
|
import net.i2p.data.Destination
|
||||||
|
|
||||||
class DisconnectionEvent extends Event {
|
class DisconnectionEvent extends Event {
|
||||||
|
|
||||||
|
Destination destination
|
||||||
|
|
||||||
Destination destination
|
@Override
|
||||||
|
public String toString() {
|
||||||
@Override
|
"DisconnectionEvent ${super.toString()} destination:${destination.toBase32()}"
|
||||||
public String toString() {
|
}
|
||||||
"DisconnectionEvent ${super.toString()} destination:${destination.toBase32()}"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -8,39 +8,39 @@ import net.i2p.data.Destination
|
|||||||
|
|
||||||
@Log
|
@Log
|
||||||
class Endpoint implements Closeable {
|
class Endpoint implements Closeable {
|
||||||
final Destination destination
|
final Destination destination
|
||||||
final InputStream inputStream
|
final InputStream inputStream
|
||||||
final OutputStream outputStream
|
final OutputStream outputStream
|
||||||
final def toClose
|
final def toClose
|
||||||
|
|
||||||
private final AtomicBoolean closed = new AtomicBoolean()
|
private final AtomicBoolean closed = new AtomicBoolean()
|
||||||
|
|
||||||
Endpoint(Destination destination, InputStream inputStream, OutputStream outputStream, def toClose) {
|
Endpoint(Destination destination, InputStream inputStream, OutputStream outputStream, def toClose) {
|
||||||
this.destination = destination
|
this.destination = destination
|
||||||
this.inputStream = inputStream
|
this.inputStream = inputStream
|
||||||
this.outputStream = outputStream
|
this.outputStream = outputStream
|
||||||
this.toClose = toClose
|
this.toClose = toClose
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
if (!closed.compareAndSet(false, true)) {
|
if (!closed.compareAndSet(false, true)) {
|
||||||
log.log(Level.WARNING,"Close loop detected for ${destination.toBase32()}", new Exception())
|
log.log(Level.WARNING,"Close loop detected for ${destination.toBase32()}", new Exception())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (inputStream != null) {
|
if (inputStream != null) {
|
||||||
try {inputStream.close()} catch (Exception ignore) {}
|
try {inputStream.close()} catch (Exception ignore) {}
|
||||||
}
|
}
|
||||||
if (outputStream != null) {
|
if (outputStream != null) {
|
||||||
try {outputStream.close()} catch (Exception ignore) {}
|
try {outputStream.close()} catch (Exception ignore) {}
|
||||||
}
|
}
|
||||||
if (toClose != null) {
|
if (toClose != null) {
|
||||||
try {toClose.reset()} catch (Exception ignore) {}
|
try {toClose.reset()} catch (Exception ignore) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
"destination: ${destination.toBase32()}"
|
"destination: ${destination.toBase32()}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,18 +5,18 @@ import net.i2p.client.streaming.I2PSocketManager
|
|||||||
|
|
||||||
class I2PAcceptor {
|
class I2PAcceptor {
|
||||||
|
|
||||||
final I2PSocketManager socketManager
|
final I2PSocketManager socketManager
|
||||||
final I2PServerSocket serverSocket
|
final I2PServerSocket serverSocket
|
||||||
|
|
||||||
I2PAcceptor() {}
|
I2PAcceptor() {}
|
||||||
|
|
||||||
I2PAcceptor(I2PSocketManager socketManager) {
|
I2PAcceptor(I2PSocketManager socketManager) {
|
||||||
this.socketManager = socketManager
|
this.socketManager = socketManager
|
||||||
this.serverSocket = socketManager.getServerSocket()
|
this.serverSocket = socketManager.getServerSocket()
|
||||||
}
|
}
|
||||||
|
|
||||||
Endpoint accept() {
|
Endpoint accept() {
|
||||||
def socket = serverSocket.accept()
|
def socket = serverSocket.accept()
|
||||||
new Endpoint(socket.getPeerDestination(), socket.getInputStream(), socket.getOutputStream(), socket)
|
new Endpoint(socket.getPeerDestination(), socket.getInputStream(), socket.getOutputStream(), socket)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,18 +4,18 @@ import net.i2p.client.streaming.I2PSocketManager
|
|||||||
import net.i2p.data.Destination
|
import net.i2p.data.Destination
|
||||||
|
|
||||||
class I2PConnector {
|
class I2PConnector {
|
||||||
|
|
||||||
final I2PSocketManager socketManager
|
final I2PSocketManager socketManager
|
||||||
|
|
||||||
I2PConnector() {}
|
I2PConnector() {}
|
||||||
|
|
||||||
I2PConnector(I2PSocketManager socketManager) {
|
I2PConnector(I2PSocketManager socketManager) {
|
||||||
this.socketManager = socketManager
|
this.socketManager = socketManager
|
||||||
}
|
}
|
||||||
|
|
||||||
Endpoint connect(Destination dest) {
|
Endpoint connect(Destination dest) {
|
||||||
def socket = socketManager.connect(dest)
|
def socket = socketManager.connect(dest)
|
||||||
new Endpoint(dest, socket.getInputStream(), socket.getOutputStream(), socket)
|
new Endpoint(dest, socket.getInputStream(), socket.getOutputStream(), socket)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -11,27 +11,27 @@ import com.muwire.core.trust.TrustService
|
|||||||
import net.i2p.data.Destination
|
import net.i2p.data.Destination
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connection where the other side is a leaf.
|
* Connection where the other side is a leaf.
|
||||||
* Such connections can only be incoming.
|
* Such connections can only be incoming.
|
||||||
* @author zab
|
* @author zab
|
||||||
*/
|
*/
|
||||||
class LeafConnection extends Connection {
|
class LeafConnection extends Connection {
|
||||||
|
|
||||||
public LeafConnection(EventBus eventBus, Endpoint endpoint, HostCache hostCache,
|
public LeafConnection(EventBus eventBus, Endpoint endpoint, HostCache hostCache,
|
||||||
TrustService trustService, MuWireSettings settings) {
|
TrustService trustService, MuWireSettings settings) {
|
||||||
super(eventBus, endpoint, true, hostCache, trustService, settings);
|
super(eventBus, endpoint, true, hostCache, trustService, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void read() {
|
protected void read() {
|
||||||
// TODO Auto-generated method stub
|
// TODO Auto-generated method stub
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
@Override
|
||||||
|
protected void write(Object message) {
|
||||||
@Override
|
// TODO Auto-generated method stub
|
||||||
protected void write(Object message) {
|
|
||||||
// TODO Auto-generated method stub
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -13,68 +13,68 @@ import net.i2p.data.Destination
|
|||||||
|
|
||||||
@Log
|
@Log
|
||||||
class LeafConnectionManager extends ConnectionManager {
|
class LeafConnectionManager extends ConnectionManager {
|
||||||
|
|
||||||
final int maxConnections
|
final int maxConnections
|
||||||
|
|
||||||
final Map<Destination, UltrapeerConnection> connections = new ConcurrentHashMap()
|
final Map<Destination, UltrapeerConnection> connections = new ConcurrentHashMap()
|
||||||
|
|
||||||
public LeafConnectionManager(EventBus eventBus, Persona me, int maxConnections,
|
public LeafConnectionManager(EventBus eventBus, Persona me, int maxConnections,
|
||||||
HostCache hostCache, MuWireSettings settings) {
|
HostCache hostCache, MuWireSettings settings) {
|
||||||
super(eventBus, me, hostCache, settings)
|
super(eventBus, me, hostCache, settings)
|
||||||
this.maxConnections = maxConnections
|
this.maxConnections = maxConnections
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void drop(Destination d) {
|
public void drop(Destination d) {
|
||||||
// TODO Auto-generated method stub
|
// TODO Auto-generated method stub
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void onQueryEvent(QueryEvent e) {
|
void onQueryEvent(QueryEvent e) {
|
||||||
if (me.destination == e.receivedOn) {
|
if (me.destination == e.receivedOn) {
|
||||||
connections.values().each { it.sendQuery(e) }
|
connections.values().each { it.sendQuery(e) }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<Connection> getConnections() {
|
public Collection<Connection> getConnections() {
|
||||||
connections.values()
|
connections.values()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int getDesiredConnections() {
|
protected int getDesiredConnections() {
|
||||||
return maxConnections;
|
return maxConnections;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isConnected(Destination d) {
|
public boolean isConnected(Destination d) {
|
||||||
connections.containsKey(d)
|
connections.containsKey(d)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onConnectionEvent(ConnectionEvent e) {
|
|
||||||
if (e.incoming || e.leaf) {
|
|
||||||
log.severe("Got inconsistent event as a leaf! $e")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (e.status != ConnectionAttemptStatus.SUCCESSFUL)
|
|
||||||
return
|
|
||||||
|
|
||||||
Connection c = new UltrapeerConnection(eventBus, e.endpoint)
|
|
||||||
connections.put(e.endpoint.destination, c)
|
|
||||||
c.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDisconnectionEvent(DisconnectionEvent e) {
|
|
||||||
def removed = connections.remove(e.destination)
|
|
||||||
if (removed == null)
|
|
||||||
log.severe("removed destination not present in connection manager ${e.destination.toBase32()}")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConnectionEvent(ConnectionEvent e) {
|
||||||
|
if (e.incoming || e.leaf) {
|
||||||
|
log.severe("Got inconsistent event as a leaf! $e")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (e.status != ConnectionAttemptStatus.SUCCESSFUL)
|
||||||
|
return
|
||||||
|
|
||||||
|
Connection c = new UltrapeerConnection(eventBus, e.endpoint)
|
||||||
|
connections.put(e.endpoint.destination, c)
|
||||||
|
c.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisconnectionEvent(DisconnectionEvent e) {
|
||||||
|
def removed = connections.remove(e.destination)
|
||||||
|
if (removed == null)
|
||||||
|
log.severe("removed destination not present in connection manager ${e.destination.toBase32()}")
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
void shutdown() {
|
void shutdown() {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
@@ -20,63 +21,63 @@ import net.i2p.data.Destination
|
|||||||
*/
|
*/
|
||||||
@Log
|
@Log
|
||||||
class PeerConnection extends Connection {
|
class PeerConnection extends Connection {
|
||||||
|
|
||||||
|
private final DataInputStream dis
|
||||||
|
private final DataOutputStream dos
|
||||||
|
|
||||||
|
private final byte[] readHeader = new byte[3]
|
||||||
|
private final byte[] writeHeader = new byte[3]
|
||||||
|
|
||||||
|
private final JsonSlurper slurper = new JsonSlurper()
|
||||||
|
|
||||||
private final DataInputStream dis
|
public PeerConnection(EventBus eventBus, Endpoint endpoint,
|
||||||
private final DataOutputStream dos
|
boolean incoming, HostCache hostCache, TrustService trustService,
|
||||||
|
|
||||||
private final byte[] readHeader = new byte[3]
|
|
||||||
private final byte[] writeHeader = new byte[3]
|
|
||||||
|
|
||||||
private final JsonSlurper slurper = new JsonSlurper()
|
|
||||||
|
|
||||||
public PeerConnection(EventBus eventBus, Endpoint endpoint,
|
|
||||||
boolean incoming, HostCache hostCache, TrustService trustService,
|
|
||||||
MuWireSettings settings) {
|
MuWireSettings settings) {
|
||||||
super(eventBus, endpoint, incoming, hostCache, trustService, settings)
|
super(eventBus, endpoint, incoming, hostCache, trustService, settings)
|
||||||
this.dis = new DataInputStream(endpoint.inputStream)
|
this.dis = new DataInputStream(endpoint.inputStream)
|
||||||
this.dos = new DataOutputStream(endpoint.outputStream)
|
this.dos = new DataOutputStream(endpoint.outputStream)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void read() {
|
protected void read() {
|
||||||
dis.readFully(readHeader)
|
dis.readFully(readHeader)
|
||||||
int length = DataUtil.readLength(readHeader)
|
int length = DataUtil.readLength(readHeader)
|
||||||
log.fine("$name read length $length")
|
log.fine("$name read length $length")
|
||||||
|
|
||||||
byte[] payload = new byte[length]
|
byte[] payload = new byte[length]
|
||||||
dis.readFully(payload)
|
dis.readFully(payload)
|
||||||
|
|
||||||
if ((readHeader[0] & (byte)0x80) == 0x80) {
|
if ((readHeader[0] & (byte)0x80) == 0x80) {
|
||||||
// TODO process binary
|
// TODO process binary
|
||||||
} else {
|
} else {
|
||||||
def json = slurper.parse(payload)
|
def json = slurper.parse(payload)
|
||||||
if (json.type == null)
|
if (json.type == null)
|
||||||
throw new Exception("missing json type")
|
throw new Exception("missing json type")
|
||||||
switch(json.type) {
|
switch(json.type) {
|
||||||
case "Ping" : handlePing(); break;
|
case "Ping" : handlePing(); break;
|
||||||
case "Pong" : handlePong(json); break;
|
case "Pong" : handlePong(json); break;
|
||||||
case "Search": handleSearch(json); break
|
case "Search": handleSearch(json); break
|
||||||
default :
|
default :
|
||||||
throw new Exception("unknown json type ${json.type}")
|
throw new Exception("unknown json type ${json.type}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
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
|
||||||
} else {
|
} else {
|
||||||
// TODO: write binary
|
// TODO: write binary
|
||||||
}
|
}
|
||||||
|
|
||||||
dos.write(writeHeader)
|
dos.write(writeHeader)
|
||||||
dos.write(payload)
|
dos.write(payload)
|
||||||
dos.flush()
|
dos.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -17,30 +17,30 @@ import net.i2p.data.Destination
|
|||||||
*/
|
*/
|
||||||
class UltrapeerConnection extends Connection {
|
class UltrapeerConnection extends Connection {
|
||||||
|
|
||||||
public UltrapeerConnection(EventBus eventBus, Endpoint endpoint, HostCache hostCache, TrustService trustService) {
|
public UltrapeerConnection(EventBus eventBus, Endpoint endpoint, HostCache hostCache, TrustService trustService) {
|
||||||
super(eventBus, endpoint, false, hostCache, trustService)
|
super(eventBus, endpoint, false, hostCache, trustService)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void read() {
|
protected void read() {
|
||||||
// TODO Auto-generated method stub
|
// TODO Auto-generated method stub
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
@Override
|
||||||
|
protected void write(Object message) {
|
||||||
|
if (message instanceof Map) {
|
||||||
|
writeJsonMessage(message)
|
||||||
|
} else {
|
||||||
|
writeBinaryMessage(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
private void writeJsonMessage(def message) {
|
||||||
protected void write(Object message) {
|
|
||||||
if (message instanceof Map) {
|
}
|
||||||
writeJsonMessage(message)
|
|
||||||
} else {
|
private void writeBinaryMessage(def message) {
|
||||||
writeBinaryMessage(message)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void writeJsonMessage(def message) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeBinaryMessage(def message) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -15,28 +15,28 @@ import net.i2p.data.Destination
|
|||||||
|
|
||||||
@Log
|
@Log
|
||||||
class UltrapeerConnectionManager extends ConnectionManager {
|
class UltrapeerConnectionManager extends ConnectionManager {
|
||||||
|
|
||||||
final int maxPeers, maxLeafs
|
final int maxPeers, maxLeafs
|
||||||
final TrustService trustService
|
final TrustService trustService
|
||||||
|
|
||||||
|
final Map<Destination, PeerConnection> peerConnections = new ConcurrentHashMap()
|
||||||
|
final Map<Destination, LeafConnection> leafConnections = new ConcurrentHashMap()
|
||||||
|
|
||||||
|
UltrapeerConnectionManager() {}
|
||||||
|
|
||||||
final Map<Destination, PeerConnection> peerConnections = new ConcurrentHashMap()
|
public UltrapeerConnectionManager(EventBus eventBus, Persona me, int maxPeers, int maxLeafs,
|
||||||
final Map<Destination, LeafConnection> leafConnections = new ConcurrentHashMap()
|
|
||||||
|
|
||||||
UltrapeerConnectionManager() {}
|
|
||||||
|
|
||||||
public UltrapeerConnectionManager(EventBus eventBus, Persona me, int maxPeers, int maxLeafs,
|
|
||||||
HostCache hostCache, TrustService trustService, MuWireSettings settings) {
|
HostCache hostCache, TrustService trustService, MuWireSettings settings) {
|
||||||
super(eventBus, me, hostCache, settings)
|
super(eventBus, me, hostCache, settings)
|
||||||
this.maxPeers = maxPeers
|
this.maxPeers = maxPeers
|
||||||
this.maxLeafs = maxLeafs
|
this.maxLeafs = maxLeafs
|
||||||
this.trustService = trustService
|
this.trustService = trustService
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
public void drop(Destination d) {
|
public void drop(Destination d) {
|
||||||
peerConnections.get(d)?.close()
|
peerConnections.get(d)?.close()
|
||||||
leafConnections.get(d)?.close()
|
leafConnections.get(d)?.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
void onQueryEvent(QueryEvent e) {
|
void onQueryEvent(QueryEvent e) {
|
||||||
forwardQueryToLeafs(e)
|
forwardQueryToLeafs(e)
|
||||||
if (!e.firstHop)
|
if (!e.firstHop)
|
||||||
@@ -50,58 +50,58 @@ class UltrapeerConnectionManager extends ConnectionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<Connection> getConnections() {
|
public Collection<Connection> getConnections() {
|
||||||
def rv = new ArrayList(peerConnections.size() + leafConnections.size())
|
def rv = new ArrayList(peerConnections.size() + leafConnections.size())
|
||||||
rv.addAll(peerConnections.values())
|
rv.addAll(peerConnections.values())
|
||||||
rv.addAll(leafConnections.values())
|
rv.addAll(leafConnections.values())
|
||||||
rv
|
rv
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean hasLeafSlots() {
|
boolean hasLeafSlots() {
|
||||||
leafConnections.size() < maxLeafs
|
leafConnections.size() < maxLeafs
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean hasPeerSlots() {
|
boolean hasPeerSlots() {
|
||||||
peerConnections.size() < maxPeers
|
peerConnections.size() < maxPeers
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int getDesiredConnections() {
|
protected int getDesiredConnections() {
|
||||||
return maxPeers / 2;
|
return maxPeers / 2;
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
public boolean isConnected(Destination d) {
|
public boolean isConnected(Destination d) {
|
||||||
peerConnections.containsKey(d) || leafConnections.containsKey(d)
|
peerConnections.containsKey(d) || leafConnections.containsKey(d)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onConnectionEvent(ConnectionEvent e) {
|
|
||||||
if (!e.incoming && e.leaf) {
|
|
||||||
log.severe("Inconsistent event $e")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.status != ConnectionAttemptStatus.SUCCESSFUL)
|
|
||||||
return
|
|
||||||
|
|
||||||
Connection c = e.leaf ?
|
|
||||||
new LeafConnection(eventBus, e.endpoint, hostCache, trustService, settings) :
|
|
||||||
new PeerConnection(eventBus, e.endpoint, e.incoming, hostCache, trustService, settings)
|
|
||||||
def map = e.leaf ? leafConnections : peerConnections
|
|
||||||
map.put(e.endpoint.destination, c)
|
|
||||||
c.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDisconnectionEvent(DisconnectionEvent e) {
|
|
||||||
def removed = peerConnections.remove(e.destination)
|
|
||||||
if (removed == null)
|
|
||||||
removed = leafConnections.remove(e.destination)
|
|
||||||
if (removed == null)
|
|
||||||
log.severe("Removed connection not present in either leaf or peer map ${e.destination.toBase32()}")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConnectionEvent(ConnectionEvent e) {
|
||||||
|
if (!e.incoming && e.leaf) {
|
||||||
|
log.severe("Inconsistent event $e")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.status != ConnectionAttemptStatus.SUCCESSFUL)
|
||||||
|
return
|
||||||
|
|
||||||
|
Connection c = e.leaf ?
|
||||||
|
new LeafConnection(eventBus, e.endpoint, hostCache, trustService, settings) :
|
||||||
|
new PeerConnection(eventBus, e.endpoint, e.incoming, hostCache, trustService, settings)
|
||||||
|
def map = e.leaf ? leafConnections : peerConnections
|
||||||
|
map.put(e.endpoint.destination, c)
|
||||||
|
c.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisconnectionEvent(DisconnectionEvent e) {
|
||||||
|
def removed = peerConnections.remove(e.destination)
|
||||||
|
if (removed == null)
|
||||||
|
removed = leafConnections.remove(e.destination)
|
||||||
|
if (removed == null)
|
||||||
|
log.severe("Removed connection not present in either leaf or peer map ${e.destination.toBase32()}")
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
void shutdown() {
|
void shutdown() {
|
||||||
peerConnections.values().stream().parallel().forEach({v -> v.close()})
|
peerConnections.values().stream().parallel().forEach({v -> v.close()})
|
||||||
@@ -109,8 +109,8 @@ class UltrapeerConnectionManager extends ConnectionManager {
|
|||||||
peerConnections.clear()
|
peerConnections.clear()
|
||||||
leafConnections.clear()
|
leafConnections.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
void forwardQueryToLeafs(QueryEvent e) {
|
void forwardQueryToLeafs(QueryEvent e) {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,9 +0,0 @@
|
|||||||
package com.muwire.core.content
|
|
||||||
|
|
||||||
import com.muwire.core.Event
|
|
||||||
|
|
||||||
class ContentControlEvent extends Event {
|
|
||||||
String term
|
|
||||||
boolean regex
|
|
||||||
boolean add
|
|
||||||
}
|
|
@@ -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) }
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,9 +0,0 @@
|
|||||||
package com.muwire.core.content
|
|
||||||
|
|
||||||
import com.muwire.core.Persona
|
|
||||||
|
|
||||||
class Match {
|
|
||||||
Persona persona
|
|
||||||
String [] keywords
|
|
||||||
long timestamp
|
|
||||||
}
|
|
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
@@ -21,5 +21,5 @@ class BadHashException extends Exception {
|
|||||||
public BadHashException(Throwable cause) {
|
public BadHashException(Throwable cause) {
|
||||||
super(cause);
|
super(cause);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -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")
|
||||||
@@ -56,54 +46,37 @@ public class DownloadManager {
|
|||||||
rv
|
rv
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
Set<Destination> destinations = new HashSet<>()
|
Set<Destination> destinations = new HashSet<>()
|
||||||
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()
|
persistDownloaders()
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onUIDownloadPausedEvent(UIDownloadPausedEvent e) {
|
|
||||||
persistDownloaders()
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onUIDownloadResumedEvent(UIDownloadResumedEvent e) {
|
|
||||||
persistDownloaders()
|
|
||||||
}
|
|
||||||
|
|
||||||
void resume(Downloader downloader) {
|
void resume(Downloader downloader) {
|
||||||
executor.execute({downloader.download() as Runnable})
|
executor.execute({downloader.download() as Runnable})
|
||||||
}
|
}
|
||||||
|
|
||||||
void onUILoadedEvent(UILoadedEvent e) {
|
void onUILoadedEvent(UILoadedEvent e) {
|
||||||
File downloadsFile = new File(home, "downloads.json")
|
File downloadsFile = new File(home, "downloads.json")
|
||||||
if (!downloadsFile.exists())
|
if (!downloadsFile.exists())
|
||||||
@@ -113,7 +86,7 @@ public class DownloadManager {
|
|||||||
def json = slurper.parseText(it)
|
def json = slurper.parseText(it)
|
||||||
File file = new File(DataUtil.readi18nString(Base64.decode(json.file)))
|
File file = new File(DataUtil.readi18nString(Base64.decode(json.file)))
|
||||||
def destinations = new HashSet<>()
|
def destinations = new HashSet<>()
|
||||||
json.destinations.each { destination ->
|
json.destinations.each { destination ->
|
||||||
destinations.add new Destination(destination)
|
destinations.add new Destination(destination)
|
||||||
}
|
}
|
||||||
InfoHash infoHash
|
InfoHash infoHash
|
||||||
@@ -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
|
downloader.download()
|
||||||
downloaders.put(infoHash, downloader)
|
|
||||||
downloader.readPieces()
|
|
||||||
if (!downloader.paused)
|
|
||||||
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()))
|
||||||
@@ -192,27 +124,20 @@ public class DownloadManager {
|
|||||||
destinations << it.toBase64()
|
destinations << it.toBase64()
|
||||||
}
|
}
|
||||||
json.destinations = destinations
|
json.destinations = destinations
|
||||||
|
|
||||||
InfoHash infoHash = downloader.getInfoHash()
|
InfoHash infoHash = downloader.getInfoHash()
|
||||||
if (infoHash.hashList != null)
|
if (infoHash.hashList != null)
|
||||||
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void shutdown() {
|
public void shutdown() {
|
||||||
downloaders.values().each { it.stop() }
|
downloaders.each { it.stop() }
|
||||||
Downloader.executorService.shutdownNow()
|
Downloader.executorService.shutdownNow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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) {
|
||||||
@@ -60,7 +53,7 @@ class DownloadSession {
|
|||||||
System.exit(1)
|
System.exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return if the request will proceed. The only time it may not
|
* @return if the request will proceed. The only time it may not
|
||||||
* is if all the pieces have been claimed by other sessions.
|
* is if all the pieces have been claimed by other sessions.
|
||||||
@@ -69,164 +62,143 @@ class DownloadSession {
|
|||||||
public boolean request() throws IOException {
|
public boolean request() throws IOException {
|
||||||
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
|
||||||
|
}
|
||||||
log.info("will download piece $piece from position $position steal $steal")
|
claimed.markDownloaded(piece)
|
||||||
|
|
||||||
long pieceStart = piece * ((long)pieceSize)
|
log.info("will download piece $piece")
|
||||||
long end = Math.min(fileLength, pieceStart + pieceSize) - 1
|
|
||||||
long start = pieceStart + position
|
long start = piece * pieceSize
|
||||||
|
long end = Math.min(fileLength, start + pieceSize) - 1
|
||||||
|
long length = end - start + 1
|
||||||
|
|
||||||
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
|
||||||
|
}
|
||||||
|
|
||||||
|
receivedStart = Long.parseLong(group[0][1])
|
||||||
|
receivedEnd = Long.parseLong(group[0][2])
|
||||||
}
|
}
|
||||||
|
|
||||||
// prase X-Alt if present
|
|
||||||
if (headers.containsKey("X-Alt")) {
|
|
||||||
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")
|
||||||
endpoint.close()
|
endpoint.close()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// start the download
|
// start the download
|
||||||
FileChannel channel
|
channel = Files.newByteChannel(file.toPath(), EnumSet.of(StandardOpenOption.READ, StandardOpenOption.WRITE,
|
||||||
try {
|
StandardOpenOption.SPARSE, StandardOpenOption.CREATE)) // TODO: double-check, maybe CREATE_NEW
|
||||||
channel = Files.newByteChannel(file.toPath(), EnumSet.of(StandardOpenOption.READ, StandardOpenOption.WRITE,
|
mapped = channel.map(FileChannel.MapMode.READ_WRITE, start, end - start + 1)
|
||||||
StandardOpenOption.SPARSE, StandardOpenOption.CREATE))
|
|
||||||
mapped = channel.map(FileChannel.MapMode.READ_WRITE, pieceStart, end - pieceStart + 1)
|
byte[] tmp = new byte[0x1 << 13]
|
||||||
mapped.position(position)
|
while(mapped.hasRemaining()) {
|
||||||
|
if (mapped.remaining() < tmp.length)
|
||||||
byte[] tmp = new byte[0x1 << 13]
|
tmp = new byte[mapped.remaining()]
|
||||||
while(mapped.hasRemaining()) {
|
int read = is.read(tmp)
|
||||||
if (mapped.remaining() < tmp.length)
|
if (read == -1)
|
||||||
tmp = new byte[mapped.remaining()]
|
throw new IOException()
|
||||||
int read = is.read(tmp)
|
synchronized(this) {
|
||||||
if (read == -1)
|
mapped.put(tmp, 0, read)
|
||||||
throw new IOException()
|
|
||||||
synchronized(this) {
|
if (timestamps.size() == SAMPLES) {
|
||||||
mapped.put(tmp, 0, read)
|
timestamps.removeFirst()
|
||||||
dataSinceLastRead += read
|
reads.removeFirst()
|
||||||
pieces.markPartial(piece, mapped.position())
|
|
||||||
}
|
}
|
||||||
|
timestamps.addLast(System.currentTimeMillis())
|
||||||
|
reads.addLast(read)
|
||||||
}
|
}
|
||||||
|
|
||||||
mapped.clear()
|
|
||||||
digest.update(mapped)
|
|
||||||
byte [] hash = digest.digest()
|
|
||||||
byte [] expected = new byte[32]
|
|
||||||
System.arraycopy(infoHash.getHashList(), piece * 32, expected, 0, 32)
|
|
||||||
if (hash != expected) {
|
|
||||||
pieces.markPartial(piece, 0)
|
|
||||||
throw new BadHashException("bad hash on piece $piece")
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
try { channel?.close() } catch (IOException ignore) {}
|
|
||||||
DataUtil.tryUnmap(mapped)
|
|
||||||
}
|
}
|
||||||
pieces.markDownloaded(piece)
|
|
||||||
unclaim = false
|
mapped.clear()
|
||||||
|
digest.update(mapped)
|
||||||
|
byte [] hash = digest.digest()
|
||||||
|
byte [] expected = new byte[32]
|
||||||
|
System.arraycopy(infoHash.getHashList(), piece * 32, expected, 0, 32)
|
||||||
|
if (hash != expected)
|
||||||
|
throw new BadHashException()
|
||||||
|
|
||||||
|
downloaded.markDownloaded(piece)
|
||||||
} finally {
|
} finally {
|
||||||
if (unclaim && !steal)
|
claimed.clear(piece)
|
||||||
pieces.unclaim(piece)
|
try { channel?.close() } catch (IOException ignore) {}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized int positionInPiece() {
|
synchronized int positionInPiece() {
|
||||||
if (mapped == null)
|
if (mapped == null)
|
||||||
return 0
|
return 0
|
||||||
mapped.position()
|
mapped.position()
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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,18 +14,15 @@ 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 ->
|
||||||
Thread rv = new Thread(r)
|
Thread rv = new Thread(r)
|
||||||
rv.setName("download worker")
|
rv.setName("download worker")
|
||||||
@@ -39,37 +31,28 @@ public class Downloader {
|
|||||||
})
|
})
|
||||||
|
|
||||||
private final EventBus eventBus
|
private final EventBus eventBus
|
||||||
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
|
||||||
|
private volatile boolean eventFired
|
||||||
|
|
||||||
|
public Downloader(EventBus eventBus, DownloadManager downloadManager,
|
||||||
private volatile boolean cancelled, paused
|
Persona me, File file, long length, InfoHash infoHash,
|
||||||
private final AtomicBoolean eventFired = new AtomicBoolean()
|
|
||||||
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,
|
|
||||||
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,23 +61,29 @@ 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() {
|
||||||
infoHash
|
infoHash
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized void setInfoHash(InfoHash infoHash) {
|
private synchronized void setInfoHash(InfoHash infoHash) {
|
||||||
this.infoHash = infoHash
|
this.infoHash = infoHash
|
||||||
}
|
}
|
||||||
|
|
||||||
void download() {
|
void download() {
|
||||||
readPieces()
|
readPieces()
|
||||||
destinations.each {
|
destinations.each {
|
||||||
@@ -105,106 +94,68 @@ public class Downloader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void readPieces() {
|
void readPieces() {
|
||||||
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) {
|
piecesFile.withPrintWriter { writer ->
|
||||||
if (piecesFileClosed)
|
downloaded.getDownloaded().each { piece ->
|
||||||
return
|
writer.println(piece)
|
||||||
piecesFile.withPrintWriter { writer ->
|
|
||||||
pieces.write(writer)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// if at least one is downloading...
|
// if at least one is downloading...
|
||||||
boolean oneDownloading = false
|
boolean oneDownloading = false
|
||||||
activeWorkers.values().each {
|
activeWorkers.values().each {
|
||||||
if (it.currentState == WorkerState.DOWNLOADING) {
|
if (it.currentState == WorkerState.DOWNLOADING) {
|
||||||
oneDownloading = true
|
oneDownloading = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oneDownloading)
|
if (oneDownloading)
|
||||||
return DownloadState.DOWNLOADING
|
return DownloadState.DOWNLOADING
|
||||||
|
|
||||||
// at least one is requesting hashlist
|
// at least one is requesting hashlist
|
||||||
boolean oneHashlist = false
|
boolean oneHashlist = false
|
||||||
activeWorkers.values().each {
|
activeWorkers.values().each {
|
||||||
if (it.currentState == WorkerState.HASHLIST) {
|
if (it.currentState == WorkerState.HASHLIST) {
|
||||||
oneHashlist = true
|
oneHashlist = true
|
||||||
return
|
return
|
||||||
@@ -212,44 +163,33 @@ public class Downloader {
|
|||||||
}
|
}
|
||||||
if (oneHashlist)
|
if (oneHashlist)
|
||||||
return DownloadState.HASHLIST
|
return DownloadState.HASHLIST
|
||||||
|
|
||||||
return DownloadState.CONNECTING
|
return DownloadState.CONNECTING
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
it.cancel()
|
it.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public int activeWorkers() {
|
public int activeWorkers() {
|
||||||
int active = 0
|
int active = 0
|
||||||
activeWorkers.values().each {
|
activeWorkers.values().each {
|
||||||
if (it.currentState != WorkerState.FINISHED)
|
if (it.currentState != WorkerState.FINISHED)
|
||||||
active++
|
active++
|
||||||
}
|
}
|
||||||
active
|
active
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
@@ -265,27 +205,18 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
public void run() {
|
public void run() {
|
||||||
downloadThread = Thread.currentThread()
|
downloadThread = Thread.currentThread()
|
||||||
currentState = WorkerState.CONNECTING
|
currentState = WorkerState.CONNECTING
|
||||||
@@ -300,51 +231,36 @@ 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) {
|
piecesFile.delete()
|
||||||
piecesFileClosed = true
|
eventFired = true
|
||||||
piecesFile.delete()
|
|
||||||
}
|
|
||||||
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))
|
||||||
|
|
||||||
}
|
}
|
||||||
endpoint?.close()
|
endpoint?.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int speed() {
|
int speed() {
|
||||||
if (currentSession == null)
|
if (currentSession == null)
|
||||||
return 0
|
return 0
|
||||||
currentSession.speed()
|
currentSession.speed()
|
||||||
}
|
}
|
||||||
|
|
||||||
void cancel() {
|
void cancel() {
|
||||||
downloadThread?.interrupt()
|
downloadThread?.interrupt()
|
||||||
}
|
}
|
||||||
|
@@ -20,32 +20,32 @@ class HashListSession {
|
|||||||
private final String meB64
|
private final String meB64
|
||||||
private final InfoHash infoHash
|
private final InfoHash infoHash
|
||||||
private final Endpoint endpoint
|
private final Endpoint endpoint
|
||||||
|
|
||||||
HashListSession(String meB64, InfoHash infoHash, Endpoint endpoint) {
|
HashListSession(String meB64, InfoHash infoHash, Endpoint endpoint) {
|
||||||
this.meB64 = meB64
|
this.meB64 = meB64
|
||||||
this.infoHash = infoHash
|
this.infoHash = infoHash
|
||||||
this.endpoint = endpoint
|
this.endpoint = endpoint
|
||||||
}
|
}
|
||||||
|
|
||||||
InfoHash request() throws IOException {
|
InfoHash request() throws IOException {
|
||||||
InputStream is = endpoint.getInputStream()
|
InputStream is = endpoint.getInputStream()
|
||||||
OutputStream os = endpoint.getOutputStream()
|
OutputStream os = endpoint.getOutputStream()
|
||||||
|
|
||||||
String root = Base64.encode(infoHash.getRoot())
|
String root = Base64.encode(infoHash.getRoot())
|
||||||
os.write("HASHLIST $root\r\n".getBytes(StandardCharsets.US_ASCII))
|
os.write("HASHLIST $root\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
os.write("X-Persona: $meB64\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
os.write("X-Persona: $meB64\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
os.flush()
|
os.flush()
|
||||||
|
|
||||||
String code = readTillRN(is)
|
String code = readTillRN(is)
|
||||||
if (!code.startsWith("200"))
|
if (!code.startsWith("200"))
|
||||||
throw new IOException("unknown code $code")
|
throw new IOException("unknown code $code")
|
||||||
|
|
||||||
// parse all headers
|
// parse all headers
|
||||||
Set<String> headers = new HashSet<>()
|
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)
|
||||||
headers.add(header)
|
headers.add(header)
|
||||||
|
|
||||||
long receivedStart = -1
|
long receivedStart = -1
|
||||||
long receivedEnd = -1
|
long receivedEnd = -1
|
||||||
for (String receivedHeader : headers) {
|
for (String receivedHeader : headers) {
|
||||||
@@ -58,10 +58,10 @@ class HashListSession {
|
|||||||
receivedStart = Long.parseLong(group[0][1])
|
receivedStart = Long.parseLong(group[0][1])
|
||||||
receivedEnd = Long.parseLong(group[0][2])
|
receivedEnd = Long.parseLong(group[0][2])
|
||||||
}
|
}
|
||||||
|
|
||||||
if (receivedStart != 0)
|
if (receivedStart != 0)
|
||||||
throw new IOException("hashlist started at $receivedStart")
|
throw new IOException("hashlist started at $receivedStart")
|
||||||
|
|
||||||
byte[] hashList = new byte[receivedEnd]
|
byte[] hashList = new byte[receivedEnd]
|
||||||
ByteBuffer hashListBuf = ByteBuffer.wrap(hashList)
|
ByteBuffer hashListBuf = ByteBuffer.wrap(hashList)
|
||||||
byte[] tmp = new byte[0x1 << 13]
|
byte[] tmp = new byte[0x1 << 13]
|
||||||
@@ -73,7 +73,7 @@ class HashListSession {
|
|||||||
throw new IOException()
|
throw new IOException()
|
||||||
hashListBuf.put(tmp, 0, read)
|
hashListBuf.put(tmp, 0, read)
|
||||||
}
|
}
|
||||||
|
|
||||||
InfoHash received = InfoHash.fromHashList(hashList)
|
InfoHash received = InfoHash.fromHashList(hashList)
|
||||||
if (received.getRoot() != infoHash.getRoot())
|
if (received.getRoot() != infoHash.getRoot())
|
||||||
throw new IOException("fetched list doesn't match root")
|
throw new IOException("fetched list doesn't match root")
|
||||||
|
@@ -1,117 +1,64 @@
|
|||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
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 int donePieces() {
|
|
||||||
done.cardinality()
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized boolean isDownloaded(int piece) {
|
|
||||||
done.get(piece)
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized void clearAll() {
|
|
||||||
done.clear()
|
|
||||||
claimed.clear()
|
|
||||||
partials.clear()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized void write(PrintWriter writer) {
|
synchronized boolean isMarked(int piece) {
|
||||||
for (int i = done.nextSetBit(0); i >= 0; i = done.nextSetBit(i+1)) {
|
bitSet.get(piece)
|
||||||
writer.println(i)
|
}
|
||||||
}
|
|
||||||
partials.each { piece, position ->
|
synchronized int donePieces() {
|
||||||
writer.println("$piece,$position")
|
bitSet.cardinality()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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
|
|
||||||
}
|
|
@@ -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
|
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +0,0 @@
|
|||||||
package com.muwire.core.download
|
|
||||||
|
|
||||||
import com.muwire.core.Event
|
|
||||||
|
|
||||||
class UIDownloadPausedEvent extends Event {
|
|
||||||
}
|
|
@@ -1,6 +0,0 @@
|
|||||||
package com.muwire.core.download
|
|
||||||
|
|
||||||
import com.muwire.core.Event
|
|
||||||
|
|
||||||
class UIDownloadResumedEvent extends Event {
|
|
||||||
}
|
|
@@ -1,7 +0,0 @@
|
|||||||
package com.muwire.core.files
|
|
||||||
|
|
||||||
import com.muwire.core.Event
|
|
||||||
|
|
||||||
class DirectoryUnsharedEvent extends Event {
|
|
||||||
File directory
|
|
||||||
}
|
|
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -8,5 +8,5 @@ import net.i2p.data.Destination
|
|||||||
|
|
||||||
class FileDownloadedEvent extends Event {
|
class FileDownloadedEvent extends Event {
|
||||||
Downloader downloader
|
Downloader downloader
|
||||||
DownloadedFile downloadedFile
|
DownloadedFile downloadedFile
|
||||||
}
|
}
|
||||||
|
@@ -5,12 +5,6 @@ import com.muwire.core.SharedFile
|
|||||||
|
|
||||||
class FileHashedEvent extends Event {
|
class FileHashedEvent extends Event {
|
||||||
|
|
||||||
SharedFile sharedFile
|
SharedFile sharedFile
|
||||||
String error
|
String error
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
super.toString() + " sharedFile " + sharedFile?.file.getAbsolutePath() + " error: $error"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
|
||||||
@@ -13,75 +12,72 @@ import java.security.NoSuchAlgorithmException
|
|||||||
|
|
||||||
class FileHasher {
|
class FileHasher {
|
||||||
|
|
||||||
/** max size of shared file is 128 GB */
|
/** max size of shared file is 128 GB */
|
||||||
public static final long MAX_SIZE = 0x1L << 37
|
public static final long MAX_SIZE = 0x1L << 37
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @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) {
|
||||||
*/
|
if (size <= 0x1 << 30)
|
||||||
static int getPieceSize(long size) {
|
return 17
|
||||||
if (size <= 0x1 << 30)
|
|
||||||
return 17
|
for (int i = 31; i <= 37; i++) {
|
||||||
|
if (size <= 0x1L << i) {
|
||||||
for (int i = 31; i <= 37; i++) {
|
return i-13
|
||||||
if (size <= 0x1L << i) {
|
}
|
||||||
return i-13
|
}
|
||||||
}
|
|
||||||
}
|
throw new IllegalArgumentException("File too large $size")
|
||||||
|
}
|
||||||
throw new IllegalArgumentException("File too large $size")
|
|
||||||
}
|
final MessageDigest digest
|
||||||
|
|
||||||
final MessageDigest digest
|
FileHasher() {
|
||||||
|
try {
|
||||||
FileHasher() {
|
digest = MessageDigest.getInstance("SHA-256")
|
||||||
try {
|
} catch (NoSuchAlgorithmException impossible) {
|
||||||
digest = MessageDigest.getInstance("SHA-256")
|
digest = null
|
||||||
} catch (NoSuchAlgorithmException impossible) {
|
System.exit(1)
|
||||||
digest = null
|
}
|
||||||
System.exit(1)
|
}
|
||||||
}
|
|
||||||
}
|
InfoHash hashFile(File file) {
|
||||||
|
final long length = file.length()
|
||||||
InfoHash hashFile(File file) {
|
final int size = 0x1 << getPieceSize(length)
|
||||||
final long length = file.length()
|
int numPieces = (int) (length / size)
|
||||||
final int size = 0x1 << getPieceSize(length)
|
if (numPieces * size < length)
|
||||||
int numPieces = (int) (length / size)
|
numPieces++
|
||||||
if (numPieces * size < length)
|
|
||||||
numPieces++
|
def output = new ByteArrayOutputStream()
|
||||||
|
RandomAccessFile raf = new RandomAccessFile(file, "r")
|
||||||
def output = new ByteArrayOutputStream()
|
try {
|
||||||
RandomAccessFile raf = new RandomAccessFile(file, "r")
|
MappedByteBuffer buf
|
||||||
try {
|
for (int i = 0; i < numPieces - 1; i++) {
|
||||||
MappedByteBuffer buf
|
buf = raf.getChannel().map(MapMode.READ_ONLY, ((long)size) * i, size)
|
||||||
for (int i = 0; i < numPieces - 1; i++) {
|
digest.update buf
|
||||||
buf = raf.getChannel().map(MapMode.READ_ONLY, ((long)size) * i, size)
|
output.write(digest.digest(), 0, 32)
|
||||||
digest.update buf
|
}
|
||||||
DataUtil.tryUnmap(buf)
|
def lastPieceLength = length - (numPieces - 1) * ((long)size)
|
||||||
output.write(digest.digest(), 0, 32)
|
buf = raf.getChannel().map(MapMode.READ_ONLY, length - lastPieceLength, lastPieceLength)
|
||||||
}
|
digest.update buf
|
||||||
def lastPieceLength = length - (numPieces - 1) * ((long)size)
|
output.write(digest.digest(), 0, 32)
|
||||||
buf = raf.getChannel().map(MapMode.READ_ONLY, length - lastPieceLength, lastPieceLength)
|
} finally {
|
||||||
digest.update buf
|
raf.close()
|
||||||
output.write(digest.digest(), 0, 32)
|
}
|
||||||
} finally {
|
|
||||||
raf.close()
|
byte [] hashList = output.toByteArray()
|
||||||
}
|
InfoHash.fromHashList(hashList)
|
||||||
|
}
|
||||||
byte [] hashList = output.toByteArray()
|
|
||||||
InfoHash.fromHashList(hashList)
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
if (args.length != 1) {
|
if (args.length != 1) {
|
||||||
println "This utility computes an infohash of a file"
|
println "This utility computes an infohash of a file"
|
||||||
println "Pass absolute path to a file as an argument"
|
println "Pass absolute path to a file as an argument"
|
||||||
System.exit(1)
|
System.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
def file = new File(args[0])
|
def file = new File(args[0])
|
||||||
file = file.getAbsoluteFile()
|
file = file.getAbsoluteFile()
|
||||||
def hasher = new FileHasher()
|
def hasher = new FileHasher()
|
||||||
|
@@ -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()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -5,5 +5,5 @@ import com.muwire.core.SharedFile
|
|||||||
|
|
||||||
class FileLoadedEvent extends Event {
|
class FileLoadedEvent extends Event {
|
||||||
|
|
||||||
SharedFile loadedFile
|
SharedFile loadedFile
|
||||||
}
|
}
|
||||||
|
@@ -8,199 +8,131 @@ 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 {
|
||||||
|
|
||||||
|
|
||||||
final EventBus eventBus
|
final EventBus eventBus
|
||||||
final MuWireSettings settings
|
final MuWireSettings settings
|
||||||
final Map<InfoHash, Set<SharedFile>> rootToFiles = Collections.synchronizedMap(new HashMap<>())
|
final Map<InfoHash, Set<SharedFile>> rootToFiles = Collections.synchronizedMap(new HashMap<>())
|
||||||
final Map<File, SharedFile> fileToSharedFile = Collections.synchronizedMap(new HashMap<>())
|
final Map<File, SharedFile> fileToSharedFile = Collections.synchronizedMap(new HashMap<>())
|
||||||
final Map<String, Set<File>> nameToFiles = new HashMap<>()
|
final Map<String, Set<File>> nameToFiles = new HashMap<>()
|
||||||
final 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) {
|
|
||||||
this.settings = settings
|
this.settings = settings
|
||||||
this.eventBus = eventBus
|
this.eventBus = eventBus
|
||||||
}
|
}
|
||||||
|
|
||||||
void onFileHashedEvent(FileHashedEvent e) {
|
void onFileHashedEvent(FileHashedEvent e) {
|
||||||
if (e.sharedFile != null)
|
if (e.sharedFile != null)
|
||||||
addToIndex(e.sharedFile)
|
addToIndex(e.sharedFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
void onFileLoadedEvent(FileLoadedEvent e) {
|
void onFileLoadedEvent(FileLoadedEvent e) {
|
||||||
addToIndex(e.loadedFile)
|
addToIndex(e.loadedFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
void onFileDownloadedEvent(FileDownloadedEvent e) {
|
void onFileDownloadedEvent(FileDownloadedEvent e) {
|
||||||
if (settings.shareDownloadedFiles) {
|
if (settings.shareDownloadedFiles) {
|
||||||
addToIndex(e.downloadedFile)
|
addToIndex(e.downloadedFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addToIndex(SharedFile sf) {
|
private void addToIndex(SharedFile sf) {
|
||||||
log.info("Adding shared file " + sf.getFile())
|
log.info("Adding shared file " + sf.getFile())
|
||||||
InfoHash infoHash = sf.getInfoHash()
|
InfoHash infoHash = sf.getInfoHash()
|
||||||
Set<SharedFile> existing = rootToFiles.get(infoHash)
|
Set<SharedFile> existing = rootToFiles.get(infoHash)
|
||||||
if (existing == null) {
|
if (existing == null) {
|
||||||
log.info("adding new root")
|
log.info("adding new root")
|
||||||
existing = new HashSet<>()
|
existing = new HashSet<>()
|
||||||
rootToFiles.put(infoHash, existing);
|
rootToFiles.put(infoHash, existing);
|
||||||
}
|
}
|
||||||
existing.add(sf)
|
existing.add(sf)
|
||||||
fileToSharedFile.put(sf.file, sf)
|
fileToSharedFile.put(sf.file, sf)
|
||||||
|
|
||||||
String name = sf.getFile().getName()
|
String name = sf.getFile().getName()
|
||||||
Set<File> existingFiles = nameToFiles.get(name)
|
Set<File> existingFiles = nameToFiles.get(name)
|
||||||
if (existingFiles == null) {
|
if (existingFiles == null) {
|
||||||
existingFiles = new HashSet<>()
|
existingFiles = new HashSet<>()
|
||||||
nameToFiles.put(name, existingFiles)
|
nameToFiles.put(name, existingFiles)
|
||||||
}
|
}
|
||||||
existingFiles.add(sf.getFile())
|
existingFiles.add(sf.getFile())
|
||||||
|
|
||||||
String comment = sf.getComment()
|
index.add(name)
|
||||||
if (comment != null) {
|
}
|
||||||
comment = DataUtil.readi18nString(Base64.decode(comment))
|
|
||||||
index.add(comment)
|
void onFileUnsharedEvent(FileUnsharedEvent e) {
|
||||||
Set<File> existingComment = commentToFile.get(comment)
|
SharedFile sf = e.unsharedFile
|
||||||
if(existingComment == null) {
|
InfoHash infoHash = sf.getInfoHash()
|
||||||
existingComment = new HashSet<>()
|
Set<SharedFile> existing = rootToFiles.get(infoHash)
|
||||||
commentToFile.put(comment, existingComment)
|
if (existing != null) {
|
||||||
}
|
existing.remove(sf)
|
||||||
existingComment.add(sf.getFile())
|
if (existing.isEmpty()) {
|
||||||
}
|
rootToFiles.remove(infoHash)
|
||||||
|
}
|
||||||
index.add(name)
|
}
|
||||||
}
|
|
||||||
|
fileToSharedFile.remove(sf.file)
|
||||||
void onFileUnsharedEvent(FileUnsharedEvent e) {
|
|
||||||
SharedFile sf = e.unsharedFile
|
String name = sf.getFile().getName()
|
||||||
InfoHash infoHash = sf.getInfoHash()
|
Set<File> existingFiles = nameToFiles.get(name)
|
||||||
Set<SharedFile> existing = rootToFiles.get(infoHash)
|
if (existingFiles != null) {
|
||||||
if (existing != null) {
|
existingFiles.remove(sf.file)
|
||||||
existing.remove(sf)
|
if (existingFiles.isEmpty()) {
|
||||||
if (existing.isEmpty()) {
|
nameToFiles.remove(name)
|
||||||
rootToFiles.remove(infoHash)
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
index.remove(name)
|
||||||
fileToSharedFile.remove(sf.file)
|
}
|
||||||
|
|
||||||
String name = sf.getFile().getName()
|
Map<File, SharedFile> getSharedFiles() {
|
||||||
Set<File> existingFiles = nameToFiles.get(name)
|
|
||||||
if (existingFiles != null) {
|
|
||||||
existingFiles.remove(sf.file)
|
|
||||||
if (existingFiles.isEmpty()) {
|
|
||||||
nameToFiles.remove(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
|
||||||
synchronized(fileToSharedFile) {
|
synchronized(fileToSharedFile) {
|
||||||
return new HashMap<>(fileToSharedFile)
|
return new HashMap<>(fileToSharedFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<SharedFile> getSharedFiles(byte []root) {
|
Set<SharedFile> getSharedFiles(byte []root) {
|
||||||
return rootToFiles.get(new InfoHash(root))
|
return rootToFiles.get(new InfoHash(root))
|
||||||
}
|
}
|
||||||
|
|
||||||
void onSearchEvent(SearchEvent e) {
|
void onSearchEvent(SearchEvent e) {
|
||||||
// hash takes precedence
|
// hash takes precedence
|
||||||
ResultsEvent re = null
|
ResultsEvent re = null
|
||||||
if (e.searchHash != null) {
|
if (e.searchHash != null) {
|
||||||
Set<SharedFile> found
|
Set<SharedFile> found
|
||||||
found = rootToFiles.get new InfoHash(e.searchHash)
|
found = rootToFiles.get new InfoHash(e.searchHash)
|
||||||
found = filter(found, e.oobInfohash)
|
found = filter(found, e.oobInfohash)
|
||||||
if (found != null && !found.isEmpty())
|
if (found != null && !found.isEmpty())
|
||||||
re = new ResultsEvent(results: found.asList(), uuid: e.uuid, searchEvent: e)
|
re = new ResultsEvent(results: found.asList(), uuid: e.uuid, searchEvent: e)
|
||||||
} 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, [])
|
Set<SharedFile> sharedFiles = new HashSet<>()
|
||||||
if (e.searchComments)
|
files.each { sharedFiles.add fileToSharedFile[it] }
|
||||||
files.addAll commentToFile.getOrDefault(it, [])
|
|
||||||
}
|
|
||||||
Set<SharedFile> sharedFiles = new HashSet<>()
|
|
||||||
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)
|
|
||||||
|
}
|
||||||
}
|
|
||||||
|
if (re != null)
|
||||||
if (re != null)
|
eventBus.publish(re)
|
||||||
eventBus.publish(re)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private static Set<SharedFile> filter(Set<SharedFile> files, boolean oob) {
|
private static Set<SharedFile> filter(Set<SharedFile> files, boolean oob) {
|
||||||
if (!oob)
|
if (!oob)
|
||||||
return files
|
return files
|
||||||
Set<SharedFile> rv = new HashSet<>()
|
Set<SharedFile> rv = new HashSet<>()
|
||||||
files.each {
|
files.each {
|
||||||
if (it.getPieceSize() != 0)
|
if (it.getPieceSize() != 0)
|
||||||
rv.add(it)
|
rv.add(it)
|
||||||
}
|
}
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -4,10 +4,5 @@ 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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -4,5 +4,5 @@ import com.muwire.core.Event
|
|||||||
import com.muwire.core.SharedFile
|
import com.muwire.core.SharedFile
|
||||||
|
|
||||||
class FileUnsharedEvent extends Event {
|
class FileUnsharedEvent extends Event {
|
||||||
SharedFile unsharedFile
|
SharedFile unsharedFile
|
||||||
}
|
}
|
||||||
|
@@ -4,60 +4,44 @@ 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 {
|
||||||
|
|
||||||
final FileHasher hasher
|
final FileHasher hasher
|
||||||
final EventBus eventBus
|
final EventBus eventBus
|
||||||
final FileManager fileManager
|
final FileManager fileManager
|
||||||
final Set<File> hashed = new HashSet<>()
|
Executor executor
|
||||||
final MuWireSettings settings
|
|
||||||
Executor executor
|
HasherService(FileHasher hasher, EventBus eventBus, FileManager fileManager) {
|
||||||
|
this.hasher = hasher
|
||||||
HasherService(FileHasher hasher, EventBus eventBus, FileManager fileManager, MuWireSettings settings) {
|
this.eventBus = eventBus
|
||||||
this.hasher = hasher
|
|
||||||
this.eventBus = eventBus
|
|
||||||
this.fileManager = fileManager
|
this.fileManager = fileManager
|
||||||
this.settings = settings
|
}
|
||||||
}
|
|
||||||
|
void start() {
|
||||||
void start() {
|
executor = Executors.newSingleThreadExecutor()
|
||||||
executor = Executors.newSingleThreadExecutor()
|
}
|
||||||
}
|
|
||||||
|
void onFileSharedEvent(FileSharedEvent evt) {
|
||||||
void onFileSharedEvent(FileSharedEvent evt) {
|
if (fileManager.fileToSharedFile.containsKey(evt.file))
|
||||||
File canonical = evt.file.getCanonicalFile()
|
|
||||||
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)
|
private void process(File f) {
|
||||||
}
|
f = f.getCanonicalFile()
|
||||||
|
if (f.isDirectory()) {
|
||||||
void onFileUnsharedEvent(FileUnsharedEvent evt) {
|
f.listFiles().each {onFileSharedEvent new FileSharedEvent(file: it) }
|
||||||
hashed.remove(evt.unsharedFile.file)
|
} else {
|
||||||
}
|
if (f.length() == 0) {
|
||||||
|
eventBus.publish new FileHashedEvent(error: "Not sharing empty file $f")
|
||||||
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent evt) {
|
} else if (f.length() > FileHasher.MAX_SIZE) {
|
||||||
hashed.remove(evt.directory)
|
eventBus.publish new FileHashedEvent(error: "$f is too large to be shared ${f.length()}")
|
||||||
}
|
} else {
|
||||||
|
def hash = hasher.hashFile f
|
||||||
private void process(File f) {
|
eventBus.publish new FileHashedEvent(sharedFile: new SharedFile(f, hash, FileHasher.getPieceSize(f.length())))
|
||||||
if (f.isDirectory()) {
|
}
|
||||||
f.listFiles().each {eventBus.publish new FileSharedEvent(file: it) }
|
}
|
||||||
} else {
|
}
|
||||||
if (f.length() == 0) {
|
|
||||||
eventBus.publish new FileHashedEvent(error: "Not sharing empty file $f")
|
|
||||||
} else if (f.length() > FileHasher.MAX_SIZE) {
|
|
||||||
eventBus.publish new FileHashedEvent(error: "$f is too large to be shared ${f.length()}")
|
|
||||||
} else {
|
|
||||||
eventBus.publish new FileHashingEvent(hashingFile: f)
|
|
||||||
def hash = hasher.hashFile f
|
|
||||||
eventBus.publish new FileHashedEvent(sharedFile: new SharedFile(f, hash, FileHasher.getPieceSize(f.length())))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
@@ -26,148 +22,135 @@ import net.i2p.data.Destination
|
|||||||
@Log
|
@Log
|
||||||
class PersisterService extends Service {
|
class PersisterService extends Service {
|
||||||
|
|
||||||
final File location
|
final File location
|
||||||
final EventBus listener
|
final EventBus listener
|
||||||
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")
|
PersisterService(File location, EventBus listener, int interval, FileManager fileManager) {
|
||||||
} as ThreadFactory)
|
this.location = location
|
||||||
|
this.listener = listener
|
||||||
PersisterService(File location, EventBus listener, int interval, FileManager fileManager) {
|
this.interval = interval
|
||||||
this.location = location
|
this.fileManager = fileManager
|
||||||
this.listener = listener
|
timer = new Timer("file persister", true)
|
||||||
this.interval = interval
|
}
|
||||||
this.fileManager = fileManager
|
|
||||||
timer = new Timer("file persister timer", true)
|
void start() {
|
||||||
}
|
timer.schedule({load()} as TimerTask, 1)
|
||||||
|
}
|
||||||
void stop() {
|
|
||||||
timer.cancel()
|
void stop() {
|
||||||
}
|
timer.cancel()
|
||||||
|
}
|
||||||
void onUILoadedEvent(UILoadedEvent e) {
|
|
||||||
timer.schedule({load()} as TimerTask, 1)
|
void load() {
|
||||||
}
|
if (location.exists() && location.isFile()) {
|
||||||
|
def slurper = new JsonSlurper()
|
||||||
void onUIPersistFilesEvent(UIPersistFilesEvent e) {
|
try {
|
||||||
persistFiles()
|
location.eachLine {
|
||||||
}
|
if (it.trim().length() > 0) {
|
||||||
|
def parsed = slurper.parseText it
|
||||||
void load() {
|
def event = fromJson parsed
|
||||||
Thread.currentThread().setPriority(Thread.MIN_PRIORITY)
|
if (event != null) {
|
||||||
|
|
||||||
if (location.exists() && location.isFile()) {
|
|
||||||
int loaded = 0
|
|
||||||
def slurper = new JsonSlurper()
|
|
||||||
try {
|
|
||||||
location.eachLine {
|
|
||||||
if (it.trim().length() > 0) {
|
|
||||||
def parsed = slurper.parseText it
|
|
||||||
def event = fromJson parsed
|
|
||||||
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)
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
listener.publish(new AllFilesLoadedEvent())
|
listener.publish(new AllFilesLoadedEvent())
|
||||||
} catch (IllegalArgumentException|NumberFormatException e) {
|
} catch (IllegalArgumentException|NumberFormatException e) {
|
||||||
log.log(Level.WARNING, "couldn't load files",e)
|
log.log(Level.WARNING, "couldn't load files",e)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
listener.publish(new AllFilesLoadedEvent())
|
listener.publish(new AllFilesLoadedEvent())
|
||||||
}
|
}
|
||||||
timer.schedule({persistFiles()} as TimerTask, 0, interval)
|
timer.schedule({persistFiles()} as TimerTask, 0, interval)
|
||||||
loaded = true
|
loaded = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private static FileLoadedEvent fromJson(def json) {
|
private static FileLoadedEvent fromJson(def json) {
|
||||||
if (json.file == null || json.length == null || json.infoHash == null || json.hashList == null)
|
if (json.file == null || json.length == null || json.infoHash == null || json.hashList == null)
|
||||||
throw new IllegalArgumentException()
|
throw new IllegalArgumentException()
|
||||||
if (!(json.hashList instanceof List))
|
if (!(json.hashList instanceof List))
|
||||||
throw new IllegalArgumentException()
|
throw new IllegalArgumentException()
|
||||||
|
|
||||||
def file = new File(DataUtil.readi18nString(Base64.decode(json.file)))
|
def file = new File(DataUtil.readi18nString(Base64.decode(json.file)))
|
||||||
file = file.getCanonicalFile()
|
file = file.getCanonicalFile()
|
||||||
if (!file.exists() || file.isDirectory())
|
if (!file.exists() || file.isDirectory())
|
||||||
return null
|
return null
|
||||||
long length = Long.valueOf(json.length)
|
long length = Long.valueOf(json.length)
|
||||||
if (length != file.length())
|
if (length != file.length())
|
||||||
return null
|
return null
|
||||||
|
|
||||||
List hashList = (List) json.hashList
|
List hashList = (List) json.hashList
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
||||||
hashList.each {
|
hashList.each {
|
||||||
byte [] hash = Base64.decode it.toString()
|
byte [] hash = Base64.decode it.toString()
|
||||||
if (hash == null)
|
if (hash == null)
|
||||||
throw new IllegalArgumentException()
|
throw new IllegalArgumentException()
|
||||||
baos.write hash
|
baos.write hash
|
||||||
}
|
}
|
||||||
byte[] hashListBytes = baos.toByteArray()
|
byte[] hashListBytes = baos.toByteArray()
|
||||||
|
|
||||||
InfoHash ih = InfoHash.fromHashList(hashListBytes)
|
InfoHash ih = InfoHash.fromHashList(hashListBytes)
|
||||||
byte [] root = Base64.decode(json.infoHash.toString())
|
byte [] root = Base64.decode(json.infoHash.toString())
|
||||||
if (root == null)
|
if (root == null)
|
||||||
throw new IllegalArgumentException()
|
throw new IllegalArgumentException()
|
||||||
if (!Arrays.equals(root, ih.getRoot()))
|
if (!Arrays.equals(root, ih.getRoot()))
|
||||||
return null
|
return null
|
||||||
|
|
||||||
int pieceSize = 0
|
int pieceSize = 0
|
||||||
if (json.pieceSize != null)
|
if (json.pieceSize != null)
|
||||||
pieceSize = json.pieceSize
|
pieceSize = json.pieceSize
|
||||||
|
|
||||||
if (json.sources != null) {
|
if (json.sources != null) {
|
||||||
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)
|
return new FileLoadedEvent(loadedFile: sf)
|
||||||
sf.setComment(json.comment)
|
|
||||||
return new FileLoadedEvent(loadedFile: sf)
|
}
|
||||||
|
|
||||||
}
|
private void persistFiles() {
|
||||||
|
def sharedFiles = fileManager.getSharedFiles()
|
||||||
private void persistFiles() {
|
|
||||||
persisterExecutor.submit( {
|
File tmp = File.createTempFile("muwire-files", "tmp")
|
||||||
def sharedFiles = fileManager.getSharedFiles()
|
tmp.deleteOnExit()
|
||||||
|
tmp.withPrintWriter { writer ->
|
||||||
File tmp = File.createTempFile("muwire-files", "tmp")
|
sharedFiles.each { k, v ->
|
||||||
tmp.deleteOnExit()
|
def json = toJson(k,v)
|
||||||
tmp.withPrintWriter { writer ->
|
json = JsonOutput.toJson(json)
|
||||||
sharedFiles.each { k, v ->
|
writer.println json
|
||||||
def json = toJson(k,v)
|
}
|
||||||
json = JsonOutput.toJson(json)
|
}
|
||||||
writer.println json
|
Files.copy(tmp.toPath(), location.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||||
}
|
tmp.delete()
|
||||||
}
|
}
|
||||||
Files.copy(tmp.toPath(), location.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
|
||||||
tmp.delete()
|
private def toJson(File f, SharedFile sf) {
|
||||||
} as Runnable)
|
def json = [:]
|
||||||
}
|
json.file = Base64.encode DataUtil.encodei18nString(f.getCanonicalFile().toString())
|
||||||
|
json.length = f.length()
|
||||||
private def toJson(File f, SharedFile sf) {
|
InfoHash ih = sf.getInfoHash()
|
||||||
def json = [:]
|
json.infoHash = Base64.encode ih.getRoot()
|
||||||
json.file = sf.getB64EncodedFileName()
|
|
||||||
json.length = sf.getCachedLength()
|
|
||||||
InfoHash ih = sf.getInfoHash()
|
|
||||||
json.infoHash = sf.getB64EncodedHashRoot()
|
|
||||||
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++) {
|
||||||
if (sf instanceof DownloadedFile) {
|
System.arraycopy(ih.getHashList(), i * 32, tmp, 0, 32)
|
||||||
json.sources = sf.sources.stream().map( {d -> d.toBase64()}).collect(Collectors.toList())
|
json.hashList.add Base64.encode(tmp)
|
||||||
}
|
}
|
||||||
|
|
||||||
json
|
if (sf instanceof DownloadedFile) {
|
||||||
}
|
json.sources = sf.sources.stream().map( {d -> d.toBase64()}).collect(Collectors.toList())
|
||||||
|
}
|
||||||
|
|
||||||
|
json
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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
|
|
||||||
}
|
|
@@ -1,6 +0,0 @@
|
|||||||
package com.muwire.core.files
|
|
||||||
|
|
||||||
import com.muwire.core.Event
|
|
||||||
|
|
||||||
class UIPersistFilesEvent extends Event {
|
|
||||||
}
|
|
@@ -17,177 +17,177 @@ import net.i2p.data.Destination
|
|||||||
|
|
||||||
@Log
|
@Log
|
||||||
class CacheClient {
|
class CacheClient {
|
||||||
|
|
||||||
|
private static final int CRAWLER_RETURN = 10
|
||||||
|
|
||||||
|
final EventBus eventBus
|
||||||
|
final HostCache cache
|
||||||
|
final ConnectionManager manager
|
||||||
|
final I2PSession session
|
||||||
|
final long interval
|
||||||
|
final MuWireSettings settings
|
||||||
|
final Timer timer
|
||||||
|
|
||||||
private static final int CRAWLER_RETURN = 10
|
public CacheClient(EventBus eventBus, HostCache cache,
|
||||||
|
ConnectionManager manager, I2PSession session,
|
||||||
|
MuWireSettings settings, long interval) {
|
||||||
|
this.eventBus = eventBus
|
||||||
|
this.cache = cache
|
||||||
|
this.manager = manager
|
||||||
|
this.session = session
|
||||||
|
this.settings = settings
|
||||||
|
this.interval = interval
|
||||||
|
this.timer = new Timer("hostcache-client",true)
|
||||||
|
}
|
||||||
|
|
||||||
|
void start() {
|
||||||
|
session.addMuxedSessionListener(new Listener(), I2PSession.PROTO_DATAGRAM, 0)
|
||||||
|
timer.schedule({queryIfNeeded()} as TimerTask, 1, interval)
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop() {
|
||||||
|
timer.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
private void queryIfNeeded() {
|
||||||
|
if (!manager.getConnections().isEmpty())
|
||||||
|
return
|
||||||
|
if (!cache.getHosts(1).isEmpty())
|
||||||
|
return
|
||||||
|
|
||||||
|
log.info "Will query hostcaches"
|
||||||
|
|
||||||
|
def ping = [type: "Ping", version: 1, leaf: settings.isLeaf()]
|
||||||
|
ping = JsonOutput.toJson(ping)
|
||||||
|
def maker = new I2PDatagramMaker(session)
|
||||||
|
ping = maker.makeI2PDatagram(ping.bytes)
|
||||||
|
def options = new SendMessageOptions()
|
||||||
|
options.setSendLeaseSet(true)
|
||||||
|
CacheServers.getCacheServers().each {
|
||||||
|
log.info "Querying hostcache ${it.toBase32()}"
|
||||||
|
session.sendMessage(it, ping, 0, ping.length, I2PSession.PROTO_DATAGRAM, 1, 0, options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Listener implements I2PSessionMuxedListener {
|
||||||
|
|
||||||
|
private final JsonSlurper slurper = new JsonSlurper()
|
||||||
|
|
||||||
final EventBus eventBus
|
@Override
|
||||||
final HostCache cache
|
public void messageAvailable(I2PSession session, int msgId, long size) {
|
||||||
final ConnectionManager manager
|
}
|
||||||
final I2PSession session
|
|
||||||
final long interval
|
|
||||||
final MuWireSettings settings
|
|
||||||
final Timer timer
|
|
||||||
|
|
||||||
public CacheClient(EventBus eventBus, HostCache cache,
|
@Override
|
||||||
ConnectionManager manager, I2PSession session,
|
public void messageAvailable(I2PSession session, int msgId, long size, int proto, int fromport, int toport) {
|
||||||
MuWireSettings settings, long interval) {
|
|
||||||
this.eventBus = eventBus
|
if (proto != I2PSession.PROTO_DATAGRAM) {
|
||||||
this.cache = cache
|
log.warning "Received unexpected protocol $proto"
|
||||||
this.manager = manager
|
return
|
||||||
this.session = session
|
}
|
||||||
this.settings = settings
|
|
||||||
this.interval = interval
|
def payload = session.receiveMessage(msgId)
|
||||||
this.timer = new Timer("hostcache-client",true)
|
def dissector = new I2PDatagramDissector()
|
||||||
}
|
try {
|
||||||
|
dissector.loadI2PDatagram(payload)
|
||||||
|
def sender = dissector.getSender()
|
||||||
|
log.info("Received something from ${sender.toBase32()}")
|
||||||
|
|
||||||
|
payload = dissector.getPayload()
|
||||||
|
payload = slurper.parse(payload)
|
||||||
|
|
||||||
|
if (payload.type == null) {
|
||||||
|
log.warning("type missing")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(payload.type) {
|
||||||
|
case "Pong" : handlePong(sender, payload); break
|
||||||
|
case "CrawlerPing": handleCrawlerPing(session, sender, payload); break
|
||||||
|
default : log.warning("unknown type ${payload.type}")
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warning("Invalid datagram $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void start() {
|
@Override
|
||||||
session.addMuxedSessionListener(new Listener(), I2PSession.PROTO_DATAGRAM, 0)
|
public void reportAbuse(I2PSession session, int severity) {
|
||||||
timer.schedule({queryIfNeeded()} as TimerTask, 1, interval)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void stop() {
|
@Override
|
||||||
timer.cancel()
|
public void disconnected(I2PSession session) {
|
||||||
}
|
log.severe "I2P session disconnected"
|
||||||
|
}
|
||||||
|
|
||||||
private void queryIfNeeded() {
|
@Override
|
||||||
if (!manager.getConnections().isEmpty())
|
public void errorOccurred(I2PSession session, String message, Throwable error) {
|
||||||
return
|
log.severe "I2P error occured $message $error"
|
||||||
if (!cache.getHosts(1).isEmpty())
|
}
|
||||||
return
|
|
||||||
|
}
|
||||||
log.info "Will query hostcaches"
|
|
||||||
|
private void handlePong(Destination from, def pong) {
|
||||||
def ping = [type: "Ping", version: 1, leaf: settings.isLeaf()]
|
if (!CacheServers.isRegistered(from)) {
|
||||||
ping = JsonOutput.toJson(ping)
|
log.warning("received pong from non-registered destination")
|
||||||
def maker = new I2PDatagramMaker(session)
|
return
|
||||||
ping = maker.makeI2PDatagram(ping.bytes)
|
}
|
||||||
def options = new SendMessageOptions()
|
|
||||||
options.setSendLeaseSet(true)
|
if (pong.pongs == null) {
|
||||||
CacheServers.getCacheServers().each {
|
log.warning("malformed pong - no pongs")
|
||||||
log.info "Querying hostcache ${it.toBase32()}"
|
return
|
||||||
session.sendMessage(it, ping, 0, ping.length, I2PSession.PROTO_DATAGRAM, 1, 0, options)
|
}
|
||||||
}
|
|
||||||
}
|
pong.pongs.asList().each {
|
||||||
|
Destination dest = new Destination(it)
|
||||||
class Listener implements I2PSessionMuxedListener {
|
if (!session.getMyDestination().equals(dest))
|
||||||
|
eventBus.publish(new HostDiscoveredEvent(destination: dest, fromHostcache : true))
|
||||||
private final JsonSlurper slurper = new JsonSlurper()
|
}
|
||||||
|
|
||||||
@Override
|
}
|
||||||
public void messageAvailable(I2PSession session, int msgId, long size) {
|
|
||||||
}
|
private void handleCrawlerPing(I2PSession session, Destination from, def ping) {
|
||||||
|
if (settings.isLeaf()) {
|
||||||
@Override
|
log.warning("Received crawler ping but I'm a leaf")
|
||||||
public void messageAvailable(I2PSession session, int msgId, long size, int proto, int fromport, int toport) {
|
return
|
||||||
|
}
|
||||||
if (proto != I2PSession.PROTO_DATAGRAM) {
|
|
||||||
log.warning "Received unexpected protocol $proto"
|
switch(settings.getCrawlerResponse()) {
|
||||||
return
|
case CrawlerResponse.NONE:
|
||||||
}
|
log.info("Responding to crawlers is disabled by user")
|
||||||
|
break
|
||||||
def payload = session.receiveMessage(msgId)
|
case CrawlerResponse.ALL:
|
||||||
def dissector = new I2PDatagramDissector()
|
respondToCrawler(session, from, ping)
|
||||||
try {
|
break;
|
||||||
dissector.loadI2PDatagram(payload)
|
case CrawlerResponse.REGISTERED:
|
||||||
def sender = dissector.getSender()
|
if (CacheServers.isRegistered(from))
|
||||||
log.info("Received something from ${sender.toBase32()}")
|
respondToCrawler(session, from, ping)
|
||||||
|
else
|
||||||
payload = dissector.getPayload()
|
log.warning("Ignoring crawler ping from non-registered crawler")
|
||||||
payload = slurper.parse(payload)
|
break
|
||||||
|
}
|
||||||
if (payload.type == null) {
|
}
|
||||||
log.warning("type missing")
|
|
||||||
return
|
private void respondToCrawler(I2PSession session, Destination from, def ping) {
|
||||||
}
|
log.info "responding to crawler ping"
|
||||||
|
|
||||||
switch(payload.type) {
|
def neighbors = manager.getConnections().collect { c -> c.endpoint.destination.toBase64() }
|
||||||
case "Pong" : handlePong(sender, payload); break
|
Collections.shuffle(neighbors)
|
||||||
case "CrawlerPing": handleCrawlerPing(session, sender, payload); break
|
if (neighbors.size() > CRAWLER_RETURN)
|
||||||
default : log.warning("unknown type ${payload.type}")
|
neighbors = neighbors[0..CRAWLER_RETURN - 1]
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
def upManager = (UltrapeerConnectionManager) manager;
|
||||||
log.warning("Invalid datagram $e")
|
def pong = [:]
|
||||||
}
|
pong.peers = neighbors
|
||||||
}
|
pong.uuid = ping.uuid
|
||||||
|
pong.type = "CrawlerPong"
|
||||||
@Override
|
pong.version = 1
|
||||||
public void reportAbuse(I2PSession session, int severity) {
|
pong.leafSlots = upManager.hasLeafSlots()
|
||||||
}
|
pong.peerSlots = upManager.hasPeerSlots()
|
||||||
|
pong = JsonOutput.toJson(pong)
|
||||||
@Override
|
|
||||||
public void disconnected(I2PSession session) {
|
def maker = new I2PDatagramMaker(session)
|
||||||
log.severe "I2P session disconnected"
|
pong = maker.makeI2PDatagram(pong.bytes)
|
||||||
}
|
session.sendMessage(from, pong, I2PSession.PROTO_DATAGRAM, 0, 0)
|
||||||
|
}
|
||||||
@Override
|
|
||||||
public void errorOccurred(I2PSession session, String message, Throwable error) {
|
|
||||||
log.severe "I2P error occured $message $error"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handlePong(Destination from, def pong) {
|
|
||||||
if (!CacheServers.isRegistered(from)) {
|
|
||||||
log.warning("received pong from non-registered destination")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pong.pongs == null) {
|
|
||||||
log.warning("malformed pong - no pongs")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
pong.pongs.asList().each {
|
|
||||||
Destination dest = new Destination(it)
|
|
||||||
if (!session.getMyDestination().equals(dest))
|
|
||||||
eventBus.publish(new HostDiscoveredEvent(destination: dest, fromHostcache : true))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleCrawlerPing(I2PSession session, Destination from, def ping) {
|
|
||||||
if (settings.isLeaf()) {
|
|
||||||
log.warning("Received crawler ping but I'm a leaf")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch(settings.getCrawlerResponse()) {
|
|
||||||
case CrawlerResponse.NONE:
|
|
||||||
log.info("Responding to crawlers is disabled by user")
|
|
||||||
break
|
|
||||||
case CrawlerResponse.ALL:
|
|
||||||
respondToCrawler(session, from, ping)
|
|
||||||
break;
|
|
||||||
case CrawlerResponse.REGISTERED:
|
|
||||||
if (CacheServers.isRegistered(from))
|
|
||||||
respondToCrawler(session, from, ping)
|
|
||||||
else
|
|
||||||
log.warning("Ignoring crawler ping from non-registered crawler")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void respondToCrawler(I2PSession session, Destination from, def ping) {
|
|
||||||
log.info "responding to crawler ping"
|
|
||||||
|
|
||||||
def neighbors = manager.getConnections().collect { c -> c.endpoint.destination.toBase64() }
|
|
||||||
Collections.shuffle(neighbors)
|
|
||||||
if (neighbors.size() > CRAWLER_RETURN)
|
|
||||||
neighbors = neighbors[0..CRAWLER_RETURN - 1]
|
|
||||||
|
|
||||||
def upManager = (UltrapeerConnectionManager) manager;
|
|
||||||
def pong = [:]
|
|
||||||
pong.peers = neighbors
|
|
||||||
pong.uuid = ping.uuid
|
|
||||||
pong.type = "CrawlerPong"
|
|
||||||
pong.version = 1
|
|
||||||
pong.leafSlots = upManager.hasLeafSlots()
|
|
||||||
pong.peerSlots = upManager.hasPeerSlots()
|
|
||||||
pong = JsonOutput.toJson(pong)
|
|
||||||
|
|
||||||
def maker = new I2PDatagramMaker(session)
|
|
||||||
pong = maker.makeI2PDatagram(pong.bytes)
|
|
||||||
session.sendMessage(from, pong, I2PSession.PROTO_DATAGRAM, 0, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -4,25 +4,20 @@ import net.i2p.data.Destination
|
|||||||
|
|
||||||
class CacheServers {
|
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() {
|
||||||
List<Destination> allCaches = new ArrayList<>(CACHES)
|
List<Destination> allCaches = new ArrayList<>(CACHES)
|
||||||
Collections.shuffle(allCaches)
|
Collections.shuffle(allCaches)
|
||||||
if (allCaches.size() <= TO_GIVE)
|
if (allCaches.size() <= TO_GIVE)
|
||||||
return allCaches
|
return allCaches
|
||||||
allCaches[0..TO_GIVE-1]
|
allCaches[0..TO_GIVE-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
static boolean isRegistered(Destination d) {
|
static boolean isRegistered(Destination d) {
|
||||||
return CACHES.contains(d)
|
return CACHES.contains(d)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,67 +4,34 @@ import net.i2p.data.Destination
|
|||||||
|
|
||||||
class Host {
|
class Host {
|
||||||
|
|
||||||
private static final int MAX_FAILURES = 3
|
private static final int MAX_FAILURES = 3
|
||||||
|
|
||||||
|
final Destination destination
|
||||||
|
int failures,successes
|
||||||
|
|
||||||
|
public Host(Destination destination) {
|
||||||
|
this.destination = destination
|
||||||
|
}
|
||||||
|
|
||||||
final Destination destination
|
synchronized void onConnect() {
|
||||||
private final int clearInterval, hopelessInterval, rejectionInterval
|
failures = 0
|
||||||
int failures,successes
|
successes++
|
||||||
long lastAttempt
|
}
|
||||||
long lastSuccessfulAttempt
|
|
||||||
long lastRejection
|
synchronized void onFailure() {
|
||||||
|
failures++
|
||||||
public Host(Destination destination, int clearInterval, int hopelessInterval, int rejectionInterval) {
|
successes = 0
|
||||||
this.destination = destination
|
}
|
||||||
this.clearInterval = clearInterval
|
|
||||||
this.hopelessInterval = hopelessInterval
|
synchronized boolean isFailed() {
|
||||||
this.rejectionInterval = rejectionInterval
|
failures >= MAX_FAILURES
|
||||||
}
|
}
|
||||||
|
|
||||||
|
synchronized boolean hasSucceeded() {
|
||||||
|
successes > 0
|
||||||
|
}
|
||||||
|
|
||||||
private void connectSuccessful() {
|
|
||||||
failures = 0
|
|
||||||
successes++
|
|
||||||
lastAttempt = System.currentTimeMillis()
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized void onConnect() {
|
|
||||||
connectSuccessful()
|
|
||||||
lastSuccessfulAttempt = lastAttempt
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized void onReject() {
|
|
||||||
connectSuccessful()
|
|
||||||
lastRejection = lastAttempt;
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized void onFailure() {
|
|
||||||
failures++
|
|
||||||
successes = 0
|
|
||||||
lastAttempt = System.currentTimeMillis()
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized boolean isFailed() {
|
|
||||||
failures >= MAX_FAILURES
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized boolean hasSucceeded() {
|
|
||||||
successes > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -15,167 +15,138 @@ import net.i2p.data.Destination
|
|||||||
|
|
||||||
class HostCache extends Service {
|
class HostCache extends Service {
|
||||||
|
|
||||||
final TrustService trustService
|
final TrustService trustService
|
||||||
final File storage
|
final File storage
|
||||||
final int interval
|
final int interval
|
||||||
final Timer timer
|
final Timer timer
|
||||||
final MuWireSettings settings
|
final MuWireSettings settings
|
||||||
final Destination myself
|
final Destination myself
|
||||||
final Map<Destination, Host> hosts = new ConcurrentHashMap<>()
|
final Map<Destination, Host> hosts = new ConcurrentHashMap<>()
|
||||||
|
|
||||||
|
HostCache(){}
|
||||||
|
|
||||||
|
public HostCache(TrustService trustService, File storage, int interval,
|
||||||
|
MuWireSettings settings, Destination myself) {
|
||||||
|
this.trustService = trustService
|
||||||
|
this.storage = storage
|
||||||
|
this.interval = interval
|
||||||
|
this.settings = settings
|
||||||
|
this.myself = myself
|
||||||
|
this.timer = new Timer("host-persister",true)
|
||||||
|
}
|
||||||
|
|
||||||
HostCache(){}
|
void start() {
|
||||||
|
timer.schedule({load()} as TimerTask, 1)
|
||||||
public HostCache(TrustService trustService, File storage, int interval,
|
}
|
||||||
MuWireSettings settings, Destination myself) {
|
|
||||||
this.trustService = trustService
|
void stop() {
|
||||||
this.storage = storage
|
timer.cancel()
|
||||||
this.interval = interval
|
}
|
||||||
this.settings = settings
|
|
||||||
this.myself = myself
|
void onHostDiscoveredEvent(HostDiscoveredEvent e) {
|
||||||
this.timer = new Timer("host-persister",true)
|
if (myself == e.destination)
|
||||||
}
|
return
|
||||||
|
if (hosts.containsKey(e.destination)) {
|
||||||
void start() {
|
|
||||||
timer.schedule({load()} as TimerTask, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
void stop() {
|
|
||||||
timer.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
void onHostDiscoveredEvent(HostDiscoveredEvent e) {
|
|
||||||
if (myself == e.destination)
|
|
||||||
return
|
|
||||||
if (hosts.containsKey(e.destination)) {
|
|
||||||
if (!e.fromHostcache)
|
if (!e.fromHostcache)
|
||||||
return
|
return
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onConnectionEvent(ConnectionEvent e) {
|
||||||
|
if (e.leaf)
|
||||||
|
return
|
||||||
|
Destination dest = e.endpoint.destination
|
||||||
|
Host host = hosts.get(dest)
|
||||||
|
if (host == null) {
|
||||||
|
host = new Host(dest)
|
||||||
|
hosts.put(dest, host)
|
||||||
|
}
|
||||||
|
|
||||||
void onConnectionEvent(ConnectionEvent e) {
|
switch(e.status) {
|
||||||
if (e.leaf)
|
case ConnectionAttemptStatus.SUCCESSFUL:
|
||||||
return
|
case ConnectionAttemptStatus.REJECTED:
|
||||||
Destination dest = e.endpoint.destination
|
host.onConnect()
|
||||||
Host host = hosts.get(dest)
|
break
|
||||||
if (host == null) {
|
case ConnectionAttemptStatus.FAILED:
|
||||||
host = new Host(dest, settings.hostClearInterval, settings.hostHopelessInterval, settings.hostRejectInterval)
|
host.onFailure()
|
||||||
hosts.put(dest, host)
|
break
|
||||||
}
|
}
|
||||||
|
}
|
||||||
switch(e.status) {
|
|
||||||
case ConnectionAttemptStatus.SUCCESSFUL:
|
List<Destination> getHosts(int n) {
|
||||||
host.onConnect()
|
List<Destination> rv = new ArrayList<>(hosts.keySet())
|
||||||
break
|
rv.retainAll {allowHost(hosts[it])}
|
||||||
case ConnectionAttemptStatus.REJECTED:
|
if (rv.size() <= n)
|
||||||
host.onReject()
|
return rv
|
||||||
break
|
Collections.shuffle(rv)
|
||||||
case ConnectionAttemptStatus.FAILED:
|
rv[0..n-1]
|
||||||
host.onFailure()
|
}
|
||||||
break
|
|
||||||
}
|
List<Destination> getGoodHosts(int n) {
|
||||||
}
|
List<Destination> rv = new ArrayList<>(hosts.keySet())
|
||||||
|
rv.retainAll {
|
||||||
List<Destination> getHosts(int n) {
|
Host host = hosts[it]
|
||||||
List<Destination> rv = new ArrayList<>(hosts.keySet())
|
allowHost(host) && host.hasSucceeded()
|
||||||
rv.retainAll {allowHost(hosts[it])}
|
}
|
||||||
rv.removeAll {
|
if (rv.size() <= n)
|
||||||
def h = hosts[it];
|
return rv
|
||||||
(h.isFailed() && !h.canTryAgain()) || h.isRecentlyRejected()
|
Collections.shuffle(rv)
|
||||||
}
|
rv[0..n-1]
|
||||||
if (rv.size() <= n)
|
}
|
||||||
return rv
|
|
||||||
Collections.shuffle(rv)
|
void load() {
|
||||||
rv[0..n-1]
|
if (storage.exists()) {
|
||||||
}
|
JsonSlurper slurper = new JsonSlurper()
|
||||||
|
storage.eachLine {
|
||||||
List<Destination> getGoodHosts(int n) {
|
def entry = slurper.parseText(it)
|
||||||
List<Destination> rv = new ArrayList<>(hosts.keySet())
|
Destination dest = new Destination(entry.destination)
|
||||||
rv.retainAll {
|
Host host = new Host(dest)
|
||||||
Host host = hosts[it]
|
host.failures = Integer.valueOf(String.valueOf(entry.failures))
|
||||||
allowHost(host) && host.hasSucceeded()
|
host.successes = Integer.valueOf(String.valueOf(entry.successes))
|
||||||
}
|
if (allowHost(host))
|
||||||
if (rv.size() <= n)
|
hosts.put(dest, host)
|
||||||
return rv
|
}
|
||||||
Collections.shuffle(rv)
|
}
|
||||||
rv[0..n-1]
|
timer.schedule({save()} as TimerTask, interval, interval)
|
||||||
}
|
loaded = true
|
||||||
|
}
|
||||||
int countFailingHosts() {
|
|
||||||
List<Destination> rv = new ArrayList<>(hosts.keySet())
|
private boolean allowHost(Host host) {
|
||||||
rv.retainAll {
|
if (host.isFailed())
|
||||||
hosts[it].isFailed()
|
return false
|
||||||
}
|
if (host.destination == myself)
|
||||||
rv.size()
|
return false
|
||||||
}
|
TrustLevel trust = trustService.getLevel(host.destination)
|
||||||
|
switch(trust) {
|
||||||
int countHopelessHosts() {
|
case TrustLevel.DISTRUSTED :
|
||||||
List<Destination> rv = new ArrayList<>(hosts.keySet())
|
return false
|
||||||
rv.retainAll {
|
case TrustLevel.TRUSTED :
|
||||||
hosts[it].isHopeless()
|
return true
|
||||||
}
|
case TrustLevel.NEUTRAL :
|
||||||
rv.size()
|
return settings.allowUntrusted()
|
||||||
}
|
}
|
||||||
|
false
|
||||||
void load() {
|
}
|
||||||
if (storage.exists()) {
|
|
||||||
JsonSlurper slurper = new JsonSlurper()
|
private void save() {
|
||||||
storage.eachLine {
|
storage.delete()
|
||||||
def entry = slurper.parseText(it)
|
storage.withPrintWriter { writer ->
|
||||||
Destination dest = new Destination(entry.destination)
|
hosts.each { dest, host ->
|
||||||
Host host = new Host(dest, settings.hostClearInterval, settings.hostHopelessInterval, settings.hostRejectInterval)
|
if (allowHost(host)) {
|
||||||
host.failures = Integer.valueOf(String.valueOf(entry.failures))
|
def map = [:]
|
||||||
host.successes = Integer.valueOf(String.valueOf(entry.successes))
|
map.destination = dest.toBase64()
|
||||||
if (entry.lastAttempt != null)
|
map.failures = host.failures
|
||||||
host.lastAttempt = entry.lastAttempt
|
map.successes = host.successes
|
||||||
if (entry.lastSuccessfulAttempt != null)
|
def json = JsonOutput.toJson(map)
|
||||||
host.lastSuccessfulAttempt = entry.lastSuccessfulAttempt
|
writer.println json
|
||||||
if (entry.lastRejection != null)
|
}
|
||||||
host.lastRejection = entry.lastRejection
|
}
|
||||||
if (allowHost(host))
|
}
|
||||||
hosts.put(dest, host)
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
timer.schedule({save()} as TimerTask, interval, interval)
|
|
||||||
loaded = true
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean allowHost(Host host) {
|
|
||||||
if (host.destination == myself)
|
|
||||||
return false
|
|
||||||
TrustLevel trust = trustService.getLevel(host.destination)
|
|
||||||
switch(trust) {
|
|
||||||
case TrustLevel.DISTRUSTED :
|
|
||||||
return false
|
|
||||||
case TrustLevel.TRUSTED :
|
|
||||||
return true
|
|
||||||
case TrustLevel.NEUTRAL :
|
|
||||||
return settings.allowUntrusted()
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
private void save() {
|
|
||||||
storage.delete()
|
|
||||||
storage.withPrintWriter { writer ->
|
|
||||||
hosts.each { dest, host ->
|
|
||||||
if (allowHost(host) && !host.isHopeless()) {
|
|
||||||
def map = [:]
|
|
||||||
map.destination = dest.toBase64()
|
|
||||||
map.failures = host.failures
|
|
||||||
map.successes = host.successes
|
|
||||||
map.lastAttempt = host.lastAttempt
|
|
||||||
map.lastSuccessfulAttempt = host.lastSuccessfulAttempt
|
|
||||||
map.lastRejection = host.lastRejection
|
|
||||||
def json = JsonOutput.toJson(map)
|
|
||||||
writer.println json
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -6,11 +6,11 @@ import net.i2p.data.Destination
|
|||||||
|
|
||||||
class HostDiscoveredEvent extends Event {
|
class HostDiscoveredEvent extends Event {
|
||||||
|
|
||||||
Destination destination
|
Destination destination
|
||||||
boolean fromHostcache
|
boolean fromHostcache
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
"HostDiscoveredEvent ${super.toString()} destination:${destination.toBase32()} from hostcache $fromHostcache"
|
"HostDiscoveredEvent ${super.toString()} destination:${destination.toBase32()} from hostcache $fromHostcache"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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]
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,5 +0,0 @@
|
|||||||
package com.muwire.core.search;
|
|
||||||
|
|
||||||
public enum BrowseStatus {
|
|
||||||
CONNECTING, FETCHING, FINISHED, FAILED
|
|
||||||
}
|
|
@@ -1,8 +0,0 @@
|
|||||||
package com.muwire.core.search
|
|
||||||
|
|
||||||
import com.muwire.core.Event
|
|
||||||
|
|
||||||
class BrowseStatusEvent extends Event {
|
|
||||||
BrowseStatus status
|
|
||||||
int totalResults
|
|
||||||
}
|
|
@@ -6,11 +6,11 @@ import net.i2p.data.Base32
|
|||||||
import net.i2p.data.Destination
|
import net.i2p.data.Destination
|
||||||
|
|
||||||
class DeleteEvent extends Event {
|
class DeleteEvent extends Event {
|
||||||
byte [] infoHash
|
byte [] infoHash
|
||||||
Destination leaf
|
Destination leaf
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
"DeleteEvent ${super.toString()} infoHash:${Base32.encode(infoHash)} leaf:${leaf.toBase32()}"
|
"DeleteEvent ${super.toString()} infoHash:${Base32.encode(infoHash)} leaf:${leaf.toBase32()}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -21,5 +21,5 @@ class InvalidSearchResultException extends Exception {
|
|||||||
super(cause);
|
super(cause);
|
||||||
// TODO Auto-generated constructor stub
|
// TODO Auto-generated constructor stub
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -6,33 +6,33 @@ import com.muwire.core.connection.UltrapeerConnectionManager
|
|||||||
import net.i2p.data.Destination
|
import net.i2p.data.Destination
|
||||||
|
|
||||||
class LeafSearcher {
|
class LeafSearcher {
|
||||||
|
|
||||||
final UltrapeerConnectionManager connectionManager
|
final UltrapeerConnectionManager connectionManager
|
||||||
final SearchIndex searchIndex = new SearchIndex()
|
final SearchIndex searchIndex = new SearchIndex()
|
||||||
|
|
||||||
final Map<String, Set<byte[]>> fileNameToHashes = new HashMap<>()
|
final Map<String, Set<byte[]>> fileNameToHashes = new HashMap<>()
|
||||||
final Map<byte[], Set<Destination>> hashToLeafs = new HashMap<>()
|
final Map<byte[], Set<Destination>> hashToLeafs = new HashMap<>()
|
||||||
|
|
||||||
final Map<Destination, Map<byte[], Set<String>>> leafToFiles = new HashMap<>()
|
final Map<Destination, Map<byte[], Set<String>>> leafToFiles = new HashMap<>()
|
||||||
|
|
||||||
LeafSearcher(UltrapeerConnectionManager connectionManager) {
|
LeafSearcher(UltrapeerConnectionManager connectionManager) {
|
||||||
this.connectionManager = connectionManager
|
this.connectionManager = connectionManager
|
||||||
}
|
}
|
||||||
|
|
||||||
void onUpsertEvent(UpsertEvent e) {
|
void onUpsertEvent(UpsertEvent e) {
|
||||||
// TODO: implement
|
// TODO: implement
|
||||||
}
|
}
|
||||||
|
|
||||||
void onDeleteEvent(DeleteEvent e) {
|
void onDeleteEvent(DeleteEvent e) {
|
||||||
// TODO: implement
|
// TODO: implement
|
||||||
}
|
}
|
||||||
|
|
||||||
void onDisconnectionEvent(DisconnectionEvent e) {
|
void onDisconnectionEvent(DisconnectionEvent e) {
|
||||||
// TODO: implement
|
// TODO: implement
|
||||||
}
|
}
|
||||||
|
|
||||||
void onQueryEvent(QueryEvent e) {
|
void onQueryEvent(QueryEvent e) {
|
||||||
// TODO: implement
|
// TODO: implement
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -6,12 +6,12 @@ import com.muwire.core.Persona
|
|||||||
import net.i2p.data.Destination
|
import net.i2p.data.Destination
|
||||||
|
|
||||||
class QueryEvent extends Event {
|
class QueryEvent extends Event {
|
||||||
|
|
||||||
SearchEvent searchEvent
|
SearchEvent searchEvent
|
||||||
boolean firstHop
|
boolean firstHop
|
||||||
Destination replyTo
|
Destination replyTo
|
||||||
Persona originator
|
Persona originator
|
||||||
Destination receivedOn
|
Destination receivedOn
|
||||||
|
|
||||||
String toString() {
|
String toString() {
|
||||||
"searchEvent: $searchEvent firstHop:$firstHop, replyTo:${replyTo.toBase32()}" +
|
"searchEvent: $searchEvent firstHop:$firstHop, replyTo:${replyTo.toBase32()}" +
|
||||||
|
@@ -6,6 +6,6 @@ import com.muwire.core.SharedFile
|
|||||||
class ResultsEvent extends Event {
|
class ResultsEvent extends Event {
|
||||||
|
|
||||||
SearchEvent searchEvent
|
SearchEvent searchEvent
|
||||||
SharedFile[] results
|
SharedFile[] results
|
||||||
UUID uuid
|
UUID uuid
|
||||||
}
|
}
|
||||||
|
@@ -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 {
|
||||||
@@ -26,7 +23,7 @@ class ResultsParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static parseV1(Persona p, UUID uuid, def json) {
|
private static parseV1(Persona p, UUID uuid, def json) {
|
||||||
if (json.name == null)
|
if (json.name == null)
|
||||||
throw new InvalidSearchResultException("name missing")
|
throw new InvalidSearchResultException("name missing")
|
||||||
@@ -55,19 +52,18 @@ class ResultsParser {
|
|||||||
InfoHash parsedIH = InfoHash.fromHashList(hashList)
|
InfoHash parsedIH = InfoHash.fromHashList(hashList)
|
||||||
if (parsedIH.getRoot() != infoHash)
|
if (parsedIH.getRoot() != infoHash)
|
||||||
throw new InvalidSearchControlsException("infohash root doesn't match")
|
throw new InvalidSearchControlsException("infohash root doesn't match")
|
||||||
|
|
||||||
return new UIResultEvent( sender : p,
|
return new UIResultEvent( sender : p,
|
||||||
name : name,
|
name : name,
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static UIResultEvent parseV2(Persona p, UUID uuid, def json) {
|
private static UIResultEvent parseV2(Persona p, UUID uuid, def json) {
|
||||||
if (json.name == null)
|
if (json.name == null)
|
||||||
throw new InvalidSearchResultException("name missing")
|
throw new InvalidSearchResultException("name missing")
|
||||||
@@ -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)
|
||||||
|
@@ -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
|
||||||
@@ -28,9 +22,9 @@ import net.i2p.data.Destination
|
|||||||
|
|
||||||
@Log
|
@Log
|
||||||
class ResultsSender {
|
class ResultsSender {
|
||||||
|
|
||||||
private static final AtomicInteger THREAD_NO = new AtomicInteger()
|
private static final AtomicInteger THREAD_NO = new AtomicInteger()
|
||||||
|
|
||||||
private final Executor executor = Executors.newCachedThreadPool(
|
private final Executor executor = Executors.newCachedThreadPool(
|
||||||
new ThreadFactory() {
|
new ThreadFactory() {
|
||||||
@Override
|
@Override
|
||||||
@@ -41,131 +35,89 @@ class ResultsSender {
|
|||||||
rv
|
rv
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
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) {
|
||||||
ResultsSender(EventBus eventBus, I2PConnector connector, Persona me, MuWireSettings settings) {
|
|
||||||
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ResultSendJob implements Runnable {
|
private class ResultSendJob implements Runnable {
|
||||||
UUID uuid
|
UUID uuid
|
||||||
SharedFile [] results
|
SharedFile [] results
|
||||||
Destination target
|
Destination target
|
||||||
boolean oobInfohash
|
boolean oobInfohash
|
||||||
boolean compressedResults
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
byte [] tmp = new byte[InfoHash.SIZE]
|
||||||
|
JsonOutput jsonOutput = new JsonOutput()
|
||||||
|
Endpoint endpoint = null;
|
||||||
try {
|
try {
|
||||||
JsonOutput jsonOutput = new JsonOutput()
|
endpoint = connector.connect(target)
|
||||||
Endpoint endpoint = null;
|
DataOutputStream os = new DataOutputStream(endpoint.getOutputStream())
|
||||||
if (!compressedResults) {
|
os.write("POST $uuid\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
try {
|
me.write(os)
|
||||||
endpoint = connector.connect(target)
|
os.writeShort((short)results.length)
|
||||||
DataOutputStream os = new DataOutputStream(endpoint.getOutputStream())
|
results.each {
|
||||||
os.write("POST $uuid\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
byte [] name = it.getFile().getName().getBytes(StandardCharsets.UTF_8)
|
||||||
me.write(os)
|
def baos = new ByteArrayOutputStream()
|
||||||
os.writeShort((short)results.length)
|
def daos = new DataOutputStream(baos)
|
||||||
results.each {
|
daos.writeShort((short) name.length)
|
||||||
def obj = sharedFileToObj(it, settings.browseFiles)
|
daos.write(name)
|
||||||
def json = jsonOutput.toJson(obj)
|
daos.flush()
|
||||||
os.writeShort((short)json.length())
|
String encodedName = Base64.encode(baos.toByteArray())
|
||||||
os.write(json.getBytes(StandardCharsets.US_ASCII))
|
def obj = [:]
|
||||||
|
obj.type = "Result"
|
||||||
|
obj.version = oobInfohash ? 2 : 1
|
||||||
|
obj.name = encodedName
|
||||||
|
obj.infohash = Base64.encode(it.getInfoHash().getRoot())
|
||||||
|
obj.size = it.getFile().length()
|
||||||
|
obj.pieceSize = it.getPieceSize()
|
||||||
|
if (!oobInfohash) {
|
||||||
|
byte [] hashList = it.getInfoHash().getHashList()
|
||||||
|
def hashListB64 = []
|
||||||
|
for (int i = 0; i < hashList.length / InfoHash.SIZE; i++) {
|
||||||
|
System.arraycopy(hashList, InfoHash.SIZE * i, tmp, 0, InfoHash.SIZE)
|
||||||
|
hashListB64 << Base64.encode(tmp)
|
||||||
}
|
}
|
||||||
os.flush()
|
obj.hashList = hashListB64
|
||||||
} 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()
|
|
||||||
}
|
}
|
||||||
|
def json = jsonOutput.toJson(obj)
|
||||||
|
os.writeShort((short)json.length())
|
||||||
|
os.write(json.getBytes(StandardCharsets.US_ASCII))
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
os.flush()
|
||||||
log.log(Level.WARNING, "problem sending results",e)
|
} finally {
|
||||||
|
endpoint?.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static def sharedFileToObj(SharedFile sf, boolean browseFiles) {
|
|
||||||
byte [] name = sf.getFile().getName().getBytes(StandardCharsets.UTF_8)
|
|
||||||
def baos = new ByteArrayOutputStream()
|
|
||||||
def daos = new DataOutputStream(baos)
|
|
||||||
daos.writeShort((short) name.length)
|
|
||||||
daos.write(name)
|
|
||||||
daos.flush()
|
|
||||||
String encodedName = Base64.encode(baos.toByteArray())
|
|
||||||
def obj = [:]
|
|
||||||
obj.type = "Result"
|
|
||||||
obj.version = 2
|
|
||||||
obj.name = encodedName
|
|
||||||
obj.infohash = Base64.encode(sf.getInfoHash().getRoot())
|
|
||||||
obj.size = sf.getCachedLength()
|
|
||||||
obj.pieceSize = sf.getPieceSize()
|
|
||||||
|
|
||||||
if (sf instanceof DownloadedFile)
|
|
||||||
obj.sources = sf.sources.stream().map({dest -> dest.toBase64()}).collect(Collectors.toSet())
|
|
||||||
|
|
||||||
if (sf.getComment() != null)
|
|
||||||
obj.comment = sf.getComment()
|
|
||||||
|
|
||||||
obj.browse = browseFiles
|
|
||||||
obj
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -5,17 +5,15 @@ import com.muwire.core.InfoHash
|
|||||||
|
|
||||||
class SearchEvent extends Event {
|
class SearchEvent extends Event {
|
||||||
|
|
||||||
List<String> searchTerms
|
List<String> searchTerms
|
||||||
byte [] searchHash
|
byte [] searchHash
|
||||||
UUID uuid
|
UUID uuid
|
||||||
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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,59 +1,56 @@
|
|||||||
package com.muwire.core.search
|
package com.muwire.core.search
|
||||||
|
|
||||||
import com.muwire.core.SplitPattern
|
import com.muwire.core.Constants
|
||||||
|
|
||||||
class SearchIndex {
|
class SearchIndex {
|
||||||
|
|
||||||
final Map<String, Set<String>> keywords = new HashMap<>()
|
final Map<String, Set<String>> keywords = new HashMap<>()
|
||||||
|
|
||||||
void add(String string) {
|
void add(String string) {
|
||||||
String [] split = split(string)
|
String [] split = split(string)
|
||||||
split.each {
|
split.each {
|
||||||
Set<String> existing = keywords.get(it)
|
Set<String> existing = keywords.get(it)
|
||||||
if (existing == null) {
|
if (existing == null) {
|
||||||
existing = new HashSet<>()
|
existing = new HashSet<>()
|
||||||
keywords.put(it, existing)
|
keywords.put(it, existing)
|
||||||
}
|
}
|
||||||
existing.add(string)
|
existing.add(string)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void remove(String string) {
|
void remove(String string) {
|
||||||
String [] split = split(string)
|
String [] split = split(string)
|
||||||
split.each {
|
split.each {
|
||||||
Set<String> existing = keywords.get it
|
Set<String> existing = keywords.get it
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
existing.remove(string)
|
existing.remove(string)
|
||||||
if (existing.isEmpty()) {
|
if (existing.isEmpty()) {
|
||||||
keywords.remove(it)
|
keywords.remove(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
}
|
Set<String> rv = null;
|
||||||
|
|
||||||
String[] search(List<String> terms) {
|
terms.each {
|
||||||
Set<String> rv = null;
|
Set<String> forWord = keywords.getOrDefault(it,[])
|
||||||
|
if (rv == null) {
|
||||||
terms.each {
|
rv = new HashSet<>(forWord)
|
||||||
Set<String> forWord = keywords.getOrDefault(it,[])
|
} else {
|
||||||
if (rv == null) {
|
rv.retainAll(forWord)
|
||||||
rv = new HashSet<>(forWord)
|
}
|
||||||
} else {
|
|
||||||
rv.retainAll(forWord)
|
}
|
||||||
}
|
|
||||||
|
if (rv != null)
|
||||||
}
|
return rv.asList()
|
||||||
|
[]
|
||||||
if (rv != null)
|
}
|
||||||
return rv.asList()
|
|
||||||
[]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -8,17 +8,17 @@ import net.i2p.data.Destination
|
|||||||
|
|
||||||
@Log
|
@Log
|
||||||
public class SearchManager {
|
public class SearchManager {
|
||||||
|
|
||||||
private static final int EXPIRE_TIME = 60 * 1000 * 1000
|
private static final int EXPIRE_TIME = 60 * 1000 * 1000
|
||||||
private static final int CHECK_INTERVAL = 60 * 1000
|
private static final int CHECK_INTERVAL = 60 * 1000
|
||||||
|
|
||||||
private final EventBus eventBus
|
private final EventBus eventBus
|
||||||
private final Persona me
|
private final Persona me
|
||||||
private final ResultsSender resultsSender
|
private final ResultsSender resultsSender
|
||||||
private final Map<UUID, QueryEvent> responderAddress = Collections.synchronizedMap(new HashMap<>())
|
private final Map<UUID, QueryEvent> responderAddress = Collections.synchronizedMap(new HashMap<>())
|
||||||
|
|
||||||
SearchManager(){}
|
SearchManager(){}
|
||||||
|
|
||||||
SearchManager(EventBus eventBus, Persona me, ResultsSender resultsSender) {
|
SearchManager(EventBus eventBus, Persona me, ResultsSender resultsSender) {
|
||||||
this.eventBus = eventBus
|
this.eventBus = eventBus
|
||||||
this.me = me
|
this.me = me
|
||||||
@@ -26,7 +26,7 @@ public class SearchManager {
|
|||||||
Timer timer = new Timer("query-expirer", true)
|
Timer timer = new Timer("query-expirer", true)
|
||||||
timer.schedule({cleanup()} as TimerTask, CHECK_INTERVAL, CHECK_INTERVAL)
|
timer.schedule({cleanup()} as TimerTask, CHECK_INTERVAL, CHECK_INTERVAL)
|
||||||
}
|
}
|
||||||
|
|
||||||
void onQueryEvent(QueryEvent event) {
|
void onQueryEvent(QueryEvent event) {
|
||||||
if (responderAddress.containsKey(event.searchEvent.uuid)) {
|
if (responderAddress.containsKey(event.searchEvent.uuid)) {
|
||||||
log.info("Dropping duplicate search uuid $event.searchEvent.uuid")
|
log.info("Dropping duplicate search uuid $event.searchEvent.uuid")
|
||||||
@@ -35,7 +35,7 @@ public class SearchManager {
|
|||||||
responderAddress.put(event.searchEvent.uuid, event)
|
responderAddress.put(event.searchEvent.uuid, event)
|
||||||
eventBus.publish(event.searchEvent)
|
eventBus.publish(event.searchEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
void onResultsEvent(ResultsEvent event) {
|
void onResultsEvent(ResultsEvent event) {
|
||||||
Destination target = responderAddress.get(event.uuid)?.replyTo
|
Destination target = responderAddress.get(event.uuid)?.replyTo
|
||||||
if (target == null)
|
if (target == null)
|
||||||
@@ -44,13 +44,13 @@ 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) {
|
||||||
me.destination.equals(responderAddress.get(uuid)?.replyTo)
|
me.destination.equals(responderAddress.get(uuid)?.replyTo)
|
||||||
}
|
}
|
||||||
|
|
||||||
private void cleanup() {
|
private void cleanup() {
|
||||||
final long now = System.currentTimeMillis()
|
final long now = System.currentTimeMillis()
|
||||||
synchronized(responderAddress) {
|
synchronized(responderAddress) {
|
||||||
|
@@ -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
|
|
||||||
}
|
|
@@ -1,8 +0,0 @@
|
|||||||
package com.muwire.core.search
|
|
||||||
|
|
||||||
import com.muwire.core.Event
|
|
||||||
|
|
||||||
class UIResultBatchEvent extends Event {
|
|
||||||
UUID uuid
|
|
||||||
UIResultEvent[] results
|
|
||||||
}
|
|
@@ -4,19 +4,14 @@ 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() {
|
||||||
super.toString() + "name:$name size:$size sender:${sender.getHumanReadableName()} pieceSize $pieceSize"
|
super.toString() + "name:$name size:$size sender:${sender.getHumanReadableName()} pieceSize $pieceSize"
|
||||||
|
@@ -18,5 +18,5 @@ class UnexpectedResultsException extends Exception {
|
|||||||
public UnexpectedResultsException(String message) {
|
public UnexpectedResultsException(String message) {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -7,12 +7,12 @@ import net.i2p.data.Destination
|
|||||||
|
|
||||||
class UpsertEvent extends Event {
|
class UpsertEvent extends Event {
|
||||||
|
|
||||||
Set<String> names
|
Set<String> names
|
||||||
byte [] infoHash
|
byte [] infoHash
|
||||||
Destination leaf
|
Destination leaf
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
"UpsertEvent ${super.toString()} names:$names infoHash:${Base32.encode(infoHash)} leaf:${leaf.toBase32()}"
|
"UpsertEvent ${super.toString()} names:$names infoHash:${Base32.encode(infoHash)} leaf:${leaf.toBase32()}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@@ -5,6 +5,6 @@ import com.muwire.core.Persona
|
|||||||
|
|
||||||
class TrustEvent extends Event {
|
class TrustEvent extends Event {
|
||||||
|
|
||||||
Persona persona
|
Persona persona
|
||||||
TrustLevel level
|
TrustLevel level
|
||||||
}
|
}
|
||||||
|
@@ -11,87 +11,87 @@ import net.i2p.util.ConcurrentHashSet
|
|||||||
|
|
||||||
class TrustService extends Service {
|
class TrustService extends Service {
|
||||||
|
|
||||||
final File persistGood, persistBad
|
final File persistGood, persistBad
|
||||||
final long persistInterval
|
final long persistInterval
|
||||||
|
|
||||||
final Map<Destination, Persona> good = new ConcurrentHashMap<>()
|
final Map<Destination, Persona> good = new ConcurrentHashMap<>()
|
||||||
final Map<Destination, Persona> bad = new ConcurrentHashMap<>()
|
final Map<Destination, Persona> bad = new ConcurrentHashMap<>()
|
||||||
|
|
||||||
final Timer timer
|
final Timer timer
|
||||||
|
|
||||||
TrustService() {}
|
TrustService() {}
|
||||||
|
|
||||||
TrustService(File persistGood, File persistBad, long persistInterval) {
|
TrustService(File persistGood, File persistBad, long persistInterval) {
|
||||||
this.persistBad = persistBad
|
this.persistBad = persistBad
|
||||||
this.persistGood = persistGood
|
this.persistGood = persistGood
|
||||||
this.persistInterval = persistInterval
|
this.persistInterval = persistInterval
|
||||||
this.timer = new Timer("trust-persister",true)
|
this.timer = new Timer("trust-persister",true)
|
||||||
}
|
}
|
||||||
|
|
||||||
void start() {
|
void start() {
|
||||||
timer.schedule({load()} as TimerTask, 1)
|
timer.schedule({load()} as TimerTask, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
void stop() {
|
void stop() {
|
||||||
timer.cancel()
|
timer.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
void load() {
|
void load() {
|
||||||
if (persistGood.exists()) {
|
if (persistGood.exists()) {
|
||||||
persistGood.eachLine {
|
persistGood.eachLine {
|
||||||
byte [] decoded = Base64.decode(it)
|
byte [] decoded = Base64.decode(it)
|
||||||
Persona persona = new Persona(new ByteArrayInputStream(decoded))
|
Persona persona = new Persona(new ByteArrayInputStream(decoded))
|
||||||
good.put(persona.destination, persona)
|
good.put(persona.destination, persona)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (persistBad.exists()) {
|
if (persistBad.exists()) {
|
||||||
persistBad.eachLine {
|
persistBad.eachLine {
|
||||||
byte [] decoded = Base64.decode(it)
|
byte [] decoded = Base64.decode(it)
|
||||||
Persona persona = new Persona(new ByteArrayInputStream(decoded))
|
Persona persona = new Persona(new ByteArrayInputStream(decoded))
|
||||||
bad.put(persona.destination, persona)
|
bad.put(persona.destination, persona)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
timer.schedule({persist()} as TimerTask, persistInterval, persistInterval)
|
timer.schedule({persist()} as TimerTask, persistInterval, persistInterval)
|
||||||
loaded = true
|
loaded = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private void persist() {
|
private void persist() {
|
||||||
persistGood.delete()
|
persistGood.delete()
|
||||||
persistGood.withPrintWriter { writer ->
|
persistGood.withPrintWriter { writer ->
|
||||||
good.each {k,v ->
|
good.each {k,v ->
|
||||||
writer.println v.toBase64()
|
writer.println v.toBase64()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
persistBad.delete()
|
persistBad.delete()
|
||||||
persistBad.withPrintWriter { writer ->
|
persistBad.withPrintWriter { writer ->
|
||||||
bad.each { k,v ->
|
bad.each { k,v ->
|
||||||
writer.println v.toBase64()
|
writer.println v.toBase64()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TrustLevel getLevel(Destination dest) {
|
TrustLevel getLevel(Destination dest) {
|
||||||
if (good.containsKey(dest))
|
if (good.containsKey(dest))
|
||||||
return TrustLevel.TRUSTED
|
return TrustLevel.TRUSTED
|
||||||
else if (bad.containsKey(dest))
|
else if (bad.containsKey(dest))
|
||||||
return TrustLevel.DISTRUSTED
|
return TrustLevel.DISTRUSTED
|
||||||
TrustLevel.NEUTRAL
|
TrustLevel.NEUTRAL
|
||||||
}
|
}
|
||||||
|
|
||||||
void onTrustEvent(TrustEvent e) {
|
void onTrustEvent(TrustEvent e) {
|
||||||
switch(e.level) {
|
switch(e.level) {
|
||||||
case TrustLevel.TRUSTED:
|
case TrustLevel.TRUSTED:
|
||||||
bad.remove(e.persona.destination)
|
bad.remove(e.persona.destination)
|
||||||
good.put(e.persona.destination, e.persona)
|
good.put(e.persona.destination, e.persona)
|
||||||
break
|
break
|
||||||
case TrustLevel.DISTRUSTED:
|
case TrustLevel.DISTRUSTED:
|
||||||
good.remove(e.persona.destination)
|
good.remove(e.persona.destination)
|
||||||
bad.put(e.persona.destination, e.persona)
|
bad.put(e.persona.destination, e.persona)
|
||||||
break
|
break
|
||||||
case TrustLevel.NEUTRAL:
|
case TrustLevel.NEUTRAL:
|
||||||
good.remove(e.persona.destination)
|
good.remove(e.persona.destination)
|
||||||
bad.remove(e.persona.destination)
|
bad.remove(e.persona.destination)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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
|
|
||||||
}
|
|
@@ -1,7 +0,0 @@
|
|||||||
package com.muwire.core.trust
|
|
||||||
|
|
||||||
import com.muwire.core.Event
|
|
||||||
|
|
||||||
class TrustSubscriptionUpdatedEvent extends Event {
|
|
||||||
RemoteTrustList trustList
|
|
||||||
}
|
|
@@ -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,54 +21,28 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
void start() {
|
void start() {
|
||||||
session.addMuxedSessionListener(new Listener(), I2PSession.PROTO_DATAGRAM, 2)
|
session.addMuxedSessionListener(new Listener(), I2PSession.PROTO_DATAGRAM, 2)
|
||||||
timer.schedule({checkUpdate()} as TimerTask, 60000, 60 * 60 * 1000)
|
timer.schedule({checkUpdate()} as TimerTask, 60000, 60 * 60 * 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
void stop() {
|
void stop() {
|
||||||
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) {
|
||||||
@@ -85,20 +50,20 @@ class UpdateClient {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
lastUpdateCheckTime = now
|
lastUpdateCheckTime = now
|
||||||
|
|
||||||
log.info("checking for update")
|
log.info("checking for update")
|
||||||
|
|
||||||
def ping = [version : 1, myVersion : myVersion]
|
def ping = [version : 1, myVersion : myVersion]
|
||||||
ping = JsonOutput.toJson(ping)
|
ping = JsonOutput.toJson(ping)
|
||||||
def maker = new I2PDatagramMaker(session)
|
def maker = new I2PDatagramMaker(session)
|
||||||
ping = maker.makeI2PDatagram(ping.bytes)
|
ping = maker.makeI2PDatagram(ping.bytes)
|
||||||
def options = new SendMessageOptions()
|
def options = new SendMessageOptions()
|
||||||
options.setSendLeaseSet(true)
|
options.setSendLeaseSet(true)
|
||||||
session.sendMessage(UpdateServers.UPDATE_SERVER, ping, 0, ping.length, I2PSession.PROTO_DATAGRAM, 2, 0, options)
|
session.sendMessage(UpdateServers.UPDATE_SERVER, ping, 0, ping.length, I2PSession.PROTO_DATAGRAM, 2, 0, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
class Listener implements I2PSessionMuxedListener {
|
class Listener implements I2PSessionMuxedListener {
|
||||||
|
|
||||||
final JsonSlurper slurper = new JsonSlurper()
|
final JsonSlurper slurper = new JsonSlurper()
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -111,7 +76,7 @@ class UpdateClient {
|
|||||||
log.warning "Received unexpected protocol $proto"
|
log.warning "Received unexpected protocol $proto"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
def payload = session.receiveMessage(msgId)
|
def payload = session.receiveMessage(msgId)
|
||||||
def dissector = new I2PDatagramDissector()
|
def dissector = new I2PDatagramDissector()
|
||||||
try {
|
try {
|
||||||
@@ -121,53 +86,29 @@ class UpdateClient {
|
|||||||
log.warning("received something not from update server " + sender.toBase32())
|
log.warning("received something not from update server " + sender.toBase32())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("Received something from update server")
|
log.info("Received something from update server")
|
||||||
|
|
||||||
payload = dissector.getPayload()
|
payload = dissector.getPayload()
|
||||||
payload = slurper.parse(payload)
|
payload = slurper.parse(payload)
|
||||||
|
|
||||||
if (payload.version == null) {
|
if (payload.version == null) {
|
||||||
log.warning("version missing")
|
log.warning("version missing")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload.signer == null) {
|
if (payload.signer == null) {
|
||||||
log.warning("signer missing")
|
log.warning("signer missing")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (VersionComparator.comp(myVersion, payload.version) >= 0) {
|
if (VersionComparator.comp(myVersion, payload.version) >= 0) {
|
||||||
log.info("no new version available")
|
log.info("no new version available")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
String infoHash
|
log.info("new version $payload.version available, publishing event")
|
||||||
if (settings.updateType == "jar") {
|
eventBus.publish(new UpdateAvailableEvent(version : payload.version, signer : payload.signer, infoHash : payload.infoHash))
|
||||||
infoHash = payload.infoHash
|
|
||||||
} else
|
|
||||||
infoHash = payload[settings.updateType]
|
|
||||||
|
|
||||||
|
|
||||||
if (!settings.autoDownloadUpdate) {
|
|
||||||
log.info("new version $payload.version available, publishing event")
|
|
||||||
eventBus.publish(new UpdateAvailableEvent(version : payload.version, signer : payload.signer, infoHash : 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)
|
||||||
}
|
}
|
||||||
@@ -186,6 +127,6 @@ class UpdateClient {
|
|||||||
public void errorOccurred(I2PSession session, String message, Throwable error) {
|
public void errorOccurred(I2PSession session, String message, Throwable error) {
|
||||||
log.log(Level.SEVERE, message, error)
|
log.log(Level.SEVERE, message, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +0,0 @@
|
|||||||
package com.muwire.core.update
|
|
||||||
|
|
||||||
import com.muwire.core.Event
|
|
||||||
|
|
||||||
class UpdateDownloadedEvent extends Event {
|
|
||||||
String version
|
|
||||||
String signer
|
|
||||||
}
|
|
@@ -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")
|
||||||
}
|
}
|
||||||
|
@@ -2,5 +2,4 @@ package com.muwire.core.upload
|
|||||||
|
|
||||||
class ContentRequest extends Request {
|
class ContentRequest extends Request {
|
||||||
Range range
|
Range range
|
||||||
int have
|
|
||||||
}
|
}
|
||||||
|
@@ -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) {
|
||||||
|
|
||||||
ContentUploader(File file, ContentRequest request, Endpoint endpoint, Mesh mesh, int pieceSize) {
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -6,24 +6,21 @@ 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
|
||||||
|
|
||||||
HashListUploader(Endpoint endpoint, InfoHash infoHash, HashListRequest request) {
|
HashListUploader(Endpoint endpoint, InfoHash infoHash, HashListRequest request) {
|
||||||
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() {
|
||||||
OutputStream os = endpoint.getOutputStream()
|
OutputStream os = endpoint.getOutputStream()
|
||||||
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
|
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
os.write("Content-Range: 0-${mapped.remaining()}\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
os.write("Content-Range: 0-${mapped.remaining()}\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
|
||||||
byte[]tmp = new byte[0x1 << 13]
|
byte[]tmp = new byte[0x1 << 13]
|
||||||
while(mapped.hasRemaining()) {
|
while(mapped.hasRemaining()) {
|
||||||
int start = mapped.position()
|
int start = mapped.position()
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,7 @@ package com.muwire.core.upload
|
|||||||
|
|
||||||
class Range {
|
class Range {
|
||||||
final long start, end
|
final long start, end
|
||||||
|
|
||||||
Range(long start, long end) {
|
Range(long start, long end) {
|
||||||
this.start = start
|
this.start = start
|
||||||
this.end = end
|
this.end = end
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user