Compare commits
287 Commits
muwire-0.3
...
muwire-0.4
Author | SHA1 | Date | |
---|---|---|---|
337605dc0f | |||
14bdfa6b2e | |||
ed3f9da773 | |||
251080d08f | |||
f530ab999d | |||
4133384e48 | |||
600fc98868 | |||
129eeb3b88 | |||
20b51b78a0 | |||
33fe755b60 | |||
8b0668a134 | |||
730d2202fd | |||
69906a986d | |||
5bc8fa8633 | |||
7de7c9d8f3 | |||
e943f6019d | |||
2eec7bec5b | |||
c36110cf76 | |||
abe28517bc | |||
15bc4c064d | |||
91d771944b | |||
e09c456a13 | |||
d9c1067226 | |||
eda3e7ad3a | |||
e9798c7eaa | |||
66bb4eef5b | |||
55f260b3f4 | |||
32d4c3965e | |||
de1534d837 | |||
7b58e8a88a | |||
8a03b89985 | |||
1d97374857 | |||
549e8c2d98 | |||
b54d24db0d | |||
fa12e84345 | |||
6430ff2691 | |||
591313c81c | |||
ce7b6a0c65 | |||
5c4d4c4580 | |||
4cb864ff9f | |||
417675ad07 | |||
9513e5ba3c | |||
85610cf169 | |||
e8322384b8 | |||
179279ed30 | |||
ae79f0fded | |||
ed878b3762 | |||
623cca0ef2 | |||
eaa883c3ba | |||
7ae8076865 | |||
b1aa92661c | |||
9ed94c8376 | |||
fa6aea1abe | |||
0de84e704b | |||
a767dda044 | |||
56e9235d7b | |||
2fba9a74ce | |||
2bb6826906 | |||
9f339629a9 | |||
58d4207f94 | |||
32577a28dc | |||
f7b43304d4 | |||
dcbe09886d | |||
5a54b2dcda | |||
581293b24f | |||
cd072b9f76 | |||
6b74fc5956 | |||
3de2f872bb | |||
fcde917d08 | |||
4ded065010 | |||
18a1c7091a | |||
46aee19f80 | |||
92dd7064c6 | |||
b2e4dda677 | |||
e77a2c8961 | |||
ee2fd2ef68 | |||
3f95d2bf1d | |||
1390983732 | |||
ce660cefe9 | |||
72b81eb886 | |||
57d593a68a | |||
39a81a3376 | |||
fd0bf17c24 | |||
ac12bff69b | |||
feef773bac | |||
239d8f12a7 | |||
8bbc61a7cb | |||
7f31c4477f | |||
6bad67c1bf | |||
c76e6dc99f | |||
acf9db0db3 | |||
69b4f0b547 | |||
80e165b505 | |||
bcce55b873 | |||
d5c92560db | |||
f827c1c9bf | |||
88c5f1a02d | |||
d8e44f5f39 | |||
72ff47ffe5 | |||
066ee2c96d | |||
0a8016dea7 | |||
db36367b11 | |||
b6c9ccb7f6 | |||
a9dc636bce | |||
3cc0574d11 | |||
20fab9b16d | |||
4015818323 | |||
f569d45c8c | |||
3773647869 | |||
29cdbf018c | |||
94bb7022eb | |||
39808302df | |||
2d22f9c39e | |||
ee8f80bab6 | |||
3e6242e583 | |||
41181616ee | |||
eb2530ca32 | |||
b5233780ef | |||
78753d7538 | |||
4740e8b4f5 | |||
ad5b00fc90 | |||
d6c6880848 | |||
4f948c1b9e | |||
2b68c24f9c | |||
bcdf0422db | |||
f6434b478d | |||
e979fdd26f | |||
e6bfcaaab9 | |||
9780108e8a | |||
697c7d2d6d | |||
887d10c8bf | |||
ef6b8fe458 | |||
20ab55d763 | |||
eda58c9e0d | |||
fb42fc0e35 | |||
35cabc47ad | |||
5be97d0404 | |||
82b0fa253c | |||
011a4d5766 | |||
5cd1ca88c1 | |||
44c880d911 | |||
14857cb5ad | |||
7daf981f1a | |||
b99bc0ea32 | |||
1ccf6fbdfa | |||
5711979272 | |||
9a5e2b1fa3 | |||
cafc5f582e | |||
a89b423dfc | |||
79e8438941 | |||
19c2c46491 | |||
78f1d54b69 | |||
9461649ed4 | |||
8573ab2850 | |||
8b3d752727 | |||
7c54bd8966 | |||
5d0fcb7027 | |||
3ec9654d3c | |||
7c8d64b462 | |||
31e30e3d31 | |||
8caf6e99b0 | |||
624155debd | |||
4468a262ae | |||
1780901cb0 | |||
d830d9261f | |||
f5e1833a48 | |||
9feb2a3c8f | |||
b27665f5dd | |||
4465aa4134 | |||
ad766ac748 | |||
d9e7d67d86 | |||
3fefbc94b3 | |||
21034209a5 | |||
7c04c0f83c | |||
f5293d65dd | |||
8191bf6066 | |||
29b6bfd463 | |||
2f3d23bc34 | |||
98dd80c4b8 | |||
d9edb2e128 | |||
de04b40b86 | |||
7206a3d926 | |||
98b98d8938 | |||
294b8fcc2f | |||
32f601a1b1 | |||
8e3a398080 | |||
720b9688b4 | |||
e3066161c5 | |||
a9aa3a524f | |||
92848e818a | |||
a7aa3008c0 | |||
485325e824 | |||
0df2a0e039 | |||
fb7b4466c2 | |||
53105245f4 | |||
b68eab91e0 | |||
f72cf91462 | |||
a655c4ef50 | |||
5d46e9b796 | |||
642e6e67b3 | |||
2b6b86f903 | |||
f2706a4426 | |||
1af75413aa | |||
adc4077b1a | |||
01f4e2453b | |||
61267374dd | |||
970f814685 | |||
4fd9fc1991 | |||
26207ffd1b | |||
2614cfbe5f | |||
f11d461ec0 | |||
b2eb2d2755 | |||
ea46a54f19 | |||
627add45ad | |||
d364855459 | |||
14ee35e77a | |||
8773eb4ee0 | |||
51425bbfd9 | |||
6a4879bc0b | |||
e7fe56439b | |||
2886feab4a | |||
fb91194026 | |||
4527478b0d | |||
b0062f146e | |||
bf16561170 | |||
3b23dc29c4 | |||
c0645b670e | |||
30613fe530 | |||
e7822f6edc | |||
7e5c9ba115 | |||
647fa3a481 | |||
538eca9297 | |||
e73a23d4a4 | |||
76e41a0383 | |||
7045927666 | |||
5fb3086b42 | |||
2de18227c1 | |||
bd12a1de3d | |||
a3a91050c8 | |||
6c1cc28e49 | |||
b6e5b54f05 | |||
a6e559ec67 | |||
f11badb824 | |||
44da44ff6f | |||
aae3fc29ca | |||
c30aa19d8b | |||
c79e8712d0 | |||
ed12d78a48 | |||
d27872cc8b | |||
f794c39760 | |||
2be9c425f7 | |||
ab5fea9216 | |||
d1c8328080 | |||
89e761f53b | |||
40410eba63 | |||
85466a8e80 | |||
c210af7870 | |||
38ff49d28f | |||
710f9f52a8 | |||
1b6eda5a40 | |||
1ee9ccf098 | |||
0f07562de3 | |||
6eb1aa07f5 | |||
05b02834af | |||
56125f6df8 | |||
8f9996848b | |||
dd655ed60f | |||
8923c6ff7d | |||
807ab22f8e | |||
a26ad229ee | |||
5504dd2251 | |||
f9777d29f4 | |||
b23226e8c6 | |||
1249ad29e0 | |||
7bb5e5b632 | |||
b2e43f9765 | |||
2aa73c203a | |||
18d2b56563 | |||
a455b4ad6e | |||
761b683a81 | |||
1d41bcd825 | |||
f1ac038b55 | |||
396c636e42 | |||
e32c858e90 | |||
821555f3f1 | |||
089ab4f0d9 | |||
948b6292fe |
24
README.md
24
README.md
@ -4,11 +4,11 @@ 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.
|
||||
|
||||
The current stable release - 0.2.5 is avaiable for download at http://muwire.com. You can find technical documentation in the "doc" folder.
|
||||
The current stable release - 0.4.14 is avaiable for download at https://muwire.com. You can find technical documentation in the "doc" folder.
|
||||
|
||||
### Building
|
||||
|
||||
You need JRE 8 or newer. After installing that and setting up the appropriate paths, just type
|
||||
You need JDK 8 or newer. After installing that and setting up the appropriate paths, just type
|
||||
|
||||
```
|
||||
./gradlew clean assemble
|
||||
@ -19,15 +19,23 @@ If you want to run the unit tests, type
|
||||
./gradlew clean build
|
||||
```
|
||||
|
||||
Some of the UI tests will fail because they haven't been written yet :-/
|
||||
If you want to build binary bundles that do not depend on Java or I2P, see the https://github.com/zlatinb/muwire-pkg project
|
||||
|
||||
### Running
|
||||
|
||||
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.port=<port>" in there.
|
||||
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.
|
||||
|
||||
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.
|
||||
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`
|
||||
|
||||
[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
|
||||
|
||||
|
||||
### Known bugs and limitations
|
||||
|
||||
* Many UI features you would expect are not there yet
|
||||
[Default I2CP port]: https://geti2p.net/en/docs/ports
|
||||
|
21
TODO.md
21
TODO.md
@ -4,10 +4,6 @@ Not in any particular order yet
|
||||
|
||||
### 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
|
||||
|
||||
This reduces query traffic by not sending last hop queries to peers that definitely do not have the file
|
||||
@ -16,18 +12,6 @@ This reduces query traffic by not sending last hop queries to peers that definit
|
||||
|
||||
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.
|
||||
|
||||
Basically any non-gui non-cli user interface
|
||||
@ -38,8 +22,5 @@ To enable parsing of metadata from known file types and the user editing it or a
|
||||
|
||||
### Small Items
|
||||
|
||||
* Detect if router is dead and show warning or exit
|
||||
* Wrapper of some kind for in-place upgrades
|
||||
* Download file sequentially
|
||||
* Unsharing of files
|
||||
* Multiple-selection download, Ctrl-A
|
||||
* Automatic adjustment of number of I2P tunnels
|
||||
|
@ -2,7 +2,7 @@ subprojects {
|
||||
apply plugin: 'groovy'
|
||||
|
||||
dependencies {
|
||||
compile 'net.i2p:i2p:0.9.40'
|
||||
compile 'net.i2p:i2p:0.9.42'
|
||||
compile 'org.codehaus.groovy:groovy-all:2.4.15'
|
||||
}
|
||||
|
||||
|
@ -35,7 +35,7 @@ class Cli {
|
||||
|
||||
Core core
|
||||
try {
|
||||
core = new Core(props, home, "0.3.0")
|
||||
core = new Core(props, home, "0.4.15")
|
||||
} catch (Exception bad) {
|
||||
bad.printStackTrace(System.out)
|
||||
println "Failed to initialize core, exiting"
|
||||
|
@ -53,7 +53,7 @@ class CliDownloader {
|
||||
|
||||
Core core
|
||||
try {
|
||||
core = new Core(props, home, "0.3.0")
|
||||
core = new Core(props, home, "0.4.15")
|
||||
} catch (Exception bad) {
|
||||
bad.printStackTrace(System.out)
|
||||
println "Failed to initialize core, exiting"
|
||||
|
@ -2,8 +2,9 @@ apply plugin : 'application'
|
||||
mainClassName = 'com.muwire.core.Core'
|
||||
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
|
||||
dependencies {
|
||||
compile 'net.i2p.client:mstreaming:0.9.40'
|
||||
compile 'net.i2p.client:streaming:0.9.40'
|
||||
compile 'net.i2p:router:0.9.42'
|
||||
compile 'net.i2p.client:mstreaming:0.9.42'
|
||||
compile 'net.i2p.client:streaming:0.9.42'
|
||||
|
||||
testCompile 'org.junit.jupiter:junit-jupiter-api:5.4.2'
|
||||
testCompile 'junit:junit:4.12'
|
||||
|
@ -1,15 +0,0 @@
|
||||
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,6 +1,7 @@
|
||||
package com.muwire.core
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
import com.muwire.core.connection.ConnectionAcceptor
|
||||
import com.muwire.core.connection.ConnectionEstablisher
|
||||
@ -12,10 +13,14 @@ import com.muwire.core.connection.I2PConnector
|
||||
import com.muwire.core.connection.LeafConnectionManager
|
||||
import com.muwire.core.connection.UltrapeerConnectionManager
|
||||
import com.muwire.core.download.DownloadManager
|
||||
import com.muwire.core.download.SourceDiscoveredEvent
|
||||
import com.muwire.core.download.UIDownloadCancelledEvent
|
||||
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.FileHashedEvent
|
||||
import com.muwire.core.files.FileHashingEvent
|
||||
import com.muwire.core.files.FileHasher
|
||||
import com.muwire.core.files.FileLoadedEvent
|
||||
import com.muwire.core.files.FileManager
|
||||
@ -23,20 +28,28 @@ import com.muwire.core.files.FileSharedEvent
|
||||
import com.muwire.core.files.FileUnsharedEvent
|
||||
import com.muwire.core.files.HasherService
|
||||
import com.muwire.core.files.PersisterService
|
||||
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.HostCache
|
||||
import com.muwire.core.hostcache.HostDiscoveredEvent
|
||||
import com.muwire.core.mesh.MeshManager
|
||||
import com.muwire.core.search.QueryEvent
|
||||
import com.muwire.core.search.ResultsEvent
|
||||
import com.muwire.core.search.ResultsSender
|
||||
import com.muwire.core.search.SearchEvent
|
||||
import com.muwire.core.search.SearchManager
|
||||
import com.muwire.core.search.UIResultBatchEvent
|
||||
import com.muwire.core.trust.TrustEvent
|
||||
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.upload.UploadManager
|
||||
import com.muwire.core.util.MuWireLogManager
|
||||
import com.muwire.core.content.ContentControlEvent
|
||||
import com.muwire.core.content.ContentManager
|
||||
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.I2PAppContext
|
||||
@ -45,6 +58,7 @@ import net.i2p.client.I2PSession
|
||||
import net.i2p.client.streaming.I2PSocketManager
|
||||
import net.i2p.client.streaming.I2PSocketManagerFactory
|
||||
import net.i2p.client.streaming.I2PSocketOptions
|
||||
import net.i2p.client.streaming.I2PSocketManager.DisconnectListener
|
||||
import net.i2p.crypto.DSAEngine
|
||||
import net.i2p.crypto.SigType
|
||||
import net.i2p.data.Destination
|
||||
@ -52,6 +66,9 @@ import net.i2p.data.PrivateKey
|
||||
import net.i2p.data.Signature
|
||||
import net.i2p.data.SigningPrivateKey
|
||||
|
||||
import net.i2p.router.Router
|
||||
import net.i2p.router.RouterContext
|
||||
|
||||
@Log
|
||||
public class Core {
|
||||
|
||||
@ -62,6 +79,7 @@ public class Core {
|
||||
final MuWireSettings muOptions
|
||||
|
||||
private final TrustService trustService
|
||||
private final TrustSubscriber trustSubscriber
|
||||
private final PersisterService persisterService
|
||||
private final HostCache hostCache
|
||||
private final ConnectionManager connectionManager
|
||||
@ -73,23 +91,16 @@ public class Core {
|
||||
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) {
|
||||
this.home = home
|
||||
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()
|
||||
def i2pOptionsFile = new File(home,"i2p.properties")
|
||||
@ -98,8 +109,8 @@ public class Core {
|
||||
|
||||
if (!i2pOptions.containsKey("inbound.nickname"))
|
||||
i2pOptions["inbound.nickname"] = "MuWire"
|
||||
if (!i2pOptions.containsKey("outbound.nickname"))
|
||||
i2pOptions["outbound.nickname"] = "MuWire"
|
||||
if (!i2pOptions.containsKey("outbound.nickname"))
|
||||
i2pOptions["outbound.nickname"] = "MuWire"
|
||||
} else {
|
||||
i2pOptions["inbound.nickname"] = "MuWire"
|
||||
i2pOptions["outbound.nickname"] = "MuWire"
|
||||
@ -109,17 +120,58 @@ public class Core {
|
||||
i2pOptions["outbound.quantity"] = "4"
|
||||
i2pOptions["i2cp.tcp.host"] = "127.0.0.1"
|
||||
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
|
||||
I2PSession i2pSession
|
||||
I2PSocketManager socketManager
|
||||
keyDat.withInputStream {
|
||||
socketManager = new I2PSocketManagerFactory().createManager(it, i2pOptions["i2cp.tcp.host"], i2pOptions["i2cp.tcp.port"].toInteger(), i2pOptions)
|
||||
}
|
||||
socketManager.getDefaultOptions().setReadTimeout(60000)
|
||||
socketManager.getDefaultOptions().setConnectTimeout(30000)
|
||||
i2pSession = socketManager.getSession()
|
||||
I2PSession i2pSession
|
||||
I2PSocketManager socketManager
|
||||
keyDat.withInputStream {
|
||||
socketManager = new I2PSocketManagerFactory().createManager(it, i2pOptions["i2cp.tcp.host"], i2pOptions["i2cp.tcp.port"].toInteger(), i2pOptions)
|
||||
}
|
||||
socketManager.getDefaultOptions().setReadTimeout(60000)
|
||||
socketManager.getDefaultOptions().setConnectTimeout(30000)
|
||||
socketManager.addDisconnectListener({eventBus.publish(new RouterDisconnectedEvent())} as DisconnectListener)
|
||||
i2pSession = socketManager.getSession()
|
||||
|
||||
def destination = new Destination()
|
||||
def spk = new SigningPrivateKey(Constants.SIG_TYPE)
|
||||
@ -128,7 +180,7 @@ public class Core {
|
||||
def privateKey = new PrivateKey()
|
||||
privateKey.readBytes(it)
|
||||
spk.readBytes(it)
|
||||
}
|
||||
}
|
||||
|
||||
def baos = new ByteArrayOutputStream()
|
||||
def daos = new DataOutputStream(baos)
|
||||
@ -146,89 +198,110 @@ public class Core {
|
||||
me = new Persona(new ByteArrayInputStream(baos.toByteArray()))
|
||||
log.info("Loaded myself as "+me.getHumanReadableName())
|
||||
|
||||
eventBus = new EventBus()
|
||||
eventBus = new EventBus()
|
||||
|
||||
log.info("initializing trust service")
|
||||
File goodTrust = new File(home, "trusted")
|
||||
File badTrust = new File(home, "distrusted")
|
||||
trustService = new TrustService(goodTrust, badTrust, 5000)
|
||||
eventBus.register(TrustEvent.class, trustService)
|
||||
log.info("initializing trust service")
|
||||
File goodTrust = new File(home, "trusted")
|
||||
File badTrust = new File(home, "distrusted")
|
||||
trustService = new TrustService(goodTrust, badTrust, 5000)
|
||||
eventBus.register(TrustEvent.class, trustService)
|
||||
|
||||
|
||||
log.info "initializing file manager"
|
||||
fileManager = new FileManager(eventBus, props)
|
||||
eventBus.register(FileHashedEvent.class, fileManager)
|
||||
eventBus.register(FileLoadedEvent.class, fileManager)
|
||||
eventBus.register(FileDownloadedEvent.class, fileManager)
|
||||
eventBus.register(FileUnsharedEvent.class, fileManager)
|
||||
eventBus.register(SearchEvent.class, fileManager)
|
||||
log.info "initializing file manager"
|
||||
fileManager = new FileManager(eventBus, props)
|
||||
eventBus.register(FileHashedEvent.class, fileManager)
|
||||
eventBus.register(FileLoadedEvent.class, fileManager)
|
||||
eventBus.register(FileDownloadedEvent.class, fileManager)
|
||||
eventBus.register(FileUnsharedEvent.class, fileManager)
|
||||
eventBus.register(SearchEvent.class, fileManager)
|
||||
eventBus.register(DirectoryUnsharedEvent.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)
|
||||
eventBus.register(SourceDiscoveredEvent.class, meshManager)
|
||||
|
||||
log.info "initializing persistence service"
|
||||
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 60000, fileManager)
|
||||
eventBus.register(UILoadedEvent.class, persisterService)
|
||||
|
||||
log.info("initializing host cache")
|
||||
File hostStorage = new File(home, "hosts.json")
|
||||
log.info("initializing host cache")
|
||||
File hostStorage = new File(home, "hosts.json")
|
||||
hostCache = new HostCache(trustService,hostStorage, 30000, props, i2pSession.getMyDestination())
|
||||
eventBus.register(HostDiscoveredEvent.class, hostCache)
|
||||
eventBus.register(ConnectionEvent.class, hostCache)
|
||||
eventBus.register(HostDiscoveredEvent.class, hostCache)
|
||||
eventBus.register(ConnectionEvent.class, hostCache)
|
||||
|
||||
log.info("initializing connection manager")
|
||||
connectionManager = props.isLeaf() ?
|
||||
new LeafConnectionManager(eventBus, me, 3, hostCache, props) :
|
||||
log.info("initializing connection manager")
|
||||
connectionManager = props.isLeaf() ?
|
||||
new LeafConnectionManager(eventBus, me, 3, hostCache, props) :
|
||||
new UltrapeerConnectionManager(eventBus, me, 512, 512, hostCache, trustService, props)
|
||||
eventBus.register(TrustEvent.class, connectionManager)
|
||||
eventBus.register(ConnectionEvent.class, connectionManager)
|
||||
eventBus.register(DisconnectionEvent.class, connectionManager)
|
||||
eventBus.register(TrustEvent.class, connectionManager)
|
||||
eventBus.register(ConnectionEvent.class, connectionManager)
|
||||
eventBus.register(DisconnectionEvent.class, connectionManager)
|
||||
eventBus.register(QueryEvent.class, connectionManager)
|
||||
|
||||
log.info("initializing cache client")
|
||||
cacheClient = new CacheClient(eventBus,hostCache, connectionManager, i2pSession, props, 10000)
|
||||
log.info("initializing cache client")
|
||||
cacheClient = new CacheClient(eventBus,hostCache, connectionManager, i2pSession, props, 10000)
|
||||
|
||||
log.info("initializing update client")
|
||||
updateClient = new UpdateClient(eventBus, i2pSession, myVersion, props)
|
||||
updateClient = new UpdateClient(eventBus, i2pSession, myVersion, props, fileManager, me)
|
||||
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)
|
||||
|
||||
log.info "initializing search manager"
|
||||
SearchManager searchManager = new SearchManager(eventBus, me, resultsSender)
|
||||
eventBus.register(QueryEvent.class, searchManager)
|
||||
eventBus.register(ResultsEvent.class, searchManager)
|
||||
log.info "initializing search manager"
|
||||
SearchManager searchManager = new SearchManager(eventBus, me, resultsSender)
|
||||
eventBus.register(QueryEvent.class, searchManager)
|
||||
eventBus.register(ResultsEvent.class, searchManager)
|
||||
|
||||
log.info("initializing download manager")
|
||||
downloadManager = new DownloadManager(eventBus, i2pConnector, home, me)
|
||||
downloadManager = new DownloadManager(eventBus, trustService, meshManager, props, i2pConnector, home, me)
|
||||
eventBus.register(UIDownloadEvent.class, downloadManager)
|
||||
eventBus.register(UILoadedEvent.class, downloadManager)
|
||||
eventBus.register(FileDownloadedEvent.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")
|
||||
UploadManager uploadManager = new UploadManager(eventBus, fileManager)
|
||||
uploadManager = new UploadManager(eventBus, fileManager, meshManager, downloadManager)
|
||||
|
||||
log.info("initializing connection establisher")
|
||||
connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache)
|
||||
|
||||
log.info("initializing acceptor")
|
||||
I2PAcceptor i2pAcceptor = new I2PAcceptor(socketManager)
|
||||
connectionAcceptor = new ConnectionAcceptor(eventBus, connectionManager, props,
|
||||
log.info("initializing acceptor")
|
||||
I2PAcceptor i2pAcceptor = new I2PAcceptor(socketManager)
|
||||
connectionAcceptor = new ConnectionAcceptor(eventBus, connectionManager, props,
|
||||
i2pAcceptor, hostCache, trustService, searchManager, uploadManager, connectionEstablisher)
|
||||
|
||||
log.info("initializing directory watcher")
|
||||
directoryWatcher = new DirectoryWatcher(eventBus, fileManager)
|
||||
eventBus.register(FileSharedEvent.class, directoryWatcher)
|
||||
eventBus.register(AllFilesLoadedEvent.class, directoryWatcher)
|
||||
eventBus.register(DirectoryUnsharedEvent.class, directoryWatcher)
|
||||
|
||||
log.info("initializing hasher service")
|
||||
hasherService = new HasherService(new FileHasher(), eventBus, fileManager)
|
||||
eventBus.register(FileSharedEvent.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)
|
||||
}
|
||||
|
||||
public void startServices() {
|
||||
hasherService.start()
|
||||
directoryWatcher.start()
|
||||
trustService.start()
|
||||
trustService.waitForLoad()
|
||||
hostCache.start()
|
||||
@ -241,6 +314,12 @@ public class Core {
|
||||
}
|
||||
|
||||
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")
|
||||
downloadManager.shutdown()
|
||||
log.info("shutting down connection acceeptor")
|
||||
@ -249,8 +328,14 @@ public class Core {
|
||||
connectionEstablisher.stop()
|
||||
log.info("shutting down directory watcher")
|
||||
directoryWatcher.stop()
|
||||
log.info("shutting down cache client")
|
||||
cacheClient.stop()
|
||||
log.info("shutting down connection manager")
|
||||
connectionManager.shutdown()
|
||||
if (router != null) {
|
||||
log.info("shutting down embedded router")
|
||||
router.shutdown(0)
|
||||
}
|
||||
}
|
||||
|
||||
static main(args) {
|
||||
@ -277,7 +362,7 @@ public class Core {
|
||||
}
|
||||
}
|
||||
|
||||
Core core = new Core(props, home, "0.3.0")
|
||||
Core core = new Core(props, home, "0.4.15")
|
||||
core.startServices()
|
||||
|
||||
// ... at the end, sleep or execute script
|
||||
|
@ -4,17 +4,17 @@ import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
class Event {
|
||||
|
||||
private static final AtomicLong SEQ_NO = new AtomicLong();
|
||||
final long seqNo
|
||||
final long timestamp
|
||||
private static final AtomicLong SEQ_NO = new AtomicLong();
|
||||
final long seqNo
|
||||
final long timestamp
|
||||
|
||||
Event() {
|
||||
seqNo = SEQ_NO.getAndIncrement()
|
||||
timestamp = System.currentTimeMillis()
|
||||
}
|
||||
Event() {
|
||||
seqNo = SEQ_NO.getAndIncrement()
|
||||
timestamp = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
"seqNo $seqNo timestamp $timestamp"
|
||||
}
|
||||
@Override
|
||||
public String toString() {
|
||||
"seqNo $seqNo timestamp $timestamp"
|
||||
}
|
||||
}
|
||||
|
@ -11,41 +11,46 @@ import groovy.util.logging.Log
|
||||
@Log
|
||||
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()
|
||||
private final Executor executor = Executors.newSingleThreadExecutor {r ->
|
||||
def rv = new Thread(r)
|
||||
rv.setDaemon(true)
|
||||
rv.setName("event-bus")
|
||||
rv
|
||||
}
|
||||
|
||||
void publish(Event e) {
|
||||
executor.execute({publishInternal(e)} as Runnable)
|
||||
}
|
||||
void publish(Event e) {
|
||||
executor.execute({publishInternal(e)} as Runnable)
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
it."on${clazz.getSimpleName()}"(e)
|
||||
} catch (Exception bad) {
|
||||
log.log(Level.SEVERE, "exception dispatching event",bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
synchronized void register(Class<? extends Event> eventType, def handler) {
|
||||
log.info "Registering $handler for type $eventType"
|
||||
def currentHandlers = handlers.get(eventType)
|
||||
if (currentHandlers == null) {
|
||||
currentHandlers = new CopyOnWriteArrayList()
|
||||
handlers.put(eventType, currentHandlers)
|
||||
}
|
||||
currentHandlers.add handler
|
||||
}
|
||||
synchronized void register(Class<? extends Event> eventType, def handler) {
|
||||
log.info "Registering $handler for type $eventType"
|
||||
def currentHandlers = handlers.get(eventType)
|
||||
if (currentHandlers == null) {
|
||||
currentHandlers = new CopyOnWriteArrayList()
|
||||
handlers.put(eventType, currentHandlers)
|
||||
}
|
||||
currentHandlers.add handler
|
||||
}
|
||||
|
||||
synchronized void unregister(Class<? extends Event> eventType, def handler) {
|
||||
log.info("Unregistering $handler for type $eventType")
|
||||
handlers[eventType]?.remove(handler)
|
||||
}
|
||||
}
|
||||
|
@ -11,77 +11,144 @@ class MuWireSettings {
|
||||
|
||||
final boolean isLeaf
|
||||
boolean allowUntrusted
|
||||
boolean searchExtraHop
|
||||
boolean allowTrustLists
|
||||
int trustListInterval
|
||||
Set<Persona> trustSubscriptions
|
||||
int downloadRetryInterval
|
||||
int updateCheckInterval
|
||||
boolean autoDownloadUpdate
|
||||
String updateType
|
||||
String nickname
|
||||
File downloadLocation
|
||||
CrawlerResponse crawlerResponse
|
||||
boolean shareDownloadedFiles
|
||||
Set<String> watchedDirectories
|
||||
float downloadSequentialRatio
|
||||
int hostClearInterval, hostHopelessInterval, hostRejectInterval
|
||||
int meshExpiration
|
||||
boolean embeddedRouter
|
||||
int inBw, outBw
|
||||
Set<String> watchedKeywords
|
||||
Set<String> watchedRegexes
|
||||
|
||||
MuWireSettings() {
|
||||
MuWireSettings() {
|
||||
this(new Properties())
|
||||
}
|
||||
|
||||
MuWireSettings(Properties props) {
|
||||
isLeaf = Boolean.valueOf(props.get("leaf","false"))
|
||||
allowUntrusted = Boolean.valueOf(props.get("allowUntrusted","true"))
|
||||
crawlerResponse = CrawlerResponse.valueOf(props.get("crawlerResponse","REGISTERED"))
|
||||
MuWireSettings(Properties props) {
|
||||
isLeaf = Boolean.valueOf(props.get("leaf","false"))
|
||||
allowUntrusted = Boolean.valueOf(props.getProperty("allowUntrusted","true"))
|
||||
searchExtraHop = Boolean.valueOf(props.getProperty("searchExtraHop","false"))
|
||||
allowTrustLists = Boolean.valueOf(props.getProperty("allowTrustLists","true"))
|
||||
trustListInterval = Integer.valueOf(props.getProperty("trustListInterval","1"))
|
||||
crawlerResponse = CrawlerResponse.valueOf(props.get("crawlerResponse","REGISTERED"))
|
||||
nickname = props.getProperty("nickname","MuWireUser")
|
||||
downloadLocation = new File((String)props.getProperty("downloadLocation",
|
||||
System.getProperty("user.home")))
|
||||
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","5"))
|
||||
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"))
|
||||
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"))
|
||||
|
||||
watchedDirectories = new HashSet<>()
|
||||
if (props.containsKey("watchedDirectories")) {
|
||||
String[] encoded = props.getProperty("watchedDirectories").split(",")
|
||||
encoded.each { watchedDirectories << DataUtil.readi18nString(Base64.decode(it)) }
|
||||
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 {
|
||||
Properties props = new Properties()
|
||||
props.setProperty("leaf", isLeaf.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("nickname", nickname)
|
||||
props.setProperty("downloadLocation", downloadLocation.getAbsolutePath())
|
||||
props.setProperty("downloadRetryInterval", String.valueOf(downloadRetryInterval))
|
||||
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("downloadSequentialRatio", String.valueOf(downloadSequentialRatio))
|
||||
props.setProperty("hostClearInterval", String.valueOf(hostClearInterval))
|
||||
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))
|
||||
|
||||
if (!watchedDirectories.isEmpty()) {
|
||||
String encoded = watchedDirectories.stream().
|
||||
map({Base64.encode(DataUtil.encodei18nString(it))}).
|
||||
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("watchedDirectories", encoded)
|
||||
props.setProperty("trustSubscriptions", encoded)
|
||||
}
|
||||
|
||||
props.store(out, "")
|
||||
}
|
||||
|
||||
boolean isLeaf() {
|
||||
isLeaf
|
||||
}
|
||||
private static Set<String> readEncodedSet(Properties props, String property) {
|
||||
Set<String> rv = new HashSet<>()
|
||||
if (props.containsKey(property)) {
|
||||
String[] encoded = props.getProperty(property).split(",")
|
||||
encoded.each { rv << DataUtil.readi18nString(Base64.decode(it)) }
|
||||
}
|
||||
rv
|
||||
}
|
||||
|
||||
boolean allowUntrusted() {
|
||||
allowUntrusted
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
void setAllowUntrusted(boolean allowUntrusted) {
|
||||
this.allowUntrusted = allowUntrusted
|
||||
}
|
||||
boolean isLeaf() {
|
||||
isLeaf
|
||||
}
|
||||
|
||||
CrawlerResponse getCrawlerResponse() {
|
||||
crawlerResponse
|
||||
}
|
||||
boolean allowUntrusted() {
|
||||
allowUntrusted
|
||||
}
|
||||
|
||||
void setCrawlerResponse(CrawlerResponse crawlerResponse) {
|
||||
this.crawlerResponse = crawlerResponse
|
||||
}
|
||||
void setAllowUntrusted(boolean allowUntrusted) {
|
||||
this.allowUntrusted = allowUntrusted
|
||||
}
|
||||
|
||||
CrawlerResponse getCrawlerResponse() {
|
||||
crawlerResponse
|
||||
}
|
||||
|
||||
void setCrawlerResponse(CrawlerResponse crawlerResponse) {
|
||||
this.crawlerResponse = crawlerResponse
|
||||
}
|
||||
|
||||
String getNickname() {
|
||||
nickname
|
||||
|
@ -82,4 +82,13 @@ public class Persona {
|
||||
Persona other = (Persona)o
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,4 @@
|
||||
package com.muwire.core
|
||||
|
||||
class RouterDisconnectedEvent extends Event {
|
||||
}
|
@ -2,12 +2,12 @@ package com.muwire.core
|
||||
|
||||
abstract class Service {
|
||||
|
||||
volatile boolean loaded
|
||||
volatile boolean loaded
|
||||
|
||||
abstract void load()
|
||||
abstract void load()
|
||||
|
||||
void waitForLoad() {
|
||||
while (!loaded)
|
||||
Thread.sleep(10)
|
||||
}
|
||||
void waitForLoad() {
|
||||
while (!loaded)
|
||||
Thread.sleep(10)
|
||||
}
|
||||
}
|
||||
|
7
core/src/main/groovy/com/muwire/core/SplitPattern.groovy
Normal file
7
core/src/main/groovy/com/muwire/core/SplitPattern.groovy
Normal file
@ -0,0 +1,7 @@
|
||||
package com.muwire.core
|
||||
|
||||
class SplitPattern {
|
||||
|
||||
public static final String SPLIT_PATTERN = "[\\*\\+\\-,\\.:;\\(\\)=_/\\\\\\!\\\"\\\'\\\$%\\|\\[\\]\\{\\}\\?]";
|
||||
|
||||
}
|
@ -22,103 +22,107 @@ import net.i2p.data.Destination
|
||||
@Log
|
||||
abstract class Connection implements Closeable {
|
||||
|
||||
final EventBus eventBus
|
||||
final Endpoint endpoint
|
||||
final boolean incoming
|
||||
final HostCache hostCache
|
||||
private static final int SEARCHES = 10
|
||||
private static final long INTERVAL = 1000
|
||||
|
||||
final EventBus eventBus
|
||||
final Endpoint endpoint
|
||||
final boolean incoming
|
||||
final HostCache hostCache
|
||||
final TrustService trustService
|
||||
final MuWireSettings settings
|
||||
|
||||
private final AtomicBoolean running = new AtomicBoolean()
|
||||
private final BlockingQueue messages = new LinkedBlockingQueue()
|
||||
private final Thread reader, writer
|
||||
private final AtomicBoolean running = new AtomicBoolean()
|
||||
private final BlockingQueue messages = new LinkedBlockingQueue()
|
||||
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) {
|
||||
this.eventBus = eventBus
|
||||
this.incoming = incoming
|
||||
this.endpoint = endpoint
|
||||
this.hostCache = hostCache
|
||||
this.eventBus = eventBus
|
||||
this.incoming = incoming
|
||||
this.endpoint = endpoint
|
||||
this.hostCache = hostCache
|
||||
this.trustService = trustService
|
||||
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.setName("reader-$name")
|
||||
this.reader.setDaemon(true)
|
||||
this.reader = new Thread({readLoop()} as Runnable)
|
||||
this.reader.setName("reader-$name")
|
||||
this.reader.setDaemon(true)
|
||||
|
||||
this.writer = new Thread({writeLoop()} as Runnable)
|
||||
this.writer.setName("writer-$name")
|
||||
this.writer.setDaemon(true)
|
||||
}
|
||||
this.writer = new Thread({writeLoop()} as Runnable)
|
||||
this.writer.setName("writer-$name")
|
||||
this.writer.setDaemon(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* starts the connection threads
|
||||
*/
|
||||
void start() {
|
||||
if (!running.compareAndSet(false, true)) {
|
||||
log.log(Level.WARNING,"$name already running", new Exception())
|
||||
return
|
||||
}
|
||||
reader.start()
|
||||
writer.start()
|
||||
}
|
||||
/**
|
||||
* starts the connection threads
|
||||
*/
|
||||
void start() {
|
||||
if (!running.compareAndSet(false, true)) {
|
||||
log.log(Level.WARNING,"$name already running", new Exception())
|
||||
return
|
||||
}
|
||||
reader.start()
|
||||
writer.start()
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (!running.compareAndSet(true, false)) {
|
||||
log.log(Level.WARNING, "$name already closed", new Exception() )
|
||||
return
|
||||
}
|
||||
@Override
|
||||
public void close() {
|
||||
if (!running.compareAndSet(true, false)) {
|
||||
log.log(Level.WARNING, "$name already closed", new Exception() )
|
||||
return
|
||||
}
|
||||
log.info("closing $name")
|
||||
reader.interrupt()
|
||||
writer.interrupt()
|
||||
endpoint.close()
|
||||
eventBus.publish(new DisconnectionEvent(destination: endpoint.destination))
|
||||
}
|
||||
reader.interrupt()
|
||||
writer.interrupt()
|
||||
endpoint.close()
|
||||
eventBus.publish(new DisconnectionEvent(destination: endpoint.destination))
|
||||
}
|
||||
|
||||
protected void readLoop() {
|
||||
try {
|
||||
while(running.get()) {
|
||||
read()
|
||||
}
|
||||
} catch (SocketTimeoutException e) {
|
||||
protected void readLoop() {
|
||||
try {
|
||||
while(running.get()) {
|
||||
read()
|
||||
}
|
||||
} catch (SocketTimeoutException e) {
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING,"unhandled exception in reader",e)
|
||||
} finally {
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void read()
|
||||
protected abstract void read()
|
||||
|
||||
protected void writeLoop() {
|
||||
try {
|
||||
while(running.get()) {
|
||||
def message = messages.take()
|
||||
write(message)
|
||||
}
|
||||
} catch (Exception e) {
|
||||
protected void writeLoop() {
|
||||
try {
|
||||
while(running.get()) {
|
||||
def message = messages.take()
|
||||
write(message)
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING, "unhandled exception in writer",e)
|
||||
} finally {
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void write(def message);
|
||||
protected abstract void write(def message);
|
||||
|
||||
void sendPing() {
|
||||
def ping = [:]
|
||||
ping.type = "Ping"
|
||||
ping.version = 1
|
||||
messages.put(ping)
|
||||
lastPingSentTime = System.currentTimeMillis()
|
||||
}
|
||||
void sendPing() {
|
||||
def ping = [:]
|
||||
ping.type = "Ping"
|
||||
ping.version = 1
|
||||
messages.put(ping)
|
||||
lastPingSentTime = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
void sendQuery(QueryEvent e) {
|
||||
def query = [:]
|
||||
@ -136,27 +140,45 @@ abstract class Connection implements Closeable {
|
||||
messages.put(query)
|
||||
}
|
||||
|
||||
protected void handlePing() {
|
||||
log.fine("$name received ping")
|
||||
def pong = [:]
|
||||
pong.type = "Pong"
|
||||
pong.version = 1
|
||||
pong.pongs = hostCache.getGoodHosts(10).collect { d -> d.toBase64() }
|
||||
messages.put(pong)
|
||||
}
|
||||
protected void handlePing() {
|
||||
log.fine("$name received ping")
|
||||
def pong = [:]
|
||||
pong.type = "Pong"
|
||||
pong.version = 1
|
||||
pong.pongs = hostCache.getGoodHosts(10).collect { d -> d.toBase64() }
|
||||
messages.put(pong)
|
||||
}
|
||||
|
||||
protected void handlePong(def pong) {
|
||||
log.fine("$name received pong")
|
||||
lastPongReceivedTime = System.currentTimeMillis()
|
||||
if (pong.pongs == null)
|
||||
throw new Exception("Pong doesn't have pongs")
|
||||
pong.pongs.each {
|
||||
def dest = new Destination(it)
|
||||
eventBus.publish(new HostDiscoveredEvent(destination: dest))
|
||||
}
|
||||
}
|
||||
protected void handlePong(def pong) {
|
||||
log.fine("$name received pong")
|
||||
lastPongReceivedTime = System.currentTimeMillis()
|
||||
if (pong.pongs == null)
|
||||
throw new Exception("Pong doesn't have pongs")
|
||||
pong.pongs.each {
|
||||
def dest = new Destination(it)
|
||||
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) {
|
||||
if (throttleSearch()) {
|
||||
log.info("dropping excessive search")
|
||||
return
|
||||
}
|
||||
UUID uuid = UUID.fromString(search.uuid)
|
||||
byte [] infohash = null
|
||||
if (search.infohash != null) {
|
||||
|
@ -14,6 +14,7 @@ import com.muwire.core.hostcache.HostCache
|
||||
import com.muwire.core.trust.TrustLevel
|
||||
import com.muwire.core.trust.TrustService
|
||||
import com.muwire.core.upload.UploadManager
|
||||
import com.muwire.core.util.DataUtil
|
||||
import com.muwire.core.search.InvalidSearchResultException
|
||||
import com.muwire.core.search.ResultsParser
|
||||
import com.muwire.core.search.SearchManager
|
||||
@ -28,125 +29,128 @@ import groovy.util.logging.Log
|
||||
@Log
|
||||
class ConnectionAcceptor {
|
||||
|
||||
final EventBus eventBus
|
||||
final UltrapeerConnectionManager manager
|
||||
final MuWireSettings settings
|
||||
final I2PAcceptor acceptor
|
||||
final HostCache hostCache
|
||||
final TrustService trustService
|
||||
final EventBus eventBus
|
||||
final UltrapeerConnectionManager manager
|
||||
final MuWireSettings settings
|
||||
final I2PAcceptor acceptor
|
||||
final HostCache hostCache
|
||||
final TrustService trustService
|
||||
final SearchManager searchManager
|
||||
final UploadManager uploadManager
|
||||
final ConnectionEstablisher establisher
|
||||
|
||||
final ExecutorService acceptorThread
|
||||
final ExecutorService handshakerThreads
|
||||
final ExecutorService acceptorThread
|
||||
final ExecutorService handshakerThreads
|
||||
|
||||
private volatile shutdown
|
||||
|
||||
ConnectionAcceptor(EventBus eventBus, UltrapeerConnectionManager manager,
|
||||
MuWireSettings settings, I2PAcceptor acceptor, HostCache hostCache,
|
||||
TrustService trustService, SearchManager searchManager, UploadManager uploadManager,
|
||||
ConnectionAcceptor(EventBus eventBus, UltrapeerConnectionManager manager,
|
||||
MuWireSettings settings, I2PAcceptor acceptor, HostCache hostCache,
|
||||
TrustService trustService, SearchManager searchManager, UploadManager uploadManager,
|
||||
ConnectionEstablisher establisher) {
|
||||
this.eventBus = eventBus
|
||||
this.manager = manager
|
||||
this.settings = settings
|
||||
this.acceptor = acceptor
|
||||
this.hostCache = hostCache
|
||||
this.trustService = trustService
|
||||
this.eventBus = eventBus
|
||||
this.manager = manager
|
||||
this.settings = settings
|
||||
this.acceptor = acceptor
|
||||
this.hostCache = hostCache
|
||||
this.trustService = trustService
|
||||
this.searchManager = searchManager
|
||||
this.uploadManager = uploadManager
|
||||
this.establisher = establisher
|
||||
this.establisher = establisher
|
||||
|
||||
acceptorThread = Executors.newSingleThreadExecutor { r ->
|
||||
def rv = new Thread(r)
|
||||
rv.setDaemon(true)
|
||||
rv.setName("acceptor")
|
||||
rv
|
||||
}
|
||||
acceptorThread = Executors.newSingleThreadExecutor { r ->
|
||||
def rv = new Thread(r)
|
||||
rv.setDaemon(true)
|
||||
rv.setName("acceptor")
|
||||
rv
|
||||
}
|
||||
|
||||
handshakerThreads = Executors.newCachedThreadPool { r ->
|
||||
def rv = new Thread(r)
|
||||
rv.setDaemon(true)
|
||||
rv.setName("acceptor-processor-${System.currentTimeMillis()}")
|
||||
rv
|
||||
}
|
||||
}
|
||||
handshakerThreads = Executors.newCachedThreadPool { r ->
|
||||
def rv = new Thread(r)
|
||||
rv.setDaemon(true)
|
||||
rv.setName("acceptor-processor-${System.currentTimeMillis()}")
|
||||
rv
|
||||
}
|
||||
}
|
||||
|
||||
void start() {
|
||||
acceptorThread.execute({acceptLoop()} as Runnable)
|
||||
}
|
||||
void start() {
|
||||
acceptorThread.execute({acceptLoop()} as Runnable)
|
||||
}
|
||||
|
||||
void stop() {
|
||||
void stop() {
|
||||
shutdown = true
|
||||
acceptorThread.shutdownNow()
|
||||
handshakerThreads.shutdownNow()
|
||||
}
|
||||
acceptorThread.shutdownNow()
|
||||
handshakerThreads.shutdownNow()
|
||||
}
|
||||
|
||||
private void acceptLoop() {
|
||||
private void acceptLoop() {
|
||||
try {
|
||||
while(true) {
|
||||
def incoming = acceptor.accept()
|
||||
log.info("accepted connection from ${incoming.destination.toBase32()}")
|
||||
switch(trustService.getLevel(incoming.destination)) {
|
||||
case TrustLevel.TRUSTED : break
|
||||
case TrustLevel.NEUTRAL :
|
||||
if (settings.allowUntrusted())
|
||||
break
|
||||
case TrustLevel.DISTRUSTED :
|
||||
log.info("Disallowing distrusted connection")
|
||||
incoming.close()
|
||||
continue
|
||||
}
|
||||
handshakerThreads.execute({processIncoming(incoming)} as Runnable)
|
||||
}
|
||||
while(true) {
|
||||
def incoming = acceptor.accept()
|
||||
log.info("accepted connection from ${incoming.destination.toBase32()}")
|
||||
switch(trustService.getLevel(incoming.destination)) {
|
||||
case TrustLevel.TRUSTED : break
|
||||
case TrustLevel.NEUTRAL :
|
||||
if (settings.allowUntrusted())
|
||||
break
|
||||
case TrustLevel.DISTRUSTED :
|
||||
log.info("Disallowing distrusted connection")
|
||||
incoming.close()
|
||||
continue
|
||||
}
|
||||
handshakerThreads.execute({processIncoming(incoming)} as Runnable)
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING, "exception in accept loop",e)
|
||||
if (!shutdown)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processIncoming(Endpoint e) {
|
||||
InputStream is = e.inputStream
|
||||
try {
|
||||
int read = is.read()
|
||||
switch(read) {
|
||||
case (byte)'M':
|
||||
private void processIncoming(Endpoint e) {
|
||||
InputStream is = e.inputStream
|
||||
try {
|
||||
int read = is.read()
|
||||
switch(read) {
|
||||
case (byte)'M':
|
||||
if (settings.isLeaf())
|
||||
throw new IOException("Incoming connection as leaf")
|
||||
processMuWire(e)
|
||||
break
|
||||
case (byte)'G':
|
||||
processGET(e)
|
||||
break
|
||||
processMuWire(e)
|
||||
break
|
||||
case (byte)'G':
|
||||
processGET(e)
|
||||
break
|
||||
case (byte)'H':
|
||||
processHashList(e)
|
||||
break
|
||||
case (byte)'P':
|
||||
processPOST(e)
|
||||
break
|
||||
default:
|
||||
throw new Exception("Invalid read $read")
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.log(Level.WARNING, "incoming connection failed",ex)
|
||||
e.close()
|
||||
eventBus.publish new ConnectionEvent(endpoint: e, incoming: true, leaf: null, status: ConnectionAttemptStatus.FAILED)
|
||||
}
|
||||
}
|
||||
case (byte)'T':
|
||||
processTRUST(e)
|
||||
break
|
||||
default:
|
||||
throw new Exception("Invalid read $read")
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.log(Level.WARNING, "incoming connection failed",ex)
|
||||
e.close()
|
||||
eventBus.publish new ConnectionEvent(endpoint: e, incoming: true, leaf: null, status: ConnectionAttemptStatus.FAILED)
|
||||
}
|
||||
}
|
||||
|
||||
private void processMuWire(Endpoint e) {
|
||||
byte[] uWire = "uWire ".bytes
|
||||
for (int i = 0; i < uWire.length; i++) {
|
||||
int read = e.inputStream.read()
|
||||
if (read != uWire[i]) {
|
||||
throw new IOException("unexpected value $read at position $i")
|
||||
}
|
||||
}
|
||||
private void processMuWire(Endpoint e) {
|
||||
byte[] uWire = "uWire ".bytes
|
||||
for (int i = 0; i < uWire.length; i++) {
|
||||
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)
|
||||
byte[] type = new byte[4]
|
||||
DataInputStream dis = new DataInputStream(e.inputStream)
|
||||
dis.readFully(type)
|
||||
|
||||
if (type == "leaf".bytes)
|
||||
handleIncoming(e, true)
|
||||
@ -156,44 +160,44 @@ class ConnectionAcceptor {
|
||||
throw new IOException("unknown connection type $type")
|
||||
}
|
||||
|
||||
private void handleIncoming(Endpoint e, boolean leaf) {
|
||||
boolean accept = !manager.isConnected(e.destination) &&
|
||||
private void handleIncoming(Endpoint e, boolean leaf) {
|
||||
boolean accept = !manager.isConnected(e.destination) &&
|
||||
!establisher.isInProgress(e.destination) &&
|
||||
(leaf ? manager.hasLeafSlots() : manager.hasPeerSlots())
|
||||
if (accept) {
|
||||
log.info("accepting connection, leaf:$leaf")
|
||||
e.outputStream.write("OK".bytes)
|
||||
e.outputStream.flush()
|
||||
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))
|
||||
} else {
|
||||
log.info("rejecting connection, leaf:$leaf")
|
||||
e.outputStream.write("REJECT".bytes)
|
||||
def hosts = hostCache.getGoodHosts(10)
|
||||
if (!hosts.isEmpty()) {
|
||||
def json = [:]
|
||||
json.tryHosts = hosts.collect { d -> d.toBase64() }
|
||||
json = JsonOutput.toJson(json)
|
||||
def os = new DataOutputStream(e.outputStream)
|
||||
os.writeShort(json.bytes.length)
|
||||
os.write(json.bytes)
|
||||
}
|
||||
e.outputStream.flush()
|
||||
e.close()
|
||||
eventBus.publish(new ConnectionEvent(endpoint: e, incoming: true, leaf: leaf, status: ConnectionAttemptStatus.REJECTED))
|
||||
}
|
||||
}
|
||||
if (accept) {
|
||||
log.info("accepting connection, leaf:$leaf")
|
||||
e.outputStream.write("OK".bytes)
|
||||
e.outputStream.flush()
|
||||
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))
|
||||
} else {
|
||||
log.info("rejecting connection, leaf:$leaf")
|
||||
e.outputStream.write("REJECT".bytes)
|
||||
def hosts = hostCache.getGoodHosts(10)
|
||||
if (!hosts.isEmpty()) {
|
||||
def json = [:]
|
||||
json.tryHosts = hosts.collect { d -> d.toBase64() }
|
||||
json = JsonOutput.toJson(json)
|
||||
def os = new DataOutputStream(e.outputStream)
|
||||
os.writeShort(json.bytes.length)
|
||||
os.write(json.bytes)
|
||||
}
|
||||
e.outputStream.flush()
|
||||
e.close()
|
||||
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]
|
||||
final DataInputStream dis = new DataInputStream(e.getInputStream())
|
||||
dis.readFully(et)
|
||||
if (et != "ET ".getBytes(StandardCharsets.US_ASCII))
|
||||
throw new IOException("Invalid GET connection")
|
||||
uploadManager.processGET(e)
|
||||
}
|
||||
}
|
||||
|
||||
private void processHashList(Endpoint e) {
|
||||
byte[] ashList = new byte[8]
|
||||
@ -243,4 +247,43 @@ class ConnectionAcceptor {
|
||||
}
|
||||
}
|
||||
|
||||
private void processTRUST(Endpoint e) {
|
||||
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()
|
||||
e.close()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -22,162 +22,167 @@ import net.i2p.util.ConcurrentHashSet
|
||||
@Log
|
||||
class ConnectionEstablisher {
|
||||
|
||||
private static final int CONCURRENT = 4
|
||||
private static final int CONCURRENT = 4
|
||||
|
||||
final EventBus eventBus
|
||||
final I2PConnector i2pConnector
|
||||
final MuWireSettings settings
|
||||
final ConnectionManager connectionManager
|
||||
final HostCache hostCache
|
||||
final EventBus eventBus
|
||||
final I2PConnector i2pConnector
|
||||
final MuWireSettings settings
|
||||
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(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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
closer = Executors.newSingleThreadExecutor()
|
||||
}
|
||||
|
||||
void stop() {
|
||||
timer.cancel()
|
||||
executor.shutdownNow()
|
||||
}
|
||||
void start() {
|
||||
timer.schedule({connectIfNeeded()} as TimerTask, 100, 1000)
|
||||
}
|
||||
|
||||
private void connectIfNeeded() {
|
||||
if (!connectionManager.needsConnections())
|
||||
return
|
||||
if (inProgress.size() >= CONCURRENT)
|
||||
return
|
||||
void stop() {
|
||||
timer.cancel()
|
||||
executor.shutdownNow()
|
||||
closer.shutdown()
|
||||
}
|
||||
|
||||
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 connectIfNeeded() {
|
||||
if (!connectionManager.needsConnections())
|
||||
return
|
||||
if (inProgress.size() >= CONCURRENT)
|
||||
return
|
||||
|
||||
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()}")
|
||||
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)
|
||||
}
|
||||
|
||||
// outgoing handshake
|
||||
endpoint.outputStream.write("MuWire ".bytes)
|
||||
def type = settings.isLeaf() ? "leaf" : "peer"
|
||||
endpoint.outputStream.write(type.bytes)
|
||||
endpoint.outputStream.flush()
|
||||
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()}")
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
// outgoing handshake
|
||||
endpoint.outputStream.write("MuWire ".bytes)
|
||||
def type = settings.isLeaf() ? "leaf" : "peer"
|
||||
endpoint.outputStream.write(type.bytes)
|
||||
endpoint.outputStream.flush()
|
||||
|
||||
private void fail(Endpoint endpoint) {
|
||||
endpoint.close()
|
||||
eventBus.publish(new ConnectionEvent(endpoint: endpoint, incoming: false, leaf: false, status: ConnectionAttemptStatus.FAILED))
|
||||
}
|
||||
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 readK(Endpoint e) {
|
||||
int read = e.inputStream.read()
|
||||
if (read != 'K') {
|
||||
log.warning("unknown response after O: $read")
|
||||
fail e
|
||||
return
|
||||
}
|
||||
private void fail(Endpoint endpoint) {
|
||||
closer.execute {
|
||||
endpoint.close()
|
||||
eventBus.publish(new ConnectionEvent(endpoint: endpoint, incoming: false, leaf: false, status: ConnectionAttemptStatus.FAILED))
|
||||
} as Runnable
|
||||
}
|
||||
|
||||
log.info("connection to ${e.destination.toBase32()} established")
|
||||
private void readK(Endpoint e) {
|
||||
int read = e.inputStream.read()
|
||||
if (read != 'K') {
|
||||
log.warning("unknown response after O: $read")
|
||||
fail e
|
||||
return
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
log.info("connection to ${e.destination.toBase32()} established")
|
||||
|
||||
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")
|
||||
// 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)
|
||||
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)
|
||||
def json = new JsonSlurper()
|
||||
json = json.parse(payload)
|
||||
|
||||
if (json.tryHosts == null) {
|
||||
log.warning("post-rejection json didn't contain hosts to try")
|
||||
return
|
||||
}
|
||||
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
|
||||
e.close()
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isInProgress(Destination d) {
|
||||
inProgress.contains(d)
|
||||
|
@ -6,14 +6,14 @@ import net.i2p.data.Destination
|
||||
|
||||
class ConnectionEvent extends Event {
|
||||
|
||||
Endpoint endpoint
|
||||
boolean incoming
|
||||
Boolean leaf // can be null if uknown
|
||||
ConnectionAttemptStatus status
|
||||
Endpoint endpoint
|
||||
boolean incoming
|
||||
Boolean leaf // can be null if uknown
|
||||
ConnectionAttemptStatus status
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
"ConnectionEvent ${super.toString()} endpoint: $endpoint incoming: $incoming leaf : $leaf status : $status"
|
||||
}
|
||||
@Override
|
||||
public String toString() {
|
||||
"ConnectionEvent ${super.toString()} endpoint: $endpoint incoming: $incoming leaf : $leaf status : $status"
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -12,63 +12,63 @@ import net.i2p.data.Destination
|
||||
|
||||
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 MuWireSettings settings
|
||||
|
||||
ConnectionManager() {}
|
||||
ConnectionManager() {}
|
||||
|
||||
ConnectionManager(EventBus eventBus, Persona me, HostCache hostCache, MuWireSettings settings) {
|
||||
this.eventBus = eventBus
|
||||
ConnectionManager(EventBus eventBus, Persona me, HostCache hostCache, MuWireSettings settings) {
|
||||
this.eventBus = eventBus
|
||||
this.me = me
|
||||
this.hostCache = hostCache
|
||||
this.hostCache = hostCache
|
||||
this.settings = settings
|
||||
this.timer = new Timer("connections-pinger",true)
|
||||
}
|
||||
this.timer = new Timer("connections-pinger",true)
|
||||
}
|
||||
|
||||
void start() {
|
||||
timer.schedule({sendPings()} as TimerTask, 1000,1000)
|
||||
}
|
||||
void start() {
|
||||
timer.schedule({sendPings()} as TimerTask, 1000,1000)
|
||||
}
|
||||
|
||||
void stop() {
|
||||
timer.cancel()
|
||||
getConnections().each { it.close() }
|
||||
}
|
||||
void stop() {
|
||||
timer.cancel()
|
||||
getConnections().each { it.close() }
|
||||
}
|
||||
|
||||
void onTrustEvent(TrustEvent e) {
|
||||
if (e.level == TrustLevel.DISTRUSTED)
|
||||
drop(e.persona.destination)
|
||||
}
|
||||
void onTrustEvent(TrustEvent e) {
|
||||
if (e.level == TrustLevel.DISTRUSTED)
|
||||
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() {
|
||||
return getConnections().size() < getDesiredConnections()
|
||||
}
|
||||
boolean needsConnections() {
|
||||
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()
|
||||
|
||||
protected void sendPings() {
|
||||
final long now = System.currentTimeMillis()
|
||||
getConnections().each {
|
||||
if (now - it.lastPingSentTime > PING_TIME)
|
||||
it.sendPing()
|
||||
}
|
||||
}
|
||||
protected void sendPings() {
|
||||
final long now = System.currentTimeMillis()
|
||||
getConnections().each {
|
||||
if (now - it.lastPingSentTime > PING_TIME)
|
||||
it.sendPing()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,10 +6,10 @@ import net.i2p.data.Destination
|
||||
|
||||
class DisconnectionEvent extends Event {
|
||||
|
||||
Destination destination
|
||||
Destination destination
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
"DisconnectionEvent ${super.toString()} destination:${destination.toBase32()}"
|
||||
}
|
||||
@Override
|
||||
public String toString() {
|
||||
"DisconnectionEvent ${super.toString()} destination:${destination.toBase32()}"
|
||||
}
|
||||
}
|
||||
|
@ -8,39 +8,39 @@ import net.i2p.data.Destination
|
||||
|
||||
@Log
|
||||
class Endpoint implements Closeable {
|
||||
final Destination destination
|
||||
final InputStream inputStream
|
||||
final OutputStream outputStream
|
||||
final Destination destination
|
||||
final InputStream inputStream
|
||||
final OutputStream outputStream
|
||||
final def toClose
|
||||
|
||||
private final AtomicBoolean closed = new AtomicBoolean()
|
||||
private final AtomicBoolean closed = new AtomicBoolean()
|
||||
|
||||
Endpoint(Destination destination, InputStream inputStream, OutputStream outputStream, def toClose) {
|
||||
this.destination = destination
|
||||
this.inputStream = inputStream
|
||||
this.outputStream = outputStream
|
||||
Endpoint(Destination destination, InputStream inputStream, OutputStream outputStream, def toClose) {
|
||||
this.destination = destination
|
||||
this.inputStream = inputStream
|
||||
this.outputStream = outputStream
|
||||
this.toClose = toClose
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (!closed.compareAndSet(false, true)) {
|
||||
log.log(Level.WARNING,"Close loop detected for ${destination.toBase32()}", new Exception())
|
||||
return
|
||||
}
|
||||
if (inputStream != null) {
|
||||
try {inputStream.close()} catch (Exception ignore) {}
|
||||
}
|
||||
if (outputStream != null) {
|
||||
try {outputStream.close()} catch (Exception ignore) {}
|
||||
}
|
||||
@Override
|
||||
public void close() {
|
||||
if (!closed.compareAndSet(false, true)) {
|
||||
log.log(Level.WARNING,"Close loop detected for ${destination.toBase32()}", new Exception())
|
||||
return
|
||||
}
|
||||
if (inputStream != null) {
|
||||
try {inputStream.close()} catch (Exception ignore) {}
|
||||
}
|
||||
if (outputStream != null) {
|
||||
try {outputStream.close()} catch (Exception ignore) {}
|
||||
}
|
||||
if (toClose != null) {
|
||||
try {toClose.reset()} catch (Exception ignore) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
"destination: ${destination.toBase32()}"
|
||||
}
|
||||
@Override
|
||||
public String toString() {
|
||||
"destination: ${destination.toBase32()}"
|
||||
}
|
||||
}
|
||||
|
@ -5,18 +5,18 @@ import net.i2p.client.streaming.I2PSocketManager
|
||||
|
||||
class I2PAcceptor {
|
||||
|
||||
final I2PSocketManager socketManager
|
||||
final I2PServerSocket serverSocket
|
||||
final I2PSocketManager socketManager
|
||||
final I2PServerSocket serverSocket
|
||||
|
||||
I2PAcceptor() {}
|
||||
I2PAcceptor() {}
|
||||
|
||||
I2PAcceptor(I2PSocketManager socketManager) {
|
||||
this.socketManager = socketManager
|
||||
this.serverSocket = socketManager.getServerSocket()
|
||||
}
|
||||
I2PAcceptor(I2PSocketManager socketManager) {
|
||||
this.socketManager = socketManager
|
||||
this.serverSocket = socketManager.getServerSocket()
|
||||
}
|
||||
|
||||
Endpoint accept() {
|
||||
def socket = serverSocket.accept()
|
||||
new Endpoint(socket.getPeerDestination(), socket.getInputStream(), socket.getOutputStream(), socket)
|
||||
}
|
||||
Endpoint accept() {
|
||||
def socket = serverSocket.accept()
|
||||
new Endpoint(socket.getPeerDestination(), socket.getInputStream(), socket.getOutputStream(), socket)
|
||||
}
|
||||
}
|
||||
|
@ -5,17 +5,17 @@ import net.i2p.data.Destination
|
||||
|
||||
class I2PConnector {
|
||||
|
||||
final I2PSocketManager socketManager
|
||||
final I2PSocketManager socketManager
|
||||
|
||||
I2PConnector() {}
|
||||
I2PConnector() {}
|
||||
|
||||
I2PConnector(I2PSocketManager socketManager) {
|
||||
this.socketManager = socketManager
|
||||
}
|
||||
I2PConnector(I2PSocketManager socketManager) {
|
||||
this.socketManager = socketManager
|
||||
}
|
||||
|
||||
Endpoint connect(Destination dest) {
|
||||
def socket = socketManager.connect(dest)
|
||||
new Endpoint(dest, socket.getInputStream(), socket.getOutputStream(), socket)
|
||||
}
|
||||
Endpoint connect(Destination dest) {
|
||||
def socket = socketManager.connect(dest)
|
||||
new Endpoint(dest, socket.getInputStream(), socket.getOutputStream(), socket)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -17,21 +17,21 @@ import net.i2p.data.Destination
|
||||
*/
|
||||
class LeafConnection extends Connection {
|
||||
|
||||
public LeafConnection(EventBus eventBus, Endpoint endpoint, HostCache hostCache,
|
||||
public LeafConnection(EventBus eventBus, Endpoint endpoint, HostCache hostCache,
|
||||
TrustService trustService, MuWireSettings settings) {
|
||||
super(eventBus, endpoint, true, hostCache, trustService, settings);
|
||||
}
|
||||
super(eventBus, endpoint, true, hostCache, trustService, settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void read() {
|
||||
// TODO Auto-generated method stub
|
||||
@Override
|
||||
protected void read() {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void write(Object message) {
|
||||
// TODO Auto-generated method stub
|
||||
@Override
|
||||
protected void write(Object message) {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -14,21 +14,21 @@ import net.i2p.data.Destination
|
||||
@Log
|
||||
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) {
|
||||
super(eventBus, me, hostCache, settings)
|
||||
this.maxConnections = maxConnections
|
||||
}
|
||||
super(eventBus, me, hostCache, settings)
|
||||
this.maxConnections = maxConnections
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drop(Destination d) {
|
||||
// TODO Auto-generated method stub
|
||||
@Override
|
||||
public void drop(Destination d) {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void onQueryEvent(QueryEvent e) {
|
||||
if (me.destination == e.receivedOn) {
|
||||
@ -37,41 +37,41 @@ class LeafConnectionManager extends ConnectionManager {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Connection> getConnections() {
|
||||
connections.values()
|
||||
}
|
||||
@Override
|
||||
public Collection<Connection> getConnections() {
|
||||
connections.values()
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getDesiredConnections() {
|
||||
return maxConnections;
|
||||
}
|
||||
@Override
|
||||
protected int getDesiredConnections() {
|
||||
return maxConnections;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConnected(Destination d) {
|
||||
connections.containsKey(d)
|
||||
}
|
||||
@Override
|
||||
public boolean isConnected(Destination 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
|
||||
@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()
|
||||
}
|
||||
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 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
|
||||
void shutdown() {
|
||||
|
@ -21,62 +21,62 @@ import net.i2p.data.Destination
|
||||
@Log
|
||||
class PeerConnection extends Connection {
|
||||
|
||||
private final DataInputStream dis
|
||||
private final DataOutputStream dos
|
||||
private final DataInputStream dis
|
||||
private final DataOutputStream dos
|
||||
|
||||
private final byte[] readHeader = new byte[3]
|
||||
private final byte[] writeHeader = new byte[3]
|
||||
private final byte[] readHeader = new byte[3]
|
||||
private final byte[] writeHeader = new byte[3]
|
||||
|
||||
private final JsonSlurper slurper = new JsonSlurper()
|
||||
private final JsonSlurper slurper = new JsonSlurper()
|
||||
|
||||
public PeerConnection(EventBus eventBus, Endpoint endpoint,
|
||||
boolean incoming, HostCache hostCache, TrustService trustService,
|
||||
public PeerConnection(EventBus eventBus, Endpoint endpoint,
|
||||
boolean incoming, HostCache hostCache, TrustService trustService,
|
||||
MuWireSettings settings) {
|
||||
super(eventBus, endpoint, incoming, hostCache, trustService, settings)
|
||||
this.dis = new DataInputStream(endpoint.inputStream)
|
||||
this.dos = new DataOutputStream(endpoint.outputStream)
|
||||
}
|
||||
super(eventBus, endpoint, incoming, hostCache, trustService, settings)
|
||||
this.dis = new DataInputStream(endpoint.inputStream)
|
||||
this.dos = new DataOutputStream(endpoint.outputStream)
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void read() {
|
||||
dis.readFully(readHeader)
|
||||
int length = DataUtil.readLength(readHeader)
|
||||
log.fine("$name read length $length")
|
||||
@Override
|
||||
protected void read() {
|
||||
dis.readFully(readHeader)
|
||||
int length = DataUtil.readLength(readHeader)
|
||||
log.fine("$name read length $length")
|
||||
|
||||
byte[] payload = new byte[length]
|
||||
dis.readFully(payload)
|
||||
byte[] payload = new byte[length]
|
||||
dis.readFully(payload)
|
||||
|
||||
if ((readHeader[0] & (byte)0x80) == 0x80) {
|
||||
// TODO process binary
|
||||
} else {
|
||||
def json = slurper.parse(payload)
|
||||
if (json.type == null)
|
||||
throw new Exception("missing json type")
|
||||
switch(json.type) {
|
||||
case "Ping" : handlePing(); break;
|
||||
case "Pong" : handlePong(json); break;
|
||||
if ((readHeader[0] & (byte)0x80) == 0x80) {
|
||||
// TODO process binary
|
||||
} else {
|
||||
def json = slurper.parse(payload)
|
||||
if (json.type == null)
|
||||
throw new Exception("missing json type")
|
||||
switch(json.type) {
|
||||
case "Ping" : handlePing(); break;
|
||||
case "Pong" : handlePong(json); break;
|
||||
case "Search": handleSearch(json); break
|
||||
default :
|
||||
throw new Exception("unknown json type ${json.type}")
|
||||
}
|
||||
}
|
||||
}
|
||||
default :
|
||||
throw new Exception("unknown json type ${json.type}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void write(Object message) {
|
||||
byte[] payload
|
||||
if (message instanceof Map) {
|
||||
payload = JsonOutput.toJson(message).bytes
|
||||
DataUtil.packHeader(payload.length, writeHeader)
|
||||
log.fine "$name writing message type ${message.type} length $payload.length"
|
||||
writeHeader[0] &= (byte)0x7F
|
||||
} else {
|
||||
// TODO: write binary
|
||||
}
|
||||
@Override
|
||||
protected void write(Object message) {
|
||||
byte[] payload
|
||||
if (message instanceof Map) {
|
||||
payload = JsonOutput.toJson(message).bytes
|
||||
DataUtil.packHeader(payload.length, writeHeader)
|
||||
log.fine "$name writing message type ${message.type} length $payload.length"
|
||||
writeHeader[0] &= (byte)0x7F
|
||||
} else {
|
||||
// TODO: write binary
|
||||
}
|
||||
|
||||
dos.write(writeHeader)
|
||||
dos.write(payload)
|
||||
dos.flush()
|
||||
}
|
||||
dos.write(writeHeader)
|
||||
dos.write(payload)
|
||||
dos.flush()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -17,30 +17,30 @@ import net.i2p.data.Destination
|
||||
*/
|
||||
class UltrapeerConnection extends Connection {
|
||||
|
||||
public UltrapeerConnection(EventBus eventBus, Endpoint endpoint, HostCache hostCache, TrustService trustService) {
|
||||
super(eventBus, endpoint, false, hostCache, trustService)
|
||||
}
|
||||
public UltrapeerConnection(EventBus eventBus, Endpoint endpoint, HostCache hostCache, TrustService trustService) {
|
||||
super(eventBus, endpoint, false, hostCache, trustService)
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void read() {
|
||||
// TODO Auto-generated method stub
|
||||
@Override
|
||||
protected void read() {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void write(Object message) {
|
||||
if (message instanceof Map) {
|
||||
writeJsonMessage(message)
|
||||
} else {
|
||||
writeBinaryMessage(message)
|
||||
}
|
||||
}
|
||||
@Override
|
||||
protected void write(Object message) {
|
||||
if (message instanceof Map) {
|
||||
writeJsonMessage(message)
|
||||
} else {
|
||||
writeBinaryMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
private void writeJsonMessage(def message) {
|
||||
private void writeJsonMessage(def message) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void writeBinaryMessage(def message) {
|
||||
private void writeBinaryMessage(def message) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,26 +16,26 @@ import net.i2p.data.Destination
|
||||
@Log
|
||||
class UltrapeerConnectionManager extends ConnectionManager {
|
||||
|
||||
final int maxPeers, maxLeafs
|
||||
final int maxPeers, maxLeafs
|
||||
final TrustService trustService
|
||||
|
||||
final Map<Destination, PeerConnection> peerConnections = new ConcurrentHashMap()
|
||||
final Map<Destination, LeafConnection> leafConnections = new ConcurrentHashMap()
|
||||
final Map<Destination, PeerConnection> peerConnections = new ConcurrentHashMap()
|
||||
final Map<Destination, LeafConnection> leafConnections = new ConcurrentHashMap()
|
||||
|
||||
UltrapeerConnectionManager() {}
|
||||
UltrapeerConnectionManager() {}
|
||||
|
||||
public UltrapeerConnectionManager(EventBus eventBus, Persona me, int maxPeers, int maxLeafs,
|
||||
public UltrapeerConnectionManager(EventBus eventBus, Persona me, int maxPeers, int maxLeafs,
|
||||
HostCache hostCache, TrustService trustService, MuWireSettings settings) {
|
||||
super(eventBus, me, hostCache, settings)
|
||||
this.maxPeers = maxPeers
|
||||
this.maxLeafs = maxLeafs
|
||||
super(eventBus, me, hostCache, settings)
|
||||
this.maxPeers = maxPeers
|
||||
this.maxLeafs = maxLeafs
|
||||
this.trustService = trustService
|
||||
}
|
||||
@Override
|
||||
public void drop(Destination d) {
|
||||
peerConnections.get(d)?.close()
|
||||
}
|
||||
@Override
|
||||
public void drop(Destination d) {
|
||||
peerConnections.get(d)?.close()
|
||||
leafConnections.get(d)?.close()
|
||||
}
|
||||
}
|
||||
|
||||
void onQueryEvent(QueryEvent e) {
|
||||
forwardQueryToLeafs(e)
|
||||
@ -50,57 +50,57 @@ class UltrapeerConnectionManager extends ConnectionManager {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Connection> getConnections() {
|
||||
def rv = new ArrayList(peerConnections.size() + leafConnections.size())
|
||||
rv.addAll(peerConnections.values())
|
||||
rv.addAll(leafConnections.values())
|
||||
rv
|
||||
}
|
||||
@Override
|
||||
public Collection<Connection> getConnections() {
|
||||
def rv = new ArrayList(peerConnections.size() + leafConnections.size())
|
||||
rv.addAll(peerConnections.values())
|
||||
rv.addAll(leafConnections.values())
|
||||
rv
|
||||
}
|
||||
|
||||
boolean hasLeafSlots() {
|
||||
leafConnections.size() < maxLeafs
|
||||
}
|
||||
boolean hasLeafSlots() {
|
||||
leafConnections.size() < maxLeafs
|
||||
}
|
||||
|
||||
boolean hasPeerSlots() {
|
||||
peerConnections.size() < maxPeers
|
||||
}
|
||||
boolean hasPeerSlots() {
|
||||
peerConnections.size() < maxPeers
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getDesiredConnections() {
|
||||
return maxPeers / 2;
|
||||
}
|
||||
@Override
|
||||
public boolean isConnected(Destination d) {
|
||||
peerConnections.containsKey(d) || leafConnections.containsKey(d)
|
||||
}
|
||||
@Override
|
||||
protected int getDesiredConnections() {
|
||||
return maxPeers / 2;
|
||||
}
|
||||
@Override
|
||||
public boolean isConnected(Destination d) {
|
||||
peerConnections.containsKey(d) || leafConnections.containsKey(d)
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionEvent(ConnectionEvent e) {
|
||||
if (!e.incoming && e.leaf) {
|
||||
log.severe("Inconsistent event $e")
|
||||
return
|
||||
}
|
||||
@Override
|
||||
public void onConnectionEvent(ConnectionEvent e) {
|
||||
if (!e.incoming && e.leaf) {
|
||||
log.severe("Inconsistent event $e")
|
||||
return
|
||||
}
|
||||
|
||||
if (e.status != ConnectionAttemptStatus.SUCCESSFUL)
|
||||
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()
|
||||
}
|
||||
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 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
|
||||
void shutdown() {
|
||||
@ -110,7 +110,7 @@ class UltrapeerConnectionManager extends ConnectionManager {
|
||||
leafConnections.clear()
|
||||
}
|
||||
|
||||
void forwardQueryToLeafs(QueryEvent e) {
|
||||
void forwardQueryToLeafs(QueryEvent e) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,9 @@
|
||||
package com.muwire.core.content
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class ContentControlEvent extends Event {
|
||||
String term
|
||||
boolean regex
|
||||
boolean add
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
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) }
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
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)
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package com.muwire.core.content
|
||||
|
||||
import com.muwire.core.Persona
|
||||
|
||||
class Match {
|
||||
Persona persona
|
||||
String [] keywords
|
||||
long timestamp
|
||||
}
|
20
core/src/main/groovy/com/muwire/core/content/Matcher.groovy
Normal file
20
core/src/main/groovy/com/muwire/core/content/Matcher.groovy
Normal file
@ -0,0 +1,20 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
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()
|
||||
}
|
||||
}
|
@ -3,6 +3,10 @@ package com.muwire.core.download
|
||||
import com.muwire.core.connection.I2PConnector
|
||||
import com.muwire.core.files.FileDownloadedEvent
|
||||
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 groovy.json.JsonBuilder
|
||||
@ -14,24 +18,33 @@ import net.i2p.util.ConcurrentHashSet
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.UILoadedEvent
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
public class DownloadManager {
|
||||
|
||||
private final EventBus eventBus
|
||||
private final TrustService trustService
|
||||
private final MeshManager meshManager
|
||||
private final MuWireSettings muSettings
|
||||
private final I2PConnector connector
|
||||
private final Executor executor
|
||||
private final File incompletes, home
|
||||
private final Persona me
|
||||
|
||||
private final Set<Downloader> downloaders = new ConcurrentHashSet<>()
|
||||
private final Map<InfoHash, Downloader> downloaders = new ConcurrentHashMap<>()
|
||||
|
||||
public DownloadManager(EventBus eventBus, I2PConnector connector, File home, Persona me) {
|
||||
public DownloadManager(EventBus eventBus, TrustService trustService, MeshManager meshManager, MuWireSettings muSettings,
|
||||
I2PConnector connector, File home, Persona me) {
|
||||
this.eventBus = eventBus
|
||||
this.trustService = trustService
|
||||
this.meshManager = meshManager
|
||||
this.muSettings = muSettings
|
||||
this.connector = connector
|
||||
this.incompletes = new File(home,"incompletes")
|
||||
this.home = home
|
||||
@ -61,17 +74,27 @@ public class DownloadManager {
|
||||
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,
|
||||
infohash, pieceSize, connector, destinations,
|
||||
incompletes)
|
||||
downloaders.add(downloader)
|
||||
incompletes, pieces)
|
||||
downloaders.put(infohash, downloader)
|
||||
persistDownloaders()
|
||||
executor.execute({downloader.download()} as Runnable)
|
||||
eventBus.publish(new DownloadStartedEvent(downloader : downloader))
|
||||
}
|
||||
|
||||
public void onUIDownloadCancelledEvent(UIDownloadCancelledEvent e) {
|
||||
downloaders.remove(e.downloader)
|
||||
downloaders.remove(e.downloader.infoHash)
|
||||
persistDownloaders()
|
||||
}
|
||||
|
||||
public void onUIDownloadPausedEvent(UIDownloadPausedEvent e) {
|
||||
persistDownloaders()
|
||||
}
|
||||
|
||||
public void onUIDownloadResumedEvent(UIDownloadResumedEvent e) {
|
||||
persistDownloaders()
|
||||
}
|
||||
|
||||
@ -99,23 +122,58 @@ public class DownloadManager {
|
||||
byte [] root = Base64.decode(json.hashRoot)
|
||||
infoHash = new InfoHash(root)
|
||||
}
|
||||
|
||||
boolean sequential = false
|
||||
if (json.sequential != null)
|
||||
sequential = json.sequential
|
||||
|
||||
Pieces pieces = getPieces(infoHash, (long)json.length, json.pieceSizePow2, sequential)
|
||||
|
||||
def downloader = new Downloader(eventBus, this, me, file, (long)json.length,
|
||||
infoHash, json.pieceSizePow2, connector, destinations, incompletes)
|
||||
downloaders.add(downloader)
|
||||
downloader.download()
|
||||
infoHash, json.pieceSizePow2, connector, destinations, incompletes, pieces)
|
||||
if (json.paused != null)
|
||||
downloader.paused = json.paused
|
||||
downloaders.put(infoHash, downloader)
|
||||
downloader.readPieces()
|
||||
if (!downloader.paused)
|
||||
downloader.download()
|
||||
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) {
|
||||
downloaders.remove(e.downloader)
|
||||
downloaders.remove(e.downloader.infoHash)
|
||||
persistDownloaders()
|
||||
}
|
||||
|
||||
private void persistDownloaders() {
|
||||
File downloadsFile = new File(home,"downloads.json")
|
||||
downloadsFile.withPrintWriter { writer ->
|
||||
downloaders.each { downloader ->
|
||||
downloaders.values().each { downloader ->
|
||||
if (!downloader.cancelled) {
|
||||
def json = [:]
|
||||
json.file = Base64.encode(DataUtil.encodei18nString(downloader.file.getAbsolutePath()))
|
||||
@ -132,6 +190,11 @@ public class DownloadManager {
|
||||
json.hashList = Base64.encode(infoHash.hashList)
|
||||
else
|
||||
json.hashRoot = Base64.encode(infoHash.getRoot())
|
||||
|
||||
json.paused = downloader.paused
|
||||
|
||||
json.sequential = downloader.pieces.ratio == 0f
|
||||
|
||||
writer.println(JsonOutput.toJson(json))
|
||||
}
|
||||
}
|
||||
@ -139,7 +202,7 @@ public class DownloadManager {
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
downloaders.each { it.stop() }
|
||||
downloaders.values().each { it.stop() }
|
||||
Downloader.executorService.shutdownNow()
|
||||
}
|
||||
}
|
||||
|
@ -3,13 +3,18 @@ package com.muwire.core.download;
|
||||
import net.i2p.data.Base64
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.connection.Endpoint
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import static com.muwire.core.util.DataUtil.readTillRN
|
||||
|
||||
import groovy.util.logging.Log
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.MappedByteBuffer
|
||||
import java.nio.channels.FileChannel
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.Files
|
||||
@ -21,8 +26,7 @@ import java.util.logging.Level
|
||||
@Log
|
||||
class DownloadSession {
|
||||
|
||||
private static int SAMPLES = 10
|
||||
|
||||
private final EventBus eventBus
|
||||
private final String meB64
|
||||
private final Pieces pieces
|
||||
private final InfoHash infoHash
|
||||
@ -30,15 +34,17 @@ class DownloadSession {
|
||||
private final File file
|
||||
private final int pieceSize
|
||||
private final long fileLength
|
||||
private final Set<Integer> available
|
||||
private final MessageDigest digest
|
||||
|
||||
private final LinkedList<Long> timestamps = new LinkedList<>()
|
||||
private final LinkedList<Integer> reads = new LinkedList<>()
|
||||
private long lastSpeedRead = System.currentTimeMillis()
|
||||
private long dataSinceLastRead
|
||||
|
||||
private ByteBuffer mapped
|
||||
private MappedByteBuffer mapped
|
||||
|
||||
DownloadSession(String meB64, Pieces pieces, InfoHash infoHash, Endpoint endpoint, File file,
|
||||
int pieceSize, long fileLength) {
|
||||
DownloadSession(EventBus eventBus, String meB64, Pieces pieces, InfoHash infoHash, Endpoint endpoint, File file,
|
||||
int pieceSize, long fileLength, Set<Integer> available) {
|
||||
this.eventBus = eventBus
|
||||
this.meB64 = meB64
|
||||
this.pieces = pieces
|
||||
this.endpoint = endpoint
|
||||
@ -46,6 +52,7 @@ class DownloadSession {
|
||||
this.file = file
|
||||
this.pieceSize = pieceSize
|
||||
this.fileLength = fileLength
|
||||
this.available = available
|
||||
try {
|
||||
digest = MessageDigest.getInstance("SHA-256")
|
||||
} catch (NoSuchAlgorithmException impossible) {
|
||||
@ -63,61 +70,101 @@ class DownloadSession {
|
||||
OutputStream os = endpoint.getOutputStream()
|
||||
InputStream is = endpoint.getInputStream()
|
||||
|
||||
int piece = pieces.claim()
|
||||
if (piece == -1)
|
||||
int[] pieceAndPosition
|
||||
if (available.isEmpty())
|
||||
pieceAndPosition = pieces.claim()
|
||||
else
|
||||
pieceAndPosition = pieces.claim(new HashSet<>(available))
|
||||
if (pieceAndPosition == null)
|
||||
return false
|
||||
int piece = pieceAndPosition[0]
|
||||
int position = pieceAndPosition[1]
|
||||
boolean steal = pieceAndPosition[2] == 1
|
||||
boolean unclaim = true
|
||||
|
||||
log.info("will download piece $piece")
|
||||
|
||||
long start = piece * pieceSize
|
||||
long end = Math.min(fileLength, start + pieceSize) - 1
|
||||
long length = end - start + 1
|
||||
log.info("will download piece $piece from position $position steal $steal")
|
||||
|
||||
long pieceStart = piece * ((long)pieceSize)
|
||||
long end = Math.min(fileLength, pieceStart + pieceSize) - 1
|
||||
long start = pieceStart + position
|
||||
String root = Base64.encode(infoHash.getRoot())
|
||||
|
||||
try {
|
||||
os.write("GET $root\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("Range: $start-$end\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("X-Persona: $meB64\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("X-Persona: $meB64\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()
|
||||
String code = readTillRN(is)
|
||||
if (code.startsWith("404 ")) {
|
||||
String codeString = readTillRN(is)
|
||||
int space = codeString.indexOf(' ')
|
||||
if (space > 0)
|
||||
codeString = codeString.substring(0, space)
|
||||
|
||||
int code = Integer.parseInt(codeString.trim())
|
||||
|
||||
if (code == 404) {
|
||||
log.warning("file not found")
|
||||
endpoint.close()
|
||||
return false
|
||||
}
|
||||
|
||||
if (code.startsWith("416 ")) {
|
||||
log.warning("range $start-$end cannot be satisfied")
|
||||
return // leave endpoint open
|
||||
}
|
||||
|
||||
if (!code.startsWith("200 ")) {
|
||||
if (!(code == 200 || code == 416)) {
|
||||
log.warning("unknown code $code")
|
||||
endpoint.close()
|
||||
return false
|
||||
}
|
||||
|
||||
// parse all headers
|
||||
Set<String> headers = new HashSet<>()
|
||||
Map<String,String> headers = new HashMap<>()
|
||||
String header
|
||||
while((header = readTillRN(is)) != "" && headers.size() < Constants.MAX_HEADERS)
|
||||
headers.add(header)
|
||||
|
||||
long receivedStart = -1
|
||||
long receivedEnd = -1
|
||||
for (String receivedHeader : headers) {
|
||||
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])
|
||||
while((header = 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()
|
||||
}
|
||||
|
||||
// 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) {
|
||||
log.warning("We don't support mismatching ranges yet")
|
||||
endpoint.close()
|
||||
@ -128,8 +175,9 @@ class DownloadSession {
|
||||
FileChannel channel
|
||||
try {
|
||||
channel = Files.newByteChannel(file.toPath(), EnumSet.of(StandardOpenOption.READ, StandardOpenOption.WRITE,
|
||||
StandardOpenOption.SPARSE, StandardOpenOption.CREATE)) // TODO: double-check, maybe CREATE_NEW
|
||||
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)
|
||||
mapped.position(position)
|
||||
|
||||
byte[] tmp = new byte[0x1 << 13]
|
||||
while(mapped.hasRemaining()) {
|
||||
@ -140,13 +188,8 @@ class DownloadSession {
|
||||
throw new IOException()
|
||||
synchronized(this) {
|
||||
mapped.put(tmp, 0, read)
|
||||
|
||||
if (timestamps.size() == SAMPLES) {
|
||||
timestamps.removeFirst()
|
||||
reads.removeFirst()
|
||||
}
|
||||
timestamps.addLast(System.currentTimeMillis())
|
||||
reads.addLast(read)
|
||||
dataSinceLastRead += read
|
||||
pieces.markPartial(piece, mapped.position())
|
||||
}
|
||||
}
|
||||
|
||||
@ -155,15 +198,18 @@ class DownloadSession {
|
||||
byte [] hash = digest.digest()
|
||||
byte [] expected = new byte[32]
|
||||
System.arraycopy(infoHash.getHashList(), piece * 32, expected, 0, 32)
|
||||
if (hash != expected)
|
||||
throw new BadHashException()
|
||||
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
|
||||
} finally {
|
||||
if (unclaim)
|
||||
if (unclaim && !steal)
|
||||
pieces.unclaim(piece)
|
||||
}
|
||||
return true
|
||||
@ -176,24 +222,11 @@ class DownloadSession {
|
||||
}
|
||||
|
||||
synchronized int speed() {
|
||||
if (timestamps.size() < SAMPLES)
|
||||
return 0
|
||||
int totalRead = 0
|
||||
int idx = 0
|
||||
final long now = System.currentTimeMillis()
|
||||
|
||||
while(idx < SAMPLES && timestamps.get(idx) < now - 1000)
|
||||
idx++
|
||||
if (idx == SAMPLES)
|
||||
return 0
|
||||
if (idx == SAMPLES - 1)
|
||||
return reads[idx]
|
||||
|
||||
long interval = timestamps.last - timestamps[idx]
|
||||
if (interval == 0)
|
||||
interval = 1
|
||||
for (int i = idx; i < SAMPLES; i++)
|
||||
totalRead += reads[idx]
|
||||
(int)(totalRead * 1000.0 / interval)
|
||||
long interval = Math.max(1000, now - lastSpeedRead)
|
||||
lastSpeedRead = now;
|
||||
int rv = (int) (dataSinceLastRead * 1000.0 / interval)
|
||||
dataSinceLastRead = 0
|
||||
rv
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ 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.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
@ -18,6 +19,7 @@ import com.muwire.core.DownloadedFile
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.connection.I2PConnector
|
||||
import com.muwire.core.files.FileDownloadedEvent
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.data.Destination
|
||||
@ -25,7 +27,7 @@ import net.i2p.util.ConcurrentHashSet
|
||||
|
||||
@Log
|
||||
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 static final ExecutorService executorService = Executors.newCachedThreadPool({r ->
|
||||
@ -53,14 +55,19 @@ public class Downloader {
|
||||
private final Set<Destination> successfulDestinations = new ConcurrentHashSet<>()
|
||||
|
||||
|
||||
private volatile boolean cancelled
|
||||
private volatile boolean cancelled, paused
|
||||
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,
|
||||
File incompletes) {
|
||||
File incompletes, Pieces pieces) {
|
||||
this.eventBus = eventBus
|
||||
this.me = me
|
||||
this.downloadManager = downloadManager
|
||||
@ -73,15 +80,12 @@ public class Downloader {
|
||||
this.incompleteFile = new File(incompletes, file.getName()+".part")
|
||||
this.pieceSizePow2 = 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
|
||||
|
||||
pieces = new Pieces(nPieces, Constants.DOWNLOAD_SEQUENTIAL_RATIO)
|
||||
// default size suitable for an average of 5 seconds / 5 elements / 5 interval units
|
||||
// it's easily adjustable by resizing the size of speedArr
|
||||
this.speedArr = [ 0, 0, 0, 0, 0 ]
|
||||
}
|
||||
|
||||
public synchronized InfoHash getInfoHash() {
|
||||
@ -107,8 +111,14 @@ public class Downloader {
|
||||
if (!piecesFile.exists())
|
||||
return
|
||||
piecesFile.eachLine {
|
||||
int piece = Integer.parseInt(it)
|
||||
pieces.markDownloaded(piece)
|
||||
String [] split = it.split(",")
|
||||
int piece = Integer.parseInt(split[0])
|
||||
if (split.length == 1)
|
||||
pieces.markDownloaded(piece)
|
||||
else {
|
||||
int position = Integer.parseInt(split[1])
|
||||
pieces.markPartial(piece, position)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,9 +127,7 @@ public class Downloader {
|
||||
if (piecesFileClosed)
|
||||
return
|
||||
piecesFile.withPrintWriter { writer ->
|
||||
pieces.getDownloaded().each { piece ->
|
||||
writer.println(piece)
|
||||
}
|
||||
pieces.write(writer)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -130,19 +138,43 @@ public class Downloader {
|
||||
|
||||
|
||||
public int speed() {
|
||||
int total = 0
|
||||
int currSpeed = 0
|
||||
if (getCurrentState() == DownloadState.DOWNLOADING) {
|
||||
activeWorkers.values().each {
|
||||
if (it.currentState == WorkerState.DOWNLOADING)
|
||||
total += it.speed()
|
||||
currSpeed += it.speed()
|
||||
}
|
||||
}
|
||||
total
|
||||
|
||||
// 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() {
|
||||
if (cancelled)
|
||||
return DownloadState.CANCELLED
|
||||
if (paused)
|
||||
return DownloadState.PAUSED
|
||||
|
||||
boolean allFinished = true
|
||||
activeWorkers.values().each {
|
||||
allFinished &= it.currentState == WorkerState.FINISHED
|
||||
@ -187,6 +219,12 @@ public class Downloader {
|
||||
piecesFile.delete()
|
||||
}
|
||||
incompleteFile.delete()
|
||||
pieces.clearAll()
|
||||
}
|
||||
|
||||
public void pause() {
|
||||
paused = true
|
||||
stop()
|
||||
}
|
||||
|
||||
void stop() {
|
||||
@ -205,6 +243,8 @@ public class Downloader {
|
||||
}
|
||||
|
||||
public void resume() {
|
||||
paused = false
|
||||
readPieces()
|
||||
destinations.each { destination ->
|
||||
def worker = activeWorkers.get(destination)
|
||||
if (worker != null) {
|
||||
@ -221,12 +261,21 @@ 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 {
|
||||
private final Destination destination
|
||||
private volatile WorkerState currentState
|
||||
private volatile Thread downloadThread
|
||||
private Endpoint endpoint
|
||||
private volatile DownloadSession currentSession
|
||||
private final Set<Integer> available = new HashSet<>()
|
||||
|
||||
DownloadWorker(Destination destination) {
|
||||
this.destination = destination
|
||||
@ -247,7 +296,8 @@ public class Downloader {
|
||||
currentState = WorkerState.DOWNLOADING
|
||||
boolean requestPerformed
|
||||
while(!pieces.isComplete()) {
|
||||
currentSession = new DownloadSession(me.toBase64(), pieces, getInfoHash(), endpoint, incompleteFile, pieceSize, length)
|
||||
currentSession = new DownloadSession(eventBus, me.toBase64(), pieces, getInfoHash(),
|
||||
endpoint, incompleteFile, pieceSize, length, available)
|
||||
requestPerformed = currentSession.request()
|
||||
if (!requestPerformed)
|
||||
break
|
||||
@ -255,14 +305,19 @@ public class Downloader {
|
||||
writePieces()
|
||||
}
|
||||
} catch (Exception bad) {
|
||||
log.log(Level.WARNING,"Exception while downloading",bad)
|
||||
log.log(Level.WARNING,"Exception while downloading",DataUtil.findRoot(bad))
|
||||
} finally {
|
||||
writePieces()
|
||||
currentState = WorkerState.FINISHED
|
||||
if (pieces.isComplete() && eventFired.compareAndSet(false, true)) {
|
||||
synchronized(piecesFile) {
|
||||
piecesFileClosed = 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) {
|
||||
@ -271,7 +326,7 @@ public class Downloader {
|
||||
}
|
||||
eventBus.publish(
|
||||
new FileDownloadedEvent(
|
||||
downloadedFile : new DownloadedFile(file, getInfoHash(), pieceSizePow2, successfulDestinations),
|
||||
downloadedFile : new DownloadedFile(file.getCanonicalFile(), getInfoHash(), pieceSizePow2, successfulDestinations),
|
||||
downloader : Downloader.this))
|
||||
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ class Pieces {
|
||||
private final int nPieces
|
||||
private final float ratio
|
||||
private final Random random = new Random()
|
||||
private final Map<Integer,Integer> partials = new HashMap<>()
|
||||
|
||||
Pieces(int nPieces) {
|
||||
this(nPieces, 1.0f)
|
||||
@ -17,16 +18,22 @@ class Pieces {
|
||||
claimed = new BitSet(nPieces)
|
||||
}
|
||||
|
||||
synchronized int claim() {
|
||||
synchronized int[] claim() {
|
||||
int claimedCardinality = claimed.cardinality()
|
||||
if (claimedCardinality == nPieces)
|
||||
return -1
|
||||
if (claimedCardinality == nPieces) {
|
||||
// steal
|
||||
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 ( (1.0f * claimedCardinality) / nPieces > ratio) {
|
||||
if ( (1.0f * claimedCardinality) / nPieces >= ratio) {
|
||||
int rv = claimed.nextClearBit(0)
|
||||
claimed.set(rv)
|
||||
return rv
|
||||
return [rv, partials.getOrDefault(rv, 0), 0]
|
||||
}
|
||||
|
||||
while(true) {
|
||||
@ -34,10 +41,31 @@ class Pieces {
|
||||
if (claimed.get(start))
|
||||
continue
|
||||
claimed.set(start)
|
||||
return start
|
||||
return [start, partials.getOrDefault(start,0), 0]
|
||||
}
|
||||
}
|
||||
|
||||
synchronized int[] claim(Set<Integer> available) {
|
||||
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 = []
|
||||
for (int i = done.nextSetBit(0); i >= 0; i = done.nextSetBit(i+1)) {
|
||||
@ -49,6 +77,11 @@ class Pieces {
|
||||
synchronized void markDownloaded(int piece) {
|
||||
done.set(piece)
|
||||
claimed.set(piece)
|
||||
partials.remove(piece)
|
||||
}
|
||||
|
||||
synchronized void markPartial(int piece, int position) {
|
||||
partials.put(piece, position)
|
||||
}
|
||||
|
||||
synchronized void unclaim(int piece) {
|
||||
@ -62,4 +95,23 @@ class Pieces {
|
||||
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) {
|
||||
for (int i = done.nextSetBit(0); i >= 0; i = done.nextSetBit(i+1)) {
|
||||
writer.println(i)
|
||||
}
|
||||
partials.each { piece, position ->
|
||||
writer.println("$piece,$position")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,10 @@
|
||||
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
|
||||
}
|
@ -10,4 +10,5 @@ class UIDownloadEvent extends Event {
|
||||
UIResultEvent[] result
|
||||
Set<Destination> sources
|
||||
File target
|
||||
boolean sequential
|
||||
}
|
||||
|
@ -0,0 +1,6 @@
|
||||
package com.muwire.core.download
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class UIDownloadPausedEvent extends Event {
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package com.muwire.core.download
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class UIDownloadResumedEvent extends Event {
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class DirectoryUnsharedEvent extends Event {
|
||||
File directory
|
||||
}
|
@ -35,6 +35,7 @@ class DirectoryWatcher {
|
||||
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
|
||||
|
||||
@ -47,7 +48,7 @@ class DirectoryWatcher {
|
||||
publisherThread.setDaemon(true)
|
||||
}
|
||||
|
||||
void start() {
|
||||
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
|
||||
watchService = FileSystems.getDefault().newWatchService()
|
||||
watcherThread.start()
|
||||
publisherThread.start()
|
||||
@ -55,19 +56,25 @@ class DirectoryWatcher {
|
||||
|
||||
void stop() {
|
||||
shutdown = true
|
||||
watcherThread.interrupt()
|
||||
publisherThread.interrupt()
|
||||
watchService.close()
|
||||
watcherThread?.interrupt()
|
||||
publisherThread?.interrupt()
|
||||
watchService?.close()
|
||||
}
|
||||
|
||||
void onFileSharedEvent(FileSharedEvent e) {
|
||||
if (!e.file.isDirectory())
|
||||
return
|
||||
Path path = e.file.getCanonicalFile().toPath()
|
||||
path.register(watchService, kinds)
|
||||
WatchKey wk = path.register(watchService, kinds)
|
||||
watchedDirectories.put(e.file, wk)
|
||||
|
||||
}
|
||||
|
||||
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent e) {
|
||||
WatchKey wk = watchedDirectories.remove(e.directory)
|
||||
wk?.cancel()
|
||||
}
|
||||
|
||||
private void watch() {
|
||||
try {
|
||||
while(!shutdown) {
|
||||
@ -113,7 +120,7 @@ class DirectoryWatcher {
|
||||
|
||||
private static File join(Path parent, Path path) {
|
||||
File parentFile = parent.toFile().getCanonicalFile()
|
||||
new File(parentFile, path.toFile().getName())
|
||||
new File(parentFile, path.toFile().getName()).getCanonicalFile()
|
||||
}
|
||||
|
||||
private void publish() {
|
||||
|
@ -8,5 +8,5 @@ import net.i2p.data.Destination
|
||||
|
||||
class FileDownloadedEvent extends Event {
|
||||
Downloader downloader
|
||||
DownloadedFile downloadedFile
|
||||
DownloadedFile downloadedFile
|
||||
}
|
||||
|
@ -5,6 +5,12 @@ import com.muwire.core.SharedFile
|
||||
|
||||
class FileHashedEvent extends Event {
|
||||
|
||||
SharedFile sharedFile
|
||||
String error
|
||||
SharedFile sharedFile
|
||||
String error
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
super.toString() + " sharedFile " + sharedFile?.file.getAbsolutePath() + " error: $error"
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import net.i2p.data.Base64
|
||||
|
||||
@ -12,64 +13,67 @@ import java.security.NoSuchAlgorithmException
|
||||
|
||||
class FileHasher {
|
||||
|
||||
/** max size of shared file is 128 GB */
|
||||
public static final long MAX_SIZE = 0x1L << 37
|
||||
/** max size of shared file is 128 GB */
|
||||
public static final long MAX_SIZE = 0x1L << 37
|
||||
|
||||
/**
|
||||
* @param size of the file to be shared
|
||||
* @return the size of each piece in power of 2
|
||||
*/
|
||||
static int getPieceSize(long size) {
|
||||
if (size <= 0x1 << 30)
|
||||
return 17
|
||||
/**
|
||||
* @param size of the file to be shared
|
||||
* @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)
|
||||
return 17
|
||||
|
||||
for (int i = 31; i <= 37; i++) {
|
||||
if (size <= 0x1L << i) {
|
||||
return i-13
|
||||
}
|
||||
}
|
||||
for (int i = 31; i <= 37; i++) {
|
||||
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 {
|
||||
digest = MessageDigest.getInstance("SHA-256")
|
||||
} catch (NoSuchAlgorithmException impossible) {
|
||||
digest = null
|
||||
System.exit(1)
|
||||
}
|
||||
}
|
||||
FileHasher() {
|
||||
try {
|
||||
digest = MessageDigest.getInstance("SHA-256")
|
||||
} catch (NoSuchAlgorithmException impossible) {
|
||||
digest = null
|
||||
System.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
InfoHash hashFile(File file) {
|
||||
final long length = file.length()
|
||||
final int size = 0x1 << getPieceSize(length)
|
||||
int numPieces = (int) (length / size)
|
||||
if (numPieces * size < length)
|
||||
numPieces++
|
||||
InfoHash hashFile(File file) {
|
||||
final long length = file.length()
|
||||
final int size = 0x1 << getPieceSize(length)
|
||||
int numPieces = (int) (length / size)
|
||||
if (numPieces * size < length)
|
||||
numPieces++
|
||||
|
||||
def output = new ByteArrayOutputStream()
|
||||
RandomAccessFile raf = new RandomAccessFile(file, "r")
|
||||
try {
|
||||
MappedByteBuffer buf
|
||||
for (int i = 0; i < numPieces - 1; i++) {
|
||||
buf = raf.getChannel().map(MapMode.READ_ONLY, ((long)size) * i, size)
|
||||
digest.update buf
|
||||
output.write(digest.digest(), 0, 32)
|
||||
}
|
||||
def lastPieceLength = length - (numPieces - 1) * ((long)size)
|
||||
buf = raf.getChannel().map(MapMode.READ_ONLY, length - lastPieceLength, lastPieceLength)
|
||||
digest.update buf
|
||||
output.write(digest.digest(), 0, 32)
|
||||
} finally {
|
||||
raf.close()
|
||||
}
|
||||
def output = new ByteArrayOutputStream()
|
||||
RandomAccessFile raf = new RandomAccessFile(file, "r")
|
||||
try {
|
||||
MappedByteBuffer buf
|
||||
for (int i = 0; i < numPieces - 1; i++) {
|
||||
buf = raf.getChannel().map(MapMode.READ_ONLY, ((long)size) * i, size)
|
||||
digest.update buf
|
||||
DataUtil.tryUnmap(buf)
|
||||
output.write(digest.digest(), 0, 32)
|
||||
}
|
||||
def lastPieceLength = length - (numPieces - 1) * ((long)size)
|
||||
buf = raf.getChannel().map(MapMode.READ_ONLY, length - lastPieceLength, lastPieceLength)
|
||||
digest.update buf
|
||||
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) {
|
||||
if (args.length != 1) {
|
||||
|
@ -0,0 +1,15 @@
|
||||
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 {
|
||||
|
||||
SharedFile loadedFile
|
||||
SharedFile loadedFile
|
||||
}
|
||||
|
@ -15,26 +15,26 @@ import groovy.util.logging.Log
|
||||
class FileManager {
|
||||
|
||||
|
||||
final EventBus eventBus
|
||||
final EventBus eventBus
|
||||
final MuWireSettings settings
|
||||
final Map<InfoHash, Set<SharedFile>> rootToFiles = Collections.synchronizedMap(new HashMap<>())
|
||||
final Map<File, SharedFile> fileToSharedFile = Collections.synchronizedMap(new HashMap<>())
|
||||
final Map<String, Set<File>> nameToFiles = new HashMap<>()
|
||||
final SearchIndex index = new SearchIndex()
|
||||
final Map<InfoHash, Set<SharedFile>> rootToFiles = Collections.synchronizedMap(new HashMap<>())
|
||||
final Map<File, SharedFile> fileToSharedFile = Collections.synchronizedMap(new HashMap<>())
|
||||
final Map<String, Set<File>> nameToFiles = new HashMap<>()
|
||||
final SearchIndex index = new SearchIndex()
|
||||
|
||||
FileManager(EventBus eventBus, MuWireSettings settings) {
|
||||
FileManager(EventBus eventBus, MuWireSettings settings) {
|
||||
this.settings = settings
|
||||
this.eventBus = eventBus
|
||||
}
|
||||
this.eventBus = eventBus
|
||||
}
|
||||
|
||||
void onFileHashedEvent(FileHashedEvent e) {
|
||||
if (e.sharedFile != null)
|
||||
addToIndex(e.sharedFile)
|
||||
}
|
||||
|
||||
void onFileLoadedEvent(FileLoadedEvent e) {
|
||||
addToIndex(e.loadedFile)
|
||||
}
|
||||
void onFileLoadedEvent(FileLoadedEvent e) {
|
||||
addToIndex(e.loadedFile)
|
||||
}
|
||||
|
||||
void onFileDownloadedEvent(FileDownloadedEvent e) {
|
||||
if (settings.shareDownloadedFiles) {
|
||||
@ -42,88 +42,88 @@ class FileManager {
|
||||
}
|
||||
}
|
||||
|
||||
private void addToIndex(SharedFile sf) {
|
||||
private void addToIndex(SharedFile sf) {
|
||||
log.info("Adding shared file " + sf.getFile())
|
||||
InfoHash infoHash = sf.getInfoHash()
|
||||
Set<SharedFile> existing = rootToFiles.get(infoHash)
|
||||
if (existing == null) {
|
||||
InfoHash infoHash = sf.getInfoHash()
|
||||
Set<SharedFile> existing = rootToFiles.get(infoHash)
|
||||
if (existing == null) {
|
||||
log.info("adding new root")
|
||||
existing = new HashSet<>()
|
||||
rootToFiles.put(infoHash, existing);
|
||||
}
|
||||
existing.add(sf)
|
||||
fileToSharedFile.put(sf.file, sf)
|
||||
existing = new HashSet<>()
|
||||
rootToFiles.put(infoHash, existing);
|
||||
}
|
||||
existing.add(sf)
|
||||
fileToSharedFile.put(sf.file, sf)
|
||||
|
||||
String name = sf.getFile().getName()
|
||||
Set<File> existingFiles = nameToFiles.get(name)
|
||||
if (existingFiles == null) {
|
||||
existingFiles = new HashSet<>()
|
||||
nameToFiles.put(name, existingFiles)
|
||||
}
|
||||
existingFiles.add(sf.getFile())
|
||||
String name = sf.getFile().getName()
|
||||
Set<File> existingFiles = nameToFiles.get(name)
|
||||
if (existingFiles == null) {
|
||||
existingFiles = new HashSet<>()
|
||||
nameToFiles.put(name, existingFiles)
|
||||
}
|
||||
existingFiles.add(sf.getFile())
|
||||
|
||||
index.add(name)
|
||||
}
|
||||
index.add(name)
|
||||
}
|
||||
|
||||
void onFileUnsharedEvent(FileUnsharedEvent e) {
|
||||
SharedFile sf = e.unsharedFile
|
||||
InfoHash infoHash = sf.getInfoHash()
|
||||
Set<SharedFile> existing = rootToFiles.get(infoHash)
|
||||
if (existing != null) {
|
||||
existing.remove(sf)
|
||||
if (existing.isEmpty()) {
|
||||
rootToFiles.remove(infoHash)
|
||||
}
|
||||
}
|
||||
void onFileUnsharedEvent(FileUnsharedEvent e) {
|
||||
SharedFile sf = e.unsharedFile
|
||||
InfoHash infoHash = sf.getInfoHash()
|
||||
Set<SharedFile> existing = rootToFiles.get(infoHash)
|
||||
if (existing != null) {
|
||||
existing.remove(sf)
|
||||
if (existing.isEmpty()) {
|
||||
rootToFiles.remove(infoHash)
|
||||
}
|
||||
}
|
||||
|
||||
fileToSharedFile.remove(sf.file)
|
||||
fileToSharedFile.remove(sf.file)
|
||||
|
||||
String name = sf.getFile().getName()
|
||||
Set<File> existingFiles = nameToFiles.get(name)
|
||||
if (existingFiles != null) {
|
||||
existingFiles.remove(sf.file)
|
||||
if (existingFiles.isEmpty()) {
|
||||
nameToFiles.remove(name)
|
||||
}
|
||||
}
|
||||
String name = sf.getFile().getName()
|
||||
Set<File> existingFiles = nameToFiles.get(name)
|
||||
if (existingFiles != null) {
|
||||
existingFiles.remove(sf.file)
|
||||
if (existingFiles.isEmpty()) {
|
||||
nameToFiles.remove(name)
|
||||
}
|
||||
}
|
||||
|
||||
index.remove(name)
|
||||
}
|
||||
index.remove(name)
|
||||
}
|
||||
|
||||
Map<File, SharedFile> getSharedFiles() {
|
||||
Map<File, SharedFile> getSharedFiles() {
|
||||
synchronized(fileToSharedFile) {
|
||||
return new HashMap<>(fileToSharedFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Set<SharedFile> getSharedFiles(byte []root) {
|
||||
return rootToFiles.get(new InfoHash(root))
|
||||
}
|
||||
|
||||
void onSearchEvent(SearchEvent e) {
|
||||
// hash takes precedence
|
||||
ResultsEvent re = null
|
||||
if (e.searchHash != null) {
|
||||
void onSearchEvent(SearchEvent e) {
|
||||
// hash takes precedence
|
||||
ResultsEvent re = null
|
||||
if (e.searchHash != null) {
|
||||
Set<SharedFile> found
|
||||
found = rootToFiles.get new InfoHash(e.searchHash)
|
||||
found = filter(found, e.oobInfohash)
|
||||
if (found != null && !found.isEmpty())
|
||||
re = new ResultsEvent(results: found.asList(), uuid: e.uuid, searchEvent: e)
|
||||
} else {
|
||||
def names = index.search e.searchTerms
|
||||
Set<File> files = new HashSet<>()
|
||||
names.each { files.addAll nameToFiles.getOrDefault(it, []) }
|
||||
Set<SharedFile> sharedFiles = new HashSet<>()
|
||||
files.each { sharedFiles.add fileToSharedFile[it] }
|
||||
if (found != null && !found.isEmpty())
|
||||
re = new ResultsEvent(results: found.asList(), uuid: e.uuid, searchEvent: e)
|
||||
} else {
|
||||
def names = index.search e.searchTerms
|
||||
Set<File> files = new HashSet<>()
|
||||
names.each { files.addAll nameToFiles.getOrDefault(it, []) }
|
||||
Set<SharedFile> sharedFiles = new HashSet<>()
|
||||
files.each { sharedFiles.add fileToSharedFile[it] }
|
||||
files = filter(sharedFiles, e.oobInfohash)
|
||||
if (!sharedFiles.isEmpty())
|
||||
re = new ResultsEvent(results: sharedFiles.asList(), uuid: e.uuid, searchEvent: e)
|
||||
if (!sharedFiles.isEmpty())
|
||||
re = new ResultsEvent(results: sharedFiles.asList(), uuid: e.uuid, searchEvent: e)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (re != null)
|
||||
eventBus.publish(re)
|
||||
}
|
||||
if (re != null)
|
||||
eventBus.publish(re)
|
||||
}
|
||||
|
||||
private static Set<SharedFile> filter(Set<SharedFile> files, boolean oob) {
|
||||
if (!oob)
|
||||
@ -135,4 +135,16 @@ class FileManager {
|
||||
}
|
||||
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,5 +4,10 @@ import com.muwire.core.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
|
||||
|
||||
class FileUnsharedEvent extends Event {
|
||||
SharedFile unsharedFile
|
||||
SharedFile unsharedFile
|
||||
}
|
||||
|
@ -8,40 +8,41 @@ import com.muwire.core.SharedFile
|
||||
|
||||
class HasherService {
|
||||
|
||||
final FileHasher hasher
|
||||
final EventBus eventBus
|
||||
final FileHasher hasher
|
||||
final EventBus eventBus
|
||||
final FileManager fileManager
|
||||
Executor executor
|
||||
Executor executor
|
||||
|
||||
HasherService(FileHasher hasher, EventBus eventBus, FileManager fileManager) {
|
||||
this.hasher = hasher
|
||||
this.eventBus = eventBus
|
||||
HasherService(FileHasher hasher, EventBus eventBus, FileManager fileManager) {
|
||||
this.hasher = hasher
|
||||
this.eventBus = eventBus
|
||||
this.fileManager = fileManager
|
||||
}
|
||||
}
|
||||
|
||||
void start() {
|
||||
executor = Executors.newSingleThreadExecutor()
|
||||
}
|
||||
void start() {
|
||||
executor = Executors.newSingleThreadExecutor()
|
||||
}
|
||||
|
||||
void onFileSharedEvent(FileSharedEvent evt) {
|
||||
if (fileManager.fileToSharedFile.containsKey(evt.file))
|
||||
void onFileSharedEvent(FileSharedEvent evt) {
|
||||
if (fileManager.fileToSharedFile.containsKey(evt.file.getCanonicalFile()))
|
||||
return
|
||||
executor.execute( { -> process(evt.file) } as Runnable)
|
||||
}
|
||||
executor.execute( { -> process(evt.file) } as Runnable)
|
||||
}
|
||||
|
||||
private void process(File f) {
|
||||
f = f.getCanonicalFile()
|
||||
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 {
|
||||
def hash = hasher.hashFile f
|
||||
eventBus.publish new FileHashedEvent(sharedFile: new SharedFile(f, hash, FileHasher.getPieceSize(f.length())))
|
||||
}
|
||||
}
|
||||
}
|
||||
private void process(File f) {
|
||||
f = f.getCanonicalFile()
|
||||
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())))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,135 +23,136 @@ import net.i2p.data.Destination
|
||||
@Log
|
||||
class PersisterService extends Service {
|
||||
|
||||
final File location
|
||||
final EventBus listener
|
||||
final int interval
|
||||
final Timer timer
|
||||
final FileManager fileManager
|
||||
final File location
|
||||
final EventBus listener
|
||||
final int interval
|
||||
final Timer timer
|
||||
final FileManager fileManager
|
||||
|
||||
PersisterService(File location, EventBus listener, int interval, FileManager fileManager) {
|
||||
this.location = location
|
||||
this.listener = listener
|
||||
this.interval = interval
|
||||
this.fileManager = fileManager
|
||||
timer = new Timer("file persister", true)
|
||||
}
|
||||
PersisterService(File location, EventBus listener, int interval, FileManager fileManager) {
|
||||
this.location = location
|
||||
this.listener = listener
|
||||
this.interval = interval
|
||||
this.fileManager = fileManager
|
||||
timer = new Timer("file persister", true)
|
||||
}
|
||||
|
||||
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()
|
||||
try {
|
||||
location.eachLine {
|
||||
if (it.trim().length() > 0) {
|
||||
def parsed = slurper.parseText it
|
||||
def event = fromJson parsed
|
||||
if (event != null) {
|
||||
void load() {
|
||||
Thread.currentThread().setPriority(Thread.MIN_PRIORITY)
|
||||
|
||||
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")
|
||||
listener.publish event
|
||||
}
|
||||
}
|
||||
}
|
||||
listener.publish event
|
||||
loaded++
|
||||
if (loaded % 10 == 0)
|
||||
Thread.sleep(20)
|
||||
}
|
||||
}
|
||||
}
|
||||
listener.publish(new AllFilesLoadedEvent())
|
||||
} catch (IllegalArgumentException|NumberFormatException e) {
|
||||
} catch (IllegalArgumentException|NumberFormatException e) {
|
||||
log.log(Level.WARNING, "couldn't load files",e)
|
||||
}
|
||||
} else {
|
||||
}
|
||||
} else {
|
||||
listener.publish(new AllFilesLoadedEvent())
|
||||
}
|
||||
timer.schedule({persistFiles()} as TimerTask, 0, interval)
|
||||
loaded = true
|
||||
}
|
||||
timer.schedule({persistFiles()} as TimerTask, 0, interval)
|
||||
loaded = true
|
||||
}
|
||||
|
||||
private static FileLoadedEvent fromJson(def json) {
|
||||
if (json.file == null || json.length == null || json.infoHash == null || json.hashList == null)
|
||||
throw new IllegalArgumentException()
|
||||
if (!(json.hashList instanceof List))
|
||||
throw new IllegalArgumentException()
|
||||
private static FileLoadedEvent fromJson(def json) {
|
||||
if (json.file == null || json.length == null || json.infoHash == null || json.hashList == null)
|
||||
throw new IllegalArgumentException()
|
||||
if (!(json.hashList instanceof List))
|
||||
throw new IllegalArgumentException()
|
||||
|
||||
def file = new File(DataUtil.readi18nString(Base64.decode(json.file)))
|
||||
file = file.getCanonicalFile()
|
||||
if (!file.exists() || file.isDirectory())
|
||||
return null
|
||||
long length = Long.valueOf(json.length)
|
||||
if (length != file.length())
|
||||
return null
|
||||
def file = new File(DataUtil.readi18nString(Base64.decode(json.file)))
|
||||
file = file.getCanonicalFile()
|
||||
if (!file.exists() || file.isDirectory())
|
||||
return null
|
||||
long length = Long.valueOf(json.length)
|
||||
if (length != file.length())
|
||||
return null
|
||||
|
||||
List hashList = (List) json.hashList
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
||||
hashList.each {
|
||||
byte [] hash = Base64.decode it.toString()
|
||||
if (hash == null)
|
||||
throw new IllegalArgumentException()
|
||||
baos.write hash
|
||||
}
|
||||
byte[] hashListBytes = baos.toByteArray()
|
||||
List hashList = (List) json.hashList
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
||||
hashList.each {
|
||||
byte [] hash = Base64.decode it.toString()
|
||||
if (hash == null)
|
||||
throw new IllegalArgumentException()
|
||||
baos.write hash
|
||||
}
|
||||
byte[] hashListBytes = baos.toByteArray()
|
||||
|
||||
InfoHash ih = InfoHash.fromHashList(hashListBytes)
|
||||
byte [] root = Base64.decode(json.infoHash.toString())
|
||||
if (root == null)
|
||||
throw new IllegalArgumentException()
|
||||
if (!Arrays.equals(root, ih.getRoot()))
|
||||
return null
|
||||
InfoHash ih = InfoHash.fromHashList(hashListBytes)
|
||||
byte [] root = Base64.decode(json.infoHash.toString())
|
||||
if (root == null)
|
||||
throw new IllegalArgumentException()
|
||||
if (!Arrays.equals(root, ih.getRoot()))
|
||||
return null
|
||||
|
||||
int pieceSize = 0
|
||||
if (json.pieceSize != null)
|
||||
pieceSize = json.pieceSize
|
||||
|
||||
if (json.sources != null) {
|
||||
List sources = (List)json.sources
|
||||
Set<Destination> sourceSet = sources.stream().map({d -> new Destination(d.toString())}).collect Collectors.toSet()
|
||||
DownloadedFile df = new DownloadedFile(file, ih, pieceSize, sourceSet)
|
||||
return new FileLoadedEvent(loadedFile : df)
|
||||
}
|
||||
List sources = (List)json.sources
|
||||
Set<Destination> sourceSet = sources.stream().map({d -> new Destination(d.toString())}).collect Collectors.toSet()
|
||||
DownloadedFile df = new DownloadedFile(file, ih, pieceSize, sourceSet)
|
||||
return new FileLoadedEvent(loadedFile : df)
|
||||
}
|
||||
|
||||
|
||||
SharedFile sf = new SharedFile(file, ih, pieceSize)
|
||||
return new FileLoadedEvent(loadedFile: sf)
|
||||
SharedFile sf = new SharedFile(file, ih, pieceSize)
|
||||
return new FileLoadedEvent(loadedFile: sf)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void persistFiles() {
|
||||
def sharedFiles = fileManager.getSharedFiles()
|
||||
private void persistFiles() {
|
||||
def sharedFiles = fileManager.getSharedFiles()
|
||||
|
||||
File tmp = File.createTempFile("muwire-files", "tmp")
|
||||
tmp.deleteOnExit()
|
||||
tmp.withPrintWriter { writer ->
|
||||
sharedFiles.each { k, v ->
|
||||
def json = toJson(k,v)
|
||||
json = JsonOutput.toJson(json)
|
||||
writer.println json
|
||||
}
|
||||
}
|
||||
tmp.withPrintWriter { writer ->
|
||||
sharedFiles.each { k, v ->
|
||||
def json = toJson(k,v)
|
||||
json = JsonOutput.toJson(json)
|
||||
writer.println json
|
||||
}
|
||||
}
|
||||
Files.copy(tmp.toPath(), location.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
tmp.delete()
|
||||
}
|
||||
}
|
||||
|
||||
private def toJson(File f, SharedFile sf) {
|
||||
def json = [:]
|
||||
json.file = Base64.encode DataUtil.encodei18nString(f.getCanonicalFile().toString())
|
||||
json.length = f.length()
|
||||
InfoHash ih = sf.getInfoHash()
|
||||
json.infoHash = Base64.encode ih.getRoot()
|
||||
private def toJson(File f, SharedFile sf) {
|
||||
def json = [:]
|
||||
json.file = sf.getB64EncodedFileName()
|
||||
json.length = sf.getCachedLength()
|
||||
InfoHash ih = sf.getInfoHash()
|
||||
json.infoHash = sf.getB64EncodedHashRoot()
|
||||
json.pieceSize = sf.getPieceSize()
|
||||
byte [] tmp = new byte [32]
|
||||
json.hashList = []
|
||||
for (int i = 0;i < ih.getHashList().length / 32; i++) {
|
||||
System.arraycopy(ih.getHashList(), i * 32, tmp, 0, 32)
|
||||
json.hashList.add Base64.encode(tmp)
|
||||
}
|
||||
json.hashList = sf.getB64EncodedHashList()
|
||||
|
||||
if (sf instanceof DownloadedFile) {
|
||||
json.sources = sf.sources.stream().map( {d -> d.toBase64()}).collect(Collectors.toList())
|
||||
}
|
||||
if (sf instanceof DownloadedFile) {
|
||||
json.sources = sf.sources.stream().map( {d -> d.toBase64()}).collect(Collectors.toList())
|
||||
}
|
||||
|
||||
json
|
||||
}
|
||||
json
|
||||
}
|
||||
}
|
||||
|
@ -18,176 +18,176 @@ import net.i2p.data.Destination
|
||||
@Log
|
||||
class CacheClient {
|
||||
|
||||
private static final int CRAWLER_RETURN = 10
|
||||
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
|
||||
final EventBus eventBus
|
||||
final HostCache cache
|
||||
final ConnectionManager manager
|
||||
final I2PSession session
|
||||
final long interval
|
||||
final MuWireSettings settings
|
||||
final Timer timer
|
||||
|
||||
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)
|
||||
}
|
||||
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 start() {
|
||||
session.addMuxedSessionListener(new Listener(), I2PSession.PROTO_DATAGRAM, 0)
|
||||
timer.schedule({queryIfNeeded()} as TimerTask, 1, interval)
|
||||
}
|
||||
|
||||
void stop() {
|
||||
timer.cancel()
|
||||
}
|
||||
void stop() {
|
||||
timer.cancel()
|
||||
}
|
||||
|
||||
private void queryIfNeeded() {
|
||||
if (!manager.getConnections().isEmpty())
|
||||
return
|
||||
if (!cache.getHosts(1).isEmpty())
|
||||
return
|
||||
private void queryIfNeeded() {
|
||||
if (!manager.getConnections().isEmpty())
|
||||
return
|
||||
if (!cache.getHosts(1).isEmpty())
|
||||
return
|
||||
|
||||
log.info "Will query hostcaches"
|
||||
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)
|
||||
}
|
||||
}
|
||||
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 {
|
||||
class Listener implements I2PSessionMuxedListener {
|
||||
|
||||
private final JsonSlurper slurper = new JsonSlurper()
|
||||
private final JsonSlurper slurper = new JsonSlurper()
|
||||
|
||||
@Override
|
||||
public void messageAvailable(I2PSession session, int msgId, long size) {
|
||||
}
|
||||
@Override
|
||||
public void messageAvailable(I2PSession session, int msgId, long size) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void messageAvailable(I2PSession session, int msgId, long size, int proto, int fromport, int toport) {
|
||||
@Override
|
||||
public void messageAvailable(I2PSession session, int msgId, long size, int proto, int fromport, int toport) {
|
||||
|
||||
if (proto != I2PSession.PROTO_DATAGRAM) {
|
||||
log.warning "Received unexpected protocol $proto"
|
||||
return
|
||||
}
|
||||
if (proto != I2PSession.PROTO_DATAGRAM) {
|
||||
log.warning "Received unexpected protocol $proto"
|
||||
return
|
||||
}
|
||||
|
||||
def payload = session.receiveMessage(msgId)
|
||||
def dissector = new I2PDatagramDissector()
|
||||
try {
|
||||
dissector.loadI2PDatagram(payload)
|
||||
def sender = dissector.getSender()
|
||||
log.info("Received something from ${sender.toBase32()}")
|
||||
def payload = session.receiveMessage(msgId)
|
||||
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)
|
||||
payload = dissector.getPayload()
|
||||
payload = slurper.parse(payload)
|
||||
|
||||
if (payload.type == null) {
|
||||
log.warning("type missing")
|
||||
return
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportAbuse(I2PSession session, int severity) {
|
||||
}
|
||||
@Override
|
||||
public void reportAbuse(I2PSession session, int severity) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnected(I2PSession session) {
|
||||
log.severe "I2P session disconnected"
|
||||
}
|
||||
@Override
|
||||
public void disconnected(I2PSession session) {
|
||||
log.severe "I2P session disconnected"
|
||||
}
|
||||
|
||||
@Override
|
||||
public void errorOccurred(I2PSession session, String message, Throwable error) {
|
||||
log.severe "I2P error occured $message $error"
|
||||
}
|
||||
@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
|
||||
}
|
||||
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
|
||||
}
|
||||
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))
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
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"
|
||||
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 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 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)
|
||||
}
|
||||
def maker = new I2PDatagramMaker(session)
|
||||
pong = maker.makeI2PDatagram(pong.bytes)
|
||||
session.sendMessage(from, pong, I2PSession.PROTO_DATAGRAM, 0, 0)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -4,20 +4,25 @@ import net.i2p.data.Destination
|
||||
|
||||
class CacheServers {
|
||||
|
||||
private static final int TO_GIVE = 3
|
||||
private static Set<Destination> CACHES = [
|
||||
new Destination("Wddh2E6FyyXBF7SvUYHKdN-vjf3~N6uqQWNeBDTM0P33YjiQCOsyedrjmDZmWFrXUJfJLWnCb5bnKezfk4uDaMyj~uvDG~yvLVcFgcPWSUd7BfGgym-zqcG1q1DcM8vfun-US7YamBlmtC6MZ2j-~Igqzmgshita8aLPCfNAA6S6e2UMjjtG7QIXlxpMec75dkHdJlVWbzrk9z8Qgru3YIk0UztYgEwDNBbm9wInsbHhr3HtAfa02QcgRVqRN2PnQXuqUJs7R7~09FZPEviiIcUpkY3FeyLlX1sgQFBeGeA96blaPvZNGd6KnNdgfLgMebx5SSxC-N4KZMSMBz5cgonQF3~m2HHFRSI85zqZNG5X9bJN85t80ltiv1W1es8ZnQW4es11r7MrvJNXz5bmSH641yJIvS6qI8OJJNpFVBIQSXLD-96TayrLQPaYw~uNZ-eXaE6G5dYhiuN8xHsFI1QkdaUaVZnvDGfsRbpS5GtpUbBDbyLkdPurG0i7dN1wAAAA")
|
||||
]
|
||||
private static final int TO_GIVE = 3
|
||||
private static Set<Destination> CACHES = [
|
||||
// zlatinb
|
||||
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() {
|
||||
List<Destination> allCaches = new ArrayList<>(CACHES)
|
||||
Collections.shuffle(allCaches)
|
||||
if (allCaches.size() <= TO_GIVE)
|
||||
return allCaches
|
||||
allCaches[0..TO_GIVE-1]
|
||||
}
|
||||
static List<Destination> getCacheServers() {
|
||||
List<Destination> allCaches = new ArrayList<>(CACHES)
|
||||
Collections.shuffle(allCaches)
|
||||
if (allCaches.size() <= TO_GIVE)
|
||||
return allCaches
|
||||
allCaches[0..TO_GIVE-1]
|
||||
}
|
||||
|
||||
static boolean isRegistered(Destination d) {
|
||||
return CACHES.contains(d)
|
||||
}
|
||||
static boolean isRegistered(Destination d) {
|
||||
return CACHES.contains(d)
|
||||
}
|
||||
}
|
||||
|
@ -4,42 +4,67 @@ import net.i2p.data.Destination
|
||||
|
||||
class Host {
|
||||
|
||||
private static final int MAX_FAILURES = 3
|
||||
private static final int CLEAR_INTERVAL = 60 * 60 * 1000
|
||||
private static final int MAX_FAILURES = 3
|
||||
|
||||
final Destination destination
|
||||
int failures,successes
|
||||
final Destination destination
|
||||
private final int clearInterval, hopelessInterval, rejectionInterval
|
||||
int failures,successes
|
||||
long lastAttempt
|
||||
long lastSuccessfulAttempt
|
||||
long lastRejection
|
||||
|
||||
public Host(Destination destination) {
|
||||
this.destination = destination
|
||||
}
|
||||
public Host(Destination destination, int clearInterval, int hopelessInterval, int rejectionInterval) {
|
||||
this.destination = destination
|
||||
this.clearInterval = clearInterval
|
||||
this.hopelessInterval = hopelessInterval
|
||||
this.rejectionInterval = rejectionInterval
|
||||
}
|
||||
|
||||
synchronized void onConnect() {
|
||||
failures = 0
|
||||
successes++
|
||||
private void connectSuccessful() {
|
||||
failures = 0
|
||||
successes++
|
||||
lastAttempt = System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
|
||||
synchronized void onFailure() {
|
||||
failures++
|
||||
successes = 0
|
||||
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 isFailed() {
|
||||
failures >= MAX_FAILURES
|
||||
}
|
||||
|
||||
synchronized boolean hasSucceeded() {
|
||||
successes > 0
|
||||
}
|
||||
synchronized boolean hasSucceeded() {
|
||||
successes > 0
|
||||
}
|
||||
|
||||
synchronized void clearFailures() {
|
||||
failures = 0
|
||||
}
|
||||
|
||||
synchronized void canTryAgain() {
|
||||
System.currentTimeMillis() - lastAttempt > CLEAR_INTERVAL
|
||||
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,141 +15,151 @@ import net.i2p.data.Destination
|
||||
|
||||
class HostCache extends Service {
|
||||
|
||||
final TrustService trustService
|
||||
final File storage
|
||||
final int interval
|
||||
final Timer timer
|
||||
final MuWireSettings settings
|
||||
final Destination myself
|
||||
final Map<Destination, Host> hosts = new ConcurrentHashMap<>()
|
||||
final TrustService trustService
|
||||
final File storage
|
||||
final int interval
|
||||
final Timer timer
|
||||
final MuWireSettings settings
|
||||
final Destination myself
|
||||
final Map<Destination, Host> hosts = new ConcurrentHashMap<>()
|
||||
|
||||
HostCache(){}
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
void start() {
|
||||
timer.schedule({load()} as TimerTask, 1)
|
||||
}
|
||||
void start() {
|
||||
timer.schedule({load()} as TimerTask, 1)
|
||||
}
|
||||
|
||||
void stop() {
|
||||
timer.cancel()
|
||||
}
|
||||
void stop() {
|
||||
timer.cancel()
|
||||
}
|
||||
|
||||
void onHostDiscoveredEvent(HostDiscoveredEvent e) {
|
||||
if (myself == e.destination)
|
||||
return
|
||||
if (hosts.containsKey(e.destination)) {
|
||||
void onHostDiscoveredEvent(HostDiscoveredEvent e) {
|
||||
if (myself == e.destination)
|
||||
return
|
||||
if (hosts.containsKey(e.destination)) {
|
||||
if (!e.fromHostcache)
|
||||
return
|
||||
hosts.get(e.destination).clearFailures()
|
||||
return
|
||||
}
|
||||
Host host = new Host(e.destination)
|
||||
if (allowHost(host)) {
|
||||
hosts.put(e.destination, host)
|
||||
}
|
||||
}
|
||||
}
|
||||
Host host = new Host(e.destination, settings.hostClearInterval, settings.hostHopelessInterval, settings.hostRejectInterval)
|
||||
if (allowHost(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) {
|
||||
if (e.leaf)
|
||||
return
|
||||
Destination dest = e.endpoint.destination
|
||||
Host host = hosts.get(dest)
|
||||
if (host == null) {
|
||||
host = new Host(dest, settings.hostClearInterval, settings.hostHopelessInterval, settings.hostRejectInterval)
|
||||
hosts.put(dest, host)
|
||||
}
|
||||
|
||||
switch(e.status) {
|
||||
case ConnectionAttemptStatus.SUCCESSFUL:
|
||||
case ConnectionAttemptStatus.REJECTED:
|
||||
host.onConnect()
|
||||
break
|
||||
case ConnectionAttemptStatus.FAILED:
|
||||
host.onFailure()
|
||||
break
|
||||
}
|
||||
}
|
||||
switch(e.status) {
|
||||
case ConnectionAttemptStatus.SUCCESSFUL:
|
||||
host.onConnect()
|
||||
break
|
||||
case ConnectionAttemptStatus.REJECTED:
|
||||
host.onReject()
|
||||
break
|
||||
case ConnectionAttemptStatus.FAILED:
|
||||
host.onFailure()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
List<Destination> getHosts(int n) {
|
||||
List<Destination> rv = new ArrayList<>(hosts.keySet())
|
||||
rv.retainAll {allowHost(hosts[it])}
|
||||
if (rv.size() <= n)
|
||||
return rv
|
||||
Collections.shuffle(rv)
|
||||
rv[0..n-1]
|
||||
}
|
||||
List<Destination> getHosts(int n) {
|
||||
List<Destination> rv = new ArrayList<>(hosts.keySet())
|
||||
rv.retainAll {allowHost(hosts[it])}
|
||||
rv.removeAll {
|
||||
def h = hosts[it];
|
||||
(h.isFailed() && !h.canTryAgain()) || h.isRecentlyRejected()
|
||||
}
|
||||
if (rv.size() <= n)
|
||||
return rv
|
||||
Collections.shuffle(rv)
|
||||
rv[0..n-1]
|
||||
}
|
||||
|
||||
List<Destination> getGoodHosts(int n) {
|
||||
List<Destination> rv = new ArrayList<>(hosts.keySet())
|
||||
rv.retainAll {
|
||||
Host host = hosts[it]
|
||||
allowHost(host) && host.hasSucceeded()
|
||||
}
|
||||
if (rv.size() <= n)
|
||||
return rv
|
||||
Collections.shuffle(rv)
|
||||
rv[0..n-1]
|
||||
}
|
||||
List<Destination> getGoodHosts(int n) {
|
||||
List<Destination> rv = new ArrayList<>(hosts.keySet())
|
||||
rv.retainAll {
|
||||
Host host = hosts[it]
|
||||
allowHost(host) && host.hasSucceeded()
|
||||
}
|
||||
if (rv.size() <= n)
|
||||
return rv
|
||||
Collections.shuffle(rv)
|
||||
rv[0..n-1]
|
||||
}
|
||||
|
||||
void load() {
|
||||
if (storage.exists()) {
|
||||
JsonSlurper slurper = new JsonSlurper()
|
||||
storage.eachLine {
|
||||
def entry = slurper.parseText(it)
|
||||
Destination dest = new Destination(entry.destination)
|
||||
Host host = new Host(dest)
|
||||
host.failures = Integer.valueOf(String.valueOf(entry.failures))
|
||||
host.successes = Integer.valueOf(String.valueOf(entry.successes))
|
||||
void load() {
|
||||
if (storage.exists()) {
|
||||
JsonSlurper slurper = new JsonSlurper()
|
||||
storage.eachLine {
|
||||
def entry = slurper.parseText(it)
|
||||
Destination dest = new Destination(entry.destination)
|
||||
Host host = new Host(dest, settings.hostClearInterval, settings.hostHopelessInterval, settings.hostRejectInterval)
|
||||
host.failures = Integer.valueOf(String.valueOf(entry.failures))
|
||||
host.successes = Integer.valueOf(String.valueOf(entry.successes))
|
||||
if (entry.lastAttempt != null)
|
||||
host.lastAttempt = entry.lastAttempt
|
||||
if (allowHost(host))
|
||||
hosts.put(dest, host)
|
||||
}
|
||||
}
|
||||
timer.schedule({save()} as TimerTask, interval, interval)
|
||||
loaded = true
|
||||
}
|
||||
if (entry.lastSuccessfulAttempt != null)
|
||||
host.lastSuccessfulAttempt = entry.lastSuccessfulAttempt
|
||||
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.isFailed() && !host.canTryAgain())
|
||||
return false
|
||||
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 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)) {
|
||||
def map = [:]
|
||||
map.destination = dest.toBase64()
|
||||
map.failures = host.failures
|
||||
map.successes = host.successes
|
||||
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
|
||||
def json = JsonOutput.toJson(map)
|
||||
writer.println json
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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 {
|
||||
|
||||
Destination destination
|
||||
Destination destination
|
||||
boolean fromHostcache
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
"HostDiscoveredEvent ${super.toString()} destination:${destination.toBase32()} from hostcache $fromHostcache"
|
||||
}
|
||||
@Override
|
||||
public String toString() {
|
||||
"HostDiscoveredEvent ${super.toString()} destination:${destination.toBase32()} from hostcache $fromHostcache"
|
||||
}
|
||||
}
|
||||
|
28
core/src/main/groovy/com/muwire/core/mesh/Mesh.groovy
Normal file
28
core/src/main/groovy/com/muwire/core/mesh/Mesh.groovy
Normal file
@ -0,0 +1,28 @@
|
||||
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]
|
||||
}
|
||||
}
|
103
core/src/main/groovy/com/muwire/core/mesh/MeshManager.groovy
Normal file
103
core/src/main/groovy/com/muwire/core/mesh/MeshManager.groovy
Normal file
@ -0,0 +1,103 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
@ -6,11 +6,11 @@ import net.i2p.data.Base32
|
||||
import net.i2p.data.Destination
|
||||
|
||||
class DeleteEvent extends Event {
|
||||
byte [] infoHash
|
||||
Destination leaf
|
||||
byte [] infoHash
|
||||
Destination leaf
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
"DeleteEvent ${super.toString()} infoHash:${Base32.encode(infoHash)} leaf:${leaf.toBase32()}"
|
||||
}
|
||||
@Override
|
||||
public String toString() {
|
||||
"DeleteEvent ${super.toString()} infoHash:${Base32.encode(infoHash)} leaf:${leaf.toBase32()}"
|
||||
}
|
||||
}
|
||||
|
@ -7,32 +7,32 @@ import net.i2p.data.Destination
|
||||
|
||||
class LeafSearcher {
|
||||
|
||||
final UltrapeerConnectionManager connectionManager
|
||||
final SearchIndex searchIndex = new SearchIndex()
|
||||
final UltrapeerConnectionManager connectionManager
|
||||
final SearchIndex searchIndex = new SearchIndex()
|
||||
|
||||
final Map<String, Set<byte[]>> fileNameToHashes = new HashMap<>()
|
||||
final Map<byte[], Set<Destination>> hashToLeafs = new HashMap<>()
|
||||
final Map<String, Set<byte[]>> fileNameToHashes = 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) {
|
||||
this.connectionManager = connectionManager
|
||||
}
|
||||
LeafSearcher(UltrapeerConnectionManager connectionManager) {
|
||||
this.connectionManager = connectionManager
|
||||
}
|
||||
|
||||
void onUpsertEvent(UpsertEvent e) {
|
||||
// TODO: implement
|
||||
}
|
||||
void onUpsertEvent(UpsertEvent e) {
|
||||
// TODO: implement
|
||||
}
|
||||
|
||||
void onDeleteEvent(DeleteEvent e) {
|
||||
// TODO: implement
|
||||
}
|
||||
void onDeleteEvent(DeleteEvent e) {
|
||||
// TODO: implement
|
||||
}
|
||||
|
||||
void onDisconnectionEvent(DisconnectionEvent e) {
|
||||
// TODO: implement
|
||||
}
|
||||
void onDisconnectionEvent(DisconnectionEvent e) {
|
||||
// TODO: implement
|
||||
}
|
||||
|
||||
void onQueryEvent(QueryEvent e) {
|
||||
// TODO: implement
|
||||
}
|
||||
void onQueryEvent(QueryEvent e) {
|
||||
// TODO: implement
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -8,10 +8,10 @@ import net.i2p.data.Destination
|
||||
class QueryEvent extends Event {
|
||||
|
||||
SearchEvent searchEvent
|
||||
boolean firstHop
|
||||
Destination replyTo
|
||||
boolean firstHop
|
||||
Destination replyTo
|
||||
Persona originator
|
||||
Destination receivedOn
|
||||
Destination receivedOn
|
||||
|
||||
String toString() {
|
||||
"searchEvent: $searchEvent firstHop:$firstHop, replyTo:${replyTo.toBase32()}" +
|
||||
|
@ -6,6 +6,6 @@ import com.muwire.core.SharedFile
|
||||
class ResultsEvent extends Event {
|
||||
|
||||
SearchEvent searchEvent
|
||||
SharedFile[] results
|
||||
UUID uuid
|
||||
SharedFile[] results
|
||||
UUID uuid
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import java.util.concurrent.Executor
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ThreadFactory
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.logging.Level
|
||||
import java.util.stream.Collectors
|
||||
|
||||
import com.muwire.core.DownloadedFile
|
||||
@ -83,50 +84,54 @@ class ResultsSender {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
byte [] tmp = new byte[InfoHash.SIZE]
|
||||
JsonOutput jsonOutput = new JsonOutput()
|
||||
Endpoint endpoint = null;
|
||||
try {
|
||||
endpoint = connector.connect(target)
|
||||
DataOutputStream os = new DataOutputStream(endpoint.getOutputStream())
|
||||
os.write("POST $uuid\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
me.write(os)
|
||||
os.writeShort((short)results.length)
|
||||
results.each {
|
||||
byte [] name = it.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 = 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)
|
||||
byte [] tmp = new byte[InfoHash.SIZE]
|
||||
JsonOutput jsonOutput = new JsonOutput()
|
||||
Endpoint endpoint = null;
|
||||
try {
|
||||
endpoint = connector.connect(target)
|
||||
DataOutputStream os = new DataOutputStream(endpoint.getOutputStream())
|
||||
os.write("POST $uuid\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
me.write(os)
|
||||
os.writeShort((short)results.length)
|
||||
results.each {
|
||||
byte [] name = it.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 = 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)
|
||||
}
|
||||
obj.hashList = hashListB64
|
||||
}
|
||||
obj.hashList = hashListB64
|
||||
|
||||
if (it instanceof DownloadedFile)
|
||||
obj.sources = it.sources.stream().map({dest -> dest.toBase64()}).collect(Collectors.toSet())
|
||||
|
||||
def json = jsonOutput.toJson(obj)
|
||||
os.writeShort((short)json.length())
|
||||
os.write(json.getBytes(StandardCharsets.US_ASCII))
|
||||
}
|
||||
|
||||
if (it instanceof DownloadedFile)
|
||||
obj.sources = it.sources.stream().map({dest -> dest.toBase64()}).collect(Collectors.toSet())
|
||||
|
||||
def json = jsonOutput.toJson(obj)
|
||||
os.writeShort((short)json.length())
|
||||
os.write(json.getBytes(StandardCharsets.US_ASCII))
|
||||
os.flush()
|
||||
} finally {
|
||||
endpoint?.close()
|
||||
}
|
||||
os.flush()
|
||||
} finally {
|
||||
endpoint?.close()
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING, "problem sending results",e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,9 +5,9 @@ import com.muwire.core.InfoHash
|
||||
|
||||
class SearchEvent extends Event {
|
||||
|
||||
List<String> searchTerms
|
||||
byte [] searchHash
|
||||
UUID uuid
|
||||
List<String> searchTerms
|
||||
byte [] searchHash
|
||||
UUID uuid
|
||||
boolean oobInfohash
|
||||
|
||||
String toString() {
|
||||
|
@ -1,56 +1,59 @@
|
||||
package com.muwire.core.search
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.SplitPattern
|
||||
|
||||
class SearchIndex {
|
||||
|
||||
final Map<String, Set<String>> keywords = new HashMap<>()
|
||||
final Map<String, Set<String>> keywords = new HashMap<>()
|
||||
|
||||
void add(String string) {
|
||||
String [] split = split(string)
|
||||
split.each {
|
||||
Set<String> existing = keywords.get(it)
|
||||
if (existing == null) {
|
||||
existing = new HashSet<>()
|
||||
keywords.put(it, existing)
|
||||
}
|
||||
existing.add(string)
|
||||
}
|
||||
}
|
||||
void add(String string) {
|
||||
String [] split = split(string)
|
||||
split.each {
|
||||
Set<String> existing = keywords.get(it)
|
||||
if (existing == null) {
|
||||
existing = new HashSet<>()
|
||||
keywords.put(it, existing)
|
||||
}
|
||||
existing.add(string)
|
||||
}
|
||||
}
|
||||
|
||||
void remove(String string) {
|
||||
String [] split = split(string)
|
||||
split.each {
|
||||
Set<String> existing = keywords.get it
|
||||
if (existing != null) {
|
||||
existing.remove(string)
|
||||
if (existing.isEmpty()) {
|
||||
keywords.remove(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
void remove(String string) {
|
||||
String [] split = split(string)
|
||||
split.each {
|
||||
Set<String> existing = keywords.get it
|
||||
if (existing != null) {
|
||||
existing.remove(string)
|
||||
if (existing.isEmpty()) {
|
||||
keywords.remove(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String[] split(String source) {
|
||||
source = source.replaceAll(Constants.SPLIT_PATTERN, " ").toLowerCase()
|
||||
source.split(" ")
|
||||
}
|
||||
private static String[] split(String source) {
|
||||
source = source.replaceAll(SplitPattern.SPLIT_PATTERN, " ").toLowerCase()
|
||||
String [] 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) {
|
||||
Set<String> rv = null;
|
||||
|
||||
terms.each {
|
||||
Set<String> forWord = keywords.getOrDefault(it,[])
|
||||
if (rv == null) {
|
||||
rv = new HashSet<>(forWord)
|
||||
} else {
|
||||
rv.retainAll(forWord)
|
||||
}
|
||||
terms.each {
|
||||
Set<String> forWord = keywords.getOrDefault(it,[])
|
||||
if (rv == null) {
|
||||
rv = new HashSet<>(forWord)
|
||||
} else {
|
||||
rv.retainAll(forWord)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (rv != null)
|
||||
return rv.asList()
|
||||
[]
|
||||
}
|
||||
if (rv != null)
|
||||
return rv.asList()
|
||||
[]
|
||||
}
|
||||
}
|
||||
|
@ -7,12 +7,12 @@ import net.i2p.data.Destination
|
||||
|
||||
class UpsertEvent extends Event {
|
||||
|
||||
Set<String> names
|
||||
byte [] infoHash
|
||||
Destination leaf
|
||||
Set<String> names
|
||||
byte [] infoHash
|
||||
Destination leaf
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
"UpsertEvent ${super.toString()} names:$names infoHash:${Base32.encode(infoHash)} leaf:${leaf.toBase32()}"
|
||||
}
|
||||
@Override
|
||||
public String toString() {
|
||||
"UpsertEvent ${super.toString()} names:$names infoHash:${Base32.encode(infoHash)} leaf:${leaf.toBase32()}"
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,31 @@
|
||||
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 {
|
||||
|
||||
Persona persona
|
||||
TrustLevel level
|
||||
Persona persona
|
||||
TrustLevel level
|
||||
}
|
||||
|
@ -11,87 +11,87 @@ import net.i2p.util.ConcurrentHashSet
|
||||
|
||||
class TrustService extends Service {
|
||||
|
||||
final File persistGood, persistBad
|
||||
final long persistInterval
|
||||
final File persistGood, persistBad
|
||||
final long persistInterval
|
||||
|
||||
final Map<Destination, Persona> good = new ConcurrentHashMap<>()
|
||||
final Map<Destination, Persona> bad = new ConcurrentHashMap<>()
|
||||
final Map<Destination, Persona> good = new ConcurrentHashMap<>()
|
||||
final Map<Destination, Persona> bad = new ConcurrentHashMap<>()
|
||||
|
||||
final Timer timer
|
||||
final Timer timer
|
||||
|
||||
TrustService() {}
|
||||
TrustService() {}
|
||||
|
||||
TrustService(File persistGood, File persistBad, long persistInterval) {
|
||||
this.persistBad = persistBad
|
||||
this.persistGood = persistGood
|
||||
this.persistInterval = persistInterval
|
||||
this.timer = new Timer("trust-persister",true)
|
||||
}
|
||||
TrustService(File persistGood, File persistBad, long persistInterval) {
|
||||
this.persistBad = persistBad
|
||||
this.persistGood = persistGood
|
||||
this.persistInterval = persistInterval
|
||||
this.timer = new Timer("trust-persister",true)
|
||||
}
|
||||
|
||||
void start() {
|
||||
timer.schedule({load()} as TimerTask, 1)
|
||||
}
|
||||
void start() {
|
||||
timer.schedule({load()} as TimerTask, 1)
|
||||
}
|
||||
|
||||
void stop() {
|
||||
timer.cancel()
|
||||
}
|
||||
void stop() {
|
||||
timer.cancel()
|
||||
}
|
||||
|
||||
void load() {
|
||||
if (persistGood.exists()) {
|
||||
persistGood.eachLine {
|
||||
void load() {
|
||||
if (persistGood.exists()) {
|
||||
persistGood.eachLine {
|
||||
byte [] decoded = Base64.decode(it)
|
||||
Persona persona = new Persona(new ByteArrayInputStream(decoded))
|
||||
good.put(persona.destination, persona)
|
||||
}
|
||||
}
|
||||
if (persistBad.exists()) {
|
||||
persistBad.eachLine {
|
||||
good.put(persona.destination, persona)
|
||||
}
|
||||
}
|
||||
if (persistBad.exists()) {
|
||||
persistBad.eachLine {
|
||||
byte [] decoded = Base64.decode(it)
|
||||
Persona persona = new Persona(new ByteArrayInputStream(decoded))
|
||||
bad.put(persona.destination, persona)
|
||||
}
|
||||
}
|
||||
timer.schedule({persist()} as TimerTask, persistInterval, persistInterval)
|
||||
loaded = true
|
||||
}
|
||||
}
|
||||
}
|
||||
timer.schedule({persist()} as TimerTask, persistInterval, persistInterval)
|
||||
loaded = true
|
||||
}
|
||||
|
||||
private void persist() {
|
||||
persistGood.delete()
|
||||
persistGood.withPrintWriter { writer ->
|
||||
good.each {k,v ->
|
||||
writer.println v.toBase64()
|
||||
}
|
||||
}
|
||||
persistBad.delete()
|
||||
persistBad.withPrintWriter { writer ->
|
||||
bad.each { k,v ->
|
||||
writer.println v.toBase64()
|
||||
}
|
||||
}
|
||||
}
|
||||
private void persist() {
|
||||
persistGood.delete()
|
||||
persistGood.withPrintWriter { writer ->
|
||||
good.each {k,v ->
|
||||
writer.println v.toBase64()
|
||||
}
|
||||
}
|
||||
persistBad.delete()
|
||||
persistBad.withPrintWriter { writer ->
|
||||
bad.each { k,v ->
|
||||
writer.println v.toBase64()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TrustLevel getLevel(Destination dest) {
|
||||
if (good.containsKey(dest))
|
||||
return TrustLevel.TRUSTED
|
||||
else if (bad.containsKey(dest))
|
||||
return TrustLevel.DISTRUSTED
|
||||
TrustLevel.NEUTRAL
|
||||
}
|
||||
TrustLevel getLevel(Destination dest) {
|
||||
if (good.containsKey(dest))
|
||||
return TrustLevel.TRUSTED
|
||||
else if (bad.containsKey(dest))
|
||||
return TrustLevel.DISTRUSTED
|
||||
TrustLevel.NEUTRAL
|
||||
}
|
||||
|
||||
void onTrustEvent(TrustEvent e) {
|
||||
switch(e.level) {
|
||||
case TrustLevel.TRUSTED:
|
||||
bad.remove(e.persona.destination)
|
||||
good.put(e.persona.destination, e.persona)
|
||||
break
|
||||
case TrustLevel.DISTRUSTED:
|
||||
good.remove(e.persona.destination)
|
||||
bad.put(e.persona.destination, e.persona)
|
||||
break
|
||||
case TrustLevel.NEUTRAL:
|
||||
good.remove(e.persona.destination)
|
||||
bad.remove(e.persona.destination)
|
||||
break
|
||||
}
|
||||
}
|
||||
void onTrustEvent(TrustEvent e) {
|
||||
switch(e.level) {
|
||||
case TrustLevel.TRUSTED:
|
||||
bad.remove(e.persona.destination)
|
||||
good.put(e.persona.destination, e.persona)
|
||||
break
|
||||
case TrustLevel.DISTRUSTED:
|
||||
good.remove(e.persona.destination)
|
||||
bad.put(e.persona.destination, e.persona)
|
||||
break
|
||||
case TrustLevel.NEUTRAL:
|
||||
good.remove(e.persona.destination)
|
||||
bad.remove(e.persona.destination)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,161 @@
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package com.muwire.core.trust
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.Persona
|
||||
|
||||
class TrustSubscriptionEvent extends Event {
|
||||
Persona persona
|
||||
boolean subscribe
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package com.muwire.core.trust
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class TrustSubscriptionUpdatedEvent extends Event {
|
||||
RemoteTrustList trustList
|
||||
}
|
@ -3,7 +3,15 @@ package com.muwire.core.update
|
||||
import java.util.logging.Level
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.InfoHash
|
||||
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.JsonSlurper
|
||||
@ -13,6 +21,7 @@ import net.i2p.client.I2PSessionMuxedListener
|
||||
import net.i2p.client.SendMessageOptions
|
||||
import net.i2p.client.datagram.I2PDatagramDissector
|
||||
import net.i2p.client.datagram.I2PDatagramMaker
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.util.VersionComparator
|
||||
|
||||
@Log
|
||||
@ -21,16 +30,24 @@ class UpdateClient {
|
||||
final I2PSession session
|
||||
final String myVersion
|
||||
final MuWireSettings settings
|
||||
final FileManager fileManager
|
||||
final Persona me
|
||||
|
||||
private final Timer timer
|
||||
|
||||
private long lastUpdateCheckTime
|
||||
|
||||
UpdateClient(EventBus eventBus, I2PSession session, String myVersion, MuWireSettings settings) {
|
||||
private volatile InfoHash updateInfoHash
|
||||
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.session = session
|
||||
this.myVersion = myVersion
|
||||
this.settings = settings
|
||||
this.fileManager = fileManager
|
||||
this.me = me
|
||||
timer = new Timer("update-client",true)
|
||||
}
|
||||
|
||||
@ -43,6 +60,24 @@ class UpdateClient {
|
||||
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() {
|
||||
final long now = System.currentTimeMillis()
|
||||
if (lastUpdateCheckTime > 0) {
|
||||
@ -106,8 +141,32 @@ class UpdateClient {
|
||||
return
|
||||
}
|
||||
|
||||
log.info("new version $payload.version available, publishing event")
|
||||
eventBus.publish(new UpdateAvailableEvent(version : payload.version, signer : payload.signer, infoHash : payload.infoHash))
|
||||
String infoHash
|
||||
if (settings.updateType == "jar") {
|
||||
infoHash = payload.infoHash
|
||||
} else
|
||||
infoHash = payload[settings.updateType]
|
||||
|
||||
|
||||
if (!settings.autoDownloadUpdate) {
|
||||
log.info("new version $payload.version available, publishing event")
|
||||
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) {
|
||||
log.log(Level.WARNING,"Invalid datagram",e)
|
||||
|
@ -0,0 +1,8 @@
|
||||
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
|
||||
|
||||
class UpdateServers {
|
||||
static final Destination UPDATE_SERVER = new Destination("pSWieSRB3czCl3Zz4WpKp4Z8tjv-05zbogRDS7SEnKcSdWOupVwjzQ92GsgQh1VqgoSRk1F8dpZOnHxxz5HFy9D7ri0uFdkMyXdSKoB7IgkkvCfTAyEmeaPwSYnurF3Zk7u286E7YG2rZkQZgJ77tow7ZS0mxFB7Z0Ti-VkZ9~GeGePW~howwNm4iSQACZA0DyTpI8iv5j4I0itPCQRgaGziob~Vfvjk49nd8N4jtaDGo9cEcafikVzQ2OgBgYWL6LRbrrItwuGqsDvITUHWaElUYIDhRQYUq8gYiUA6rwAJputfhFU0J7lIxFR9vVY7YzRvcFckfr0DNI4VQVVlPnRPkUxQa--BlldMaCIppWugjgKLwqiSiHywKpSMlBWgY2z1ry4ueEBo1WEP-mEf88wRk4cFQBCKtctCQnIG2GsnATqTl-VGUAsuzeNWZiFSwXiTy~gQ094yWx-K06fFZUDt4CMiLZVhGlixiInD~34FCRC9LVMtFcqiFB2M-Ql2AAAA")
|
||||
static final Destination UPDATE_SERVER = new Destination("VJYAiCPZHNLraWvLkeRLxRiT4PHAqNqRO1nH240r7u1noBw8Pa~-lJOhKR7CccPkEN8ejSi4H6XjqKYLC8BKLVLeOgnAbedUVx81MV7DETPDdPEGV4RVu6YDFri7-tJOeqauGHxtlXT44YWuR69xKrTG3u4~iTWgxKnlBDht9Q3aVpSPFD2KqEizfVxolqXI0zmAZ2xMi8jfl0oe4GbgHrD9hR2FYj6yKfdqcUgHVobY4kDdJt-u31QqwWdsQMEj8Y3tR2XcNaITEVPiAjoKgBrYwB4jddWPNaT4XdHz76d9p9Iqes7dhOKq3OKpk6kg-bfIKiEOiA1mY49fn5h8pNShTqV7QBhh4CE4EDT3Szl~WsLdrlHUKJufSi7erEMh3coF7HORpF1wah2Xw7q470t~b8dKGKi7N7xQsqhGruDm66PH9oE9Kt9WBVBq2zORdPRtRM61I7EnrwDlbOkL0y~XpvQ3JKUQKdBQ3QsOJt8CHlhHHXMMbvqhntR61RSDBQAEAAcAAA==")
|
||||
}
|
||||
|
@ -2,4 +2,5 @@ package com.muwire.core.upload
|
||||
|
||||
class ContentRequest extends Request {
|
||||
Range range
|
||||
int have
|
||||
}
|
||||
|
@ -5,34 +5,58 @@ import java.nio.channels.FileChannel
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.Files
|
||||
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.mesh.Mesh
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import net.i2p.data.Destination
|
||||
|
||||
class ContentUploader extends Uploader {
|
||||
|
||||
private final File file
|
||||
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)
|
||||
this.file = file
|
||||
this.request = request
|
||||
this.mesh = mesh
|
||||
this.pieceSize = pieceSize
|
||||
}
|
||||
|
||||
@Override
|
||||
void respond() {
|
||||
OutputStream os = endpoint.getOutputStream()
|
||||
Range range = request.getRange()
|
||||
if (range.start >= file.length() || range.end >= file.length()) {
|
||||
os.write("416 Range Not Satisfiable\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
boolean satisfiable = true
|
||||
final long length = file.length()
|
||||
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()
|
||||
return
|
||||
}
|
||||
|
||||
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("Content-Range: $range.start-$range.end\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("Content-Range: $range.start-$range.end\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
writeMesh(request.downloader)
|
||||
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
FileChannel channel
|
||||
FileChannel channel = null
|
||||
try {
|
||||
channel = Files.newByteChannel(file.toPath(), EnumSet.of(StandardOpenOption.READ))
|
||||
mapped = channel.map(FileChannel.MapMode.READ_ONLY, range.start, range.end - range.start + 1)
|
||||
@ -48,6 +72,21 @@ class ContentUploader extends Uploader {
|
||||
} finally {
|
||||
try {channel?.close() } catch (IOException ignored) {}
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,4 +109,18 @@ class ContentUploader extends Uploader {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -51,5 +51,18 @@ class HashListUploader extends Uploader {
|
||||
request.downloader.getHumanReadableName()
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDonePieces() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTotalPieces() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTotalSize() {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import java.nio.charset.StandardCharsets
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.data.Base64
|
||||
@ -48,8 +49,14 @@ class Request {
|
||||
def decoded = Base64.decode(encoded)
|
||||
downloader = new Persona(new ByteArrayInputStream(decoded))
|
||||
}
|
||||
|
||||
int have = 0
|
||||
if (headers.containsKey("X-Have")) {
|
||||
def encoded = headers["X-Have"].trim()
|
||||
have = DataUtil.decodeXHave(encoded).size()
|
||||
}
|
||||
new ContentRequest( infoHash : infoHash, range : new Range(start, end),
|
||||
headers : headers, downloader : downloader)
|
||||
headers : headers, downloader : downloader, have : have)
|
||||
}
|
||||
|
||||
static Request parseHashListRequest(InfoHash infoHash, InputStream is) throws IOException {
|
||||
|
@ -6,7 +6,12 @@ import com.muwire.core.EventBus
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.connection.Endpoint
|
||||
import com.muwire.core.download.DownloadManager
|
||||
import com.muwire.core.download.Downloader
|
||||
import com.muwire.core.download.SourceDiscoveredEvent
|
||||
import com.muwire.core.files.FileManager
|
||||
import com.muwire.core.mesh.Mesh
|
||||
import com.muwire.core.mesh.MeshManager
|
||||
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.data.Base64
|
||||
@ -15,12 +20,17 @@ import net.i2p.data.Base64
|
||||
public class UploadManager {
|
||||
private final EventBus eventBus
|
||||
private final FileManager fileManager
|
||||
private final MeshManager meshManager
|
||||
private final DownloadManager downloadManager
|
||||
|
||||
public UploadManager() {}
|
||||
|
||||
public UploadManager(EventBus eventBus, FileManager fileManager) {
|
||||
public UploadManager(EventBus eventBus, FileManager fileManager,
|
||||
MeshManager meshManager, DownloadManager downloadManager) {
|
||||
this.eventBus = eventBus
|
||||
this.fileManager = fileManager
|
||||
this.meshManager = meshManager
|
||||
this.downloadManager = downloadManager
|
||||
}
|
||||
|
||||
public void processGET(Endpoint e) throws IOException {
|
||||
@ -44,8 +54,10 @@ public class UploadManager {
|
||||
log.info("Responding to upload request for root $infoHashString")
|
||||
|
||||
byte [] infoHashRoot = Base64.decode(infoHashString)
|
||||
InfoHash infoHash = new InfoHash(infoHashRoot)
|
||||
Set<SharedFile> sharedFiles = fileManager.getSharedFiles(infoHashRoot)
|
||||
if (sharedFiles == null || sharedFiles.isEmpty()) {
|
||||
Downloader downloader = downloadManager.downloaders.get(infoHash)
|
||||
if (downloader == null && (sharedFiles == null || sharedFiles.isEmpty())) {
|
||||
log.info "file not found"
|
||||
e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
e.getOutputStream().flush()
|
||||
@ -61,13 +73,31 @@ public class UploadManager {
|
||||
return
|
||||
}
|
||||
|
||||
Request request = Request.parseContentRequest(new InfoHash(infoHashRoot), e.getInputStream())
|
||||
ContentRequest request = Request.parseContentRequest(infoHash, e.getInputStream())
|
||||
if (request.downloader != null && request.downloader.destination != e.destination) {
|
||||
log.info("Downloader persona doesn't match their destination")
|
||||
e.close()
|
||||
return
|
||||
}
|
||||
Uploader uploader = new ContentUploader(sharedFiles.iterator().next().file, request, e)
|
||||
|
||||
if (request.have > 0)
|
||||
eventBus.publish(new SourceDiscoveredEvent(infoHash : request.infoHash, source : request.downloader))
|
||||
|
||||
Mesh mesh
|
||||
File file
|
||||
int pieceSize
|
||||
if (downloader != null) {
|
||||
mesh = meshManager.get(infoHash)
|
||||
file = downloader.incompleteFile
|
||||
pieceSize = downloader.pieceSizePow2
|
||||
} else {
|
||||
SharedFile sharedFile = sharedFiles.iterator().next();
|
||||
mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces, false)
|
||||
file = sharedFile.file
|
||||
pieceSize = sharedFile.pieceSize
|
||||
}
|
||||
|
||||
Uploader uploader = new ContentUploader(file, request, e, mesh, pieceSize)
|
||||
eventBus.publish(new UploadEvent(uploader : uploader))
|
||||
try {
|
||||
uploader.respond()
|
||||
@ -85,8 +115,10 @@ public class UploadManager {
|
||||
log.info("Responding to hashlist request for root $infoHashString")
|
||||
|
||||
byte [] infoHashRoot = Base64.decode(infoHashString)
|
||||
InfoHash infoHash = new InfoHash(infoHashRoot)
|
||||
Downloader downloader = downloadManager.downloaders.get(infoHash)
|
||||
Set<SharedFile> sharedFiles = fileManager.getSharedFiles(infoHashRoot)
|
||||
if (sharedFiles == null || sharedFiles.isEmpty()) {
|
||||
if (downloader == null && (sharedFiles == null || sharedFiles.isEmpty())) {
|
||||
log.info "file not found"
|
||||
e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
e.getOutputStream().flush()
|
||||
@ -102,13 +134,30 @@ public class UploadManager {
|
||||
return
|
||||
}
|
||||
|
||||
Request request = Request.parseHashListRequest(new InfoHash(infoHashRoot), e.getInputStream())
|
||||
Request request = Request.parseHashListRequest(infoHash, e.getInputStream())
|
||||
if (request.downloader != null && request.downloader.destination != e.destination) {
|
||||
log.info("Downloader persona doesn't match their destination")
|
||||
e.close()
|
||||
return
|
||||
}
|
||||
Uploader uploader = new HashListUploader(e, sharedFiles.iterator().next().infoHash, request)
|
||||
|
||||
InfoHash fullInfoHash
|
||||
if (downloader == null) {
|
||||
fullInfoHash = sharedFiles.iterator().next().infoHash
|
||||
} else {
|
||||
byte [] hashList = downloader.getInfoHash().getHashList()
|
||||
if (hashList != null && hashList.length > 0)
|
||||
fullInfoHash = downloader.getInfoHash()
|
||||
else {
|
||||
log.info("infohash not found in downloader")
|
||||
e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
e.getOutputStream().flush()
|
||||
e.close()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
Uploader uploader = new HashListUploader(e, fullInfoHash, request)
|
||||
eventBus.publish(new UploadEvent(uploader : uploader))
|
||||
try {
|
||||
uploader.respond()
|
||||
@ -130,8 +179,10 @@ public class UploadManager {
|
||||
log.info("Responding to upload request for root $infoHashString")
|
||||
|
||||
infoHashRoot = Base64.decode(infoHashString)
|
||||
infoHash = new InfoHash(infoHashRoot)
|
||||
sharedFiles = fileManager.getSharedFiles(infoHashRoot)
|
||||
if (sharedFiles == null || sharedFiles.isEmpty()) {
|
||||
downloader = downloadManager.downloaders.get(infoHash)
|
||||
if (downloader == null && (sharedFiles == null || sharedFiles.isEmpty())) {
|
||||
log.info "file not found"
|
||||
e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
e.getOutputStream().flush()
|
||||
@ -153,7 +204,25 @@ public class UploadManager {
|
||||
e.close()
|
||||
return
|
||||
}
|
||||
uploader = new ContentUploader(sharedFiles.iterator().next().file, request, e)
|
||||
|
||||
if (request.have > 0)
|
||||
eventBus.publish(new SourceDiscoveredEvent(infoHash : request.infoHash, source : request.downloader))
|
||||
|
||||
Mesh mesh
|
||||
File file
|
||||
int pieceSize
|
||||
if (downloader != null) {
|
||||
mesh = meshManager.get(infoHash)
|
||||
file = downloader.incompleteFile
|
||||
pieceSize = downloader.pieceSizePow2
|
||||
} else {
|
||||
SharedFile sharedFile = sharedFiles.iterator().next();
|
||||
mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces, false)
|
||||
file = sharedFile.file
|
||||
pieceSize = sharedFile.pieceSize
|
||||
}
|
||||
|
||||
uploader = new ContentUploader(file, request, e, mesh, pieceSize)
|
||||
eventBus.publish(new UploadEvent(uploader : uploader))
|
||||
try {
|
||||
uploader.respond()
|
||||
|
@ -32,4 +32,10 @@ abstract class Uploader {
|
||||
abstract int getProgress();
|
||||
|
||||
abstract String getDownloader();
|
||||
|
||||
abstract int getDonePieces();
|
||||
|
||||
abstract int getTotalPieces();
|
||||
|
||||
abstract long getTotalSize();
|
||||
}
|
||||
|
@ -1,82 +0,0 @@
|
||||
package com.muwire.core.util
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
import com.muwire.core.Constants
|
||||
|
||||
class DataUtil {
|
||||
|
||||
private final static int MAX_SHORT = (0x1 << 16) - 1
|
||||
|
||||
static void writeUnsignedShort(int value, OutputStream os) {
|
||||
if (value > MAX_SHORT || value < 0)
|
||||
throw new IllegalArgumentException("$value invalid")
|
||||
|
||||
byte lsb = (byte) (value & 0xFF)
|
||||
byte msb = (byte) (value >> 8)
|
||||
|
||||
os.write(msb)
|
||||
os.write(lsb)
|
||||
}
|
||||
|
||||
private final static int MAX_HEADER = 0x7FFFFF
|
||||
|
||||
static void packHeader(int length, byte [] header) {
|
||||
if (header.length != 3)
|
||||
throw new IllegalArgumentException("header length $header.length")
|
||||
if (length < 0 || length > MAX_HEADER)
|
||||
throw new IllegalArgumentException("length $length")
|
||||
|
||||
header[2] = (byte) (length & 0xFF)
|
||||
header[1] = (byte) ((length >> 8) & 0xFF)
|
||||
header[0] = (byte) ((length >> 16) & 0x7F)
|
||||
}
|
||||
|
||||
static int readLength(byte [] header) {
|
||||
if (header.length != 3)
|
||||
throw new IllegalArgumentException("header length $header.length")
|
||||
|
||||
return (((int)(header[0] & 0x7F)) << 16) |
|
||||
(((int)(header[1] & 0xFF) << 8)) |
|
||||
((int)header[2] & 0xFF)
|
||||
}
|
||||
|
||||
static String readi18nString(byte [] encoded) {
|
||||
if (encoded.length < 2)
|
||||
throw new IllegalArgumentException("encoding too short $encoded.length")
|
||||
int length = ((encoded[0] & 0xFF) << 8) | (encoded[1] & 0xFF)
|
||||
if (encoded.length != length + 2)
|
||||
throw new IllegalArgumentException("encoding doesn't match length, expected $length found $encoded.length")
|
||||
byte [] string = new byte[length]
|
||||
System.arraycopy(encoded, 2, string, 0, length)
|
||||
new String(string, StandardCharsets.UTF_8)
|
||||
}
|
||||
|
||||
static byte[] encodei18nString(String string) {
|
||||
byte [] utf8 = string.getBytes(StandardCharsets.UTF_8)
|
||||
if (utf8.length > Short.MAX_VALUE)
|
||||
throw new IllegalArgumentException("String in utf8 too long $utf8.length")
|
||||
def baos = new ByteArrayOutputStream()
|
||||
def daos = new DataOutputStream(baos)
|
||||
daos.writeShort((short) utf8.length)
|
||||
daos.write(utf8)
|
||||
daos.close()
|
||||
baos.toByteArray()
|
||||
}
|
||||
|
||||
public static String readTillRN(InputStream is) {
|
||||
def baos = new ByteArrayOutputStream()
|
||||
while(baos.size() < (Constants.MAX_HEADER_SIZE)) {
|
||||
byte read = is.read()
|
||||
if (read == -1)
|
||||
throw new IOException()
|
||||
if (read == '\r') {
|
||||
if (is.read() != '\n')
|
||||
throw new IOException("invalid header")
|
||||
break
|
||||
}
|
||||
baos.write(read)
|
||||
}
|
||||
new String(baos.toByteArray(), StandardCharsets.US_ASCII)
|
||||
}
|
||||
}
|
11
core/src/main/java/com/muwire/core/Constants.java
Normal file
11
core/src/main/java/com/muwire/core/Constants.java
Normal file
@ -0,0 +1,11 @@
|
||||
package com.muwire.core;
|
||||
|
||||
import net.i2p.crypto.SigType;
|
||||
|
||||
public class Constants {
|
||||
public static final byte PERSONA_VERSION = (byte)1;
|
||||
public static final SigType SIG_TYPE = SigType.EdDSA_SHA512_Ed25519;
|
||||
|
||||
public static final int MAX_HEADER_SIZE = 0x1 << 14;
|
||||
public static final int MAX_HEADERS = 16;
|
||||
}
|
@ -1,21 +1,23 @@
|
||||
package com.muwire.core;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Set;
|
||||
|
||||
import net.i2p.data.Destination;
|
||||
|
||||
public class DownloadedFile extends SharedFile {
|
||||
|
||||
private final Set<Destination> sources;
|
||||
private final Set<Destination> sources;
|
||||
|
||||
public DownloadedFile(File file, InfoHash infoHash, int pieceSize, Set<Destination> sources) {
|
||||
super(file, infoHash, pieceSize);
|
||||
this.sources = sources;
|
||||
}
|
||||
public DownloadedFile(File file, InfoHash infoHash, int pieceSize, Set<Destination> sources)
|
||||
throws IOException {
|
||||
super(file, infoHash, pieceSize);
|
||||
this.sources = sources;
|
||||
}
|
||||
|
||||
public Set<Destination> getSources() {
|
||||
return sources;
|
||||
}
|
||||
public Set<Destination> getSources() {
|
||||
return sources;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -11,83 +11,83 @@ import net.i2p.data.Base64;
|
||||
|
||||
public class InfoHash {
|
||||
|
||||
public static final int SIZE = 0x1 << 5;
|
||||
public static final int SIZE = 0x1 << 5;
|
||||
|
||||
private final byte[] root;
|
||||
private final byte[] hashList;
|
||||
private final byte[] root;
|
||||
private final byte[] hashList;
|
||||
|
||||
private final int hashCode;
|
||||
private final int hashCode;
|
||||
|
||||
public InfoHash(byte[] root, byte[] hashList) {
|
||||
if (root.length != SIZE)
|
||||
throw new IllegalArgumentException("invalid root size "+root.length);
|
||||
if (hashList != null && hashList.length % SIZE != 0)
|
||||
throw new IllegalArgumentException("invalid hashList size " + hashList.length);
|
||||
this.root = root;
|
||||
this.hashList = hashList;
|
||||
hashCode = root[0] << 24 |
|
||||
root[1] << 16 |
|
||||
root[2] << 8 |
|
||||
root[3];
|
||||
}
|
||||
public InfoHash(byte[] root, byte[] hashList) {
|
||||
if (root.length != SIZE)
|
||||
throw new IllegalArgumentException("invalid root size "+root.length);
|
||||
if (hashList != null && hashList.length % SIZE != 0)
|
||||
throw new IllegalArgumentException("invalid hashList size " + hashList.length);
|
||||
this.root = root;
|
||||
this.hashList = hashList;
|
||||
hashCode = root[0] << 24 |
|
||||
root[1] << 16 |
|
||||
root[2] << 8 |
|
||||
root[3];
|
||||
}
|
||||
|
||||
public InfoHash(byte[] root) {
|
||||
this(root, null);
|
||||
}
|
||||
public InfoHash(byte[] root) {
|
||||
this(root, null);
|
||||
}
|
||||
|
||||
public InfoHash(String base32) {
|
||||
this(Base32.decode(base32));
|
||||
}
|
||||
public InfoHash(String base32) {
|
||||
this(Base32.decode(base32));
|
||||
}
|
||||
|
||||
public static InfoHash fromHashList(byte []hashList) {
|
||||
try {
|
||||
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
|
||||
byte[] root = sha256.digest(hashList);
|
||||
return new InfoHash(root, hashList);
|
||||
} catch (NoSuchAlgorithmException impossible) {
|
||||
impossible.printStackTrace();
|
||||
System.exit(1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public static InfoHash fromHashList(byte []hashList) {
|
||||
try {
|
||||
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
|
||||
byte[] root = sha256.digest(hashList);
|
||||
return new InfoHash(root, hashList);
|
||||
} catch (NoSuchAlgorithmException impossible) {
|
||||
impossible.printStackTrace();
|
||||
System.exit(1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public byte[] getRoot() {
|
||||
return root;
|
||||
}
|
||||
public byte[] getRoot() {
|
||||
return root;
|
||||
}
|
||||
|
||||
public byte[] getHashList() {
|
||||
return hashList;
|
||||
}
|
||||
public byte[] getHashList() {
|
||||
return hashList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return hashCode;
|
||||
}
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof InfoHash)) {
|
||||
return false;
|
||||
}
|
||||
InfoHash other = (InfoHash) o;
|
||||
return Arrays.equals(root, other.root);
|
||||
}
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof InfoHash)) {
|
||||
return false;
|
||||
}
|
||||
InfoHash other = (InfoHash) o;
|
||||
return Arrays.equals(root, other.root);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
String rv = "InfoHash[root:"+Base64.encode(root) + " hashList:";
|
||||
List<String> b64HashList = new ArrayList<>();
|
||||
if (hashList != null) {
|
||||
byte [] tmp = new byte[SIZE];
|
||||
for (int i = 0; i < hashList.length / SIZE; i++) {
|
||||
System.arraycopy(hashList, SIZE * i, tmp, 0, SIZE);
|
||||
b64HashList.add(Base64.encode(tmp));
|
||||
}
|
||||
}
|
||||
rv += b64HashList.toString();
|
||||
rv += "]";
|
||||
return rv;
|
||||
}
|
||||
public String toString() {
|
||||
String rv = "InfoHash[root:"+Base64.encode(root) + " hashList:";
|
||||
List<String> b64HashList = new ArrayList<>();
|
||||
if (hashList != null) {
|
||||
byte [] tmp = new byte[SIZE];
|
||||
for (int i = 0; i < hashList.length / SIZE; i++) {
|
||||
System.arraycopy(hashList, SIZE * i, tmp, 0, SIZE);
|
||||
b64HashList.add(Base64.encode(tmp));
|
||||
}
|
||||
}
|
||||
rv += b64HashList.toString();
|
||||
rv += "]";
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
|
@ -1,41 +1,96 @@
|
||||
package com.muwire.core;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.muwire.core.util.DataUtil;
|
||||
|
||||
import net.i2p.data.Base64;
|
||||
|
||||
public class SharedFile {
|
||||
|
||||
private final File file;
|
||||
private final InfoHash infoHash;
|
||||
private final int pieceSize;
|
||||
private final File file;
|
||||
private final InfoHash infoHash;
|
||||
private final int pieceSize;
|
||||
|
||||
public SharedFile(File file, InfoHash infoHash, int pieceSize) {
|
||||
this.file = file;
|
||||
this.infoHash = infoHash;
|
||||
this.pieceSize = pieceSize;
|
||||
}
|
||||
private final String cachedPath;
|
||||
private final long cachedLength;
|
||||
|
||||
public File getFile() {
|
||||
return file;
|
||||
}
|
||||
private final String b64EncodedFileName;
|
||||
private final String b64EncodedHashRoot;
|
||||
private final List<String> b64EncodedHashList;
|
||||
|
||||
public InfoHash getInfoHash() {
|
||||
return infoHash;
|
||||
}
|
||||
public SharedFile(File file, InfoHash infoHash, int pieceSize) throws IOException {
|
||||
this.file = file;
|
||||
this.infoHash = infoHash;
|
||||
this.pieceSize = pieceSize;
|
||||
this.cachedPath = file.getAbsolutePath();
|
||||
this.cachedLength = file.length();
|
||||
this.b64EncodedFileName = Base64.encode(DataUtil.encodei18nString(file.toString()));
|
||||
this.b64EncodedHashRoot = Base64.encode(infoHash.getRoot());
|
||||
|
||||
public int getPieceSize() {
|
||||
return pieceSize;
|
||||
}
|
||||
List<String> b64List = new ArrayList<String>();
|
||||
byte[] tmp = new byte[32];
|
||||
for (int i = 0; i < infoHash.getHashList().length / 32; i++) {
|
||||
System.arraycopy(infoHash.getHashList(), i * 32, tmp, 0, 32);
|
||||
b64List.add(Base64.encode(tmp));
|
||||
}
|
||||
this.b64EncodedHashList = b64List;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return file.hashCode() ^ infoHash.hashCode();
|
||||
}
|
||||
public File getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof SharedFile))
|
||||
return false;
|
||||
SharedFile other = (SharedFile)o;
|
||||
return file.equals(other.file) && infoHash.equals(other.infoHash);
|
||||
}
|
||||
public InfoHash getInfoHash() {
|
||||
return infoHash;
|
||||
}
|
||||
|
||||
public int getPieceSize() {
|
||||
return pieceSize;
|
||||
}
|
||||
|
||||
public int getNPieces() {
|
||||
long length = file.length();
|
||||
int rawPieceSize = 0x1 << pieceSize;
|
||||
int rv = (int) (length / rawPieceSize);
|
||||
if (length % rawPieceSize != 0)
|
||||
rv++;
|
||||
return rv;
|
||||
}
|
||||
|
||||
public String getB64EncodedFileName() {
|
||||
return b64EncodedFileName;
|
||||
}
|
||||
|
||||
public String getB64EncodedHashRoot() {
|
||||
return b64EncodedHashRoot;
|
||||
}
|
||||
|
||||
public List<String> getB64EncodedHashList() {
|
||||
return b64EncodedHashList;
|
||||
}
|
||||
|
||||
public String getCachedPath() {
|
||||
return cachedPath;
|
||||
}
|
||||
|
||||
public long getCachedLength() {
|
||||
return cachedLength;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return file.hashCode() ^ infoHash.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof SharedFile))
|
||||
return false;
|
||||
SharedFile other = (SharedFile)o;
|
||||
return file.equals(other.file) && infoHash.equals(other.infoHash);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
package com.muwire.core.connection;
|
||||
|
||||
public enum ConnectionAttemptStatus {
|
||||
SUCCESSFUL, REJECTED, FAILED
|
||||
SUCCESSFUL, REJECTED, FAILED
|
||||
}
|
||||
|
@ -6,5 +6,5 @@ package com.muwire.core.hostcache;
|
||||
*
|
||||
*/
|
||||
public enum CrawlerResponse {
|
||||
ALL, REGISTERED, NONE
|
||||
ALL, REGISTERED, NONE
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
package com.muwire.core.trust;
|
||||
|
||||
public enum TrustLevel {
|
||||
TRUSTED, NEUTRAL, DISTRUSTED
|
||||
TRUSTED, NEUTRAL, DISTRUSTED
|
||||
}
|
||||
|
168
core/src/main/java/com/muwire/core/util/DataUtil.java
Normal file
168
core/src/main/java/com/muwire/core/util/DataUtil.java
Normal file
@ -0,0 +1,168 @@
|
||||
package com.muwire.core.util;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.muwire.core.Constants;
|
||||
|
||||
import net.i2p.data.Base64;
|
||||
|
||||
public class DataUtil {
|
||||
|
||||
private final static int MAX_SHORT = (0x1 << 16) - 1;
|
||||
|
||||
static void writeUnsignedShort(int value, OutputStream os) throws IOException {
|
||||
if (value > MAX_SHORT || value < 0)
|
||||
throw new IllegalArgumentException("$value invalid");
|
||||
|
||||
byte lsb = (byte) (value & 0xFF);
|
||||
byte msb = (byte) (value >> 8);
|
||||
|
||||
os.write(msb);
|
||||
os.write(lsb);
|
||||
}
|
||||
|
||||
private final static int MAX_HEADER = 0x7FFFFF;
|
||||
|
||||
static void packHeader(int length, byte [] header) {
|
||||
if (header.length != 3)
|
||||
throw new IllegalArgumentException("header length $header.length");
|
||||
if (length < 0 || length > MAX_HEADER)
|
||||
throw new IllegalArgumentException("length $length");
|
||||
|
||||
header[2] = (byte) (length & 0xFF);
|
||||
header[1] = (byte) ((length >> 8) & 0xFF);
|
||||
header[0] = (byte) ((length >> 16) & 0x7F);
|
||||
}
|
||||
|
||||
static int readLength(byte [] header) {
|
||||
if (header.length != 3)
|
||||
throw new IllegalArgumentException("header length $header.length");
|
||||
|
||||
return (((int)(header[0] & 0x7F)) << 16) |
|
||||
(((int)(header[1] & 0xFF) << 8)) |
|
||||
((int)header[2] & 0xFF);
|
||||
}
|
||||
|
||||
static String readi18nString(byte [] encoded) {
|
||||
if (encoded.length < 2)
|
||||
throw new IllegalArgumentException("encoding too short $encoded.length");
|
||||
int length = ((encoded[0] & 0xFF) << 8) | (encoded[1] & 0xFF);
|
||||
if (encoded.length != length + 2)
|
||||
throw new IllegalArgumentException("encoding doesn't match length, expected $length found $encoded.length");
|
||||
byte [] string = new byte[length];
|
||||
System.arraycopy(encoded, 2, string, 0, length);
|
||||
return new String(string, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public static byte[] encodei18nString(String string) {
|
||||
byte [] utf8 = string.getBytes(StandardCharsets.UTF_8);
|
||||
if (utf8.length > Short.MAX_VALUE)
|
||||
throw new IllegalArgumentException("String in utf8 too long $utf8.length");
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
DataOutputStream daos = new DataOutputStream(baos);
|
||||
try {
|
||||
daos.writeShort((short) utf8.length);
|
||||
daos.write(utf8);
|
||||
daos.close();
|
||||
} catch (IOException impossible) {
|
||||
throw new IllegalStateException(impossible);
|
||||
}
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
public static String readTillRN(InputStream is) throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
while(baos.size() < (Constants.MAX_HEADER_SIZE)) {
|
||||
int read = is.read();
|
||||
if (read == -1)
|
||||
throw new IOException();
|
||||
if (read == '\r') {
|
||||
if (is.read() != '\n')
|
||||
throw new IOException("invalid header");
|
||||
break;
|
||||
}
|
||||
baos.write(read);
|
||||
}
|
||||
return new String(baos.toByteArray(), StandardCharsets.US_ASCII);
|
||||
}
|
||||
|
||||
public static String encodeXHave(List<Integer> pieces, int totalPieces) {
|
||||
int bytes = totalPieces / 8;
|
||||
if (totalPieces % 8 != 0)
|
||||
bytes++;
|
||||
byte[] raw = new byte[bytes];
|
||||
for (int it : pieces) {
|
||||
int byteIdx = it / 8;
|
||||
int offset = it % 8;
|
||||
int mask = 0x80 >>> offset;
|
||||
raw[byteIdx] |= mask;
|
||||
}
|
||||
return Base64.encode(raw);
|
||||
}
|
||||
|
||||
public static List<Integer> decodeXHave(String xHave) {
|
||||
byte [] availablePieces = Base64.decode(xHave);
|
||||
List<Integer> available = new ArrayList<>();
|
||||
for (int i = 0; i < availablePieces.length; i ++) {
|
||||
byte b = availablePieces[i];
|
||||
for (int j = 0; j < 8 ; j++) {
|
||||
byte mask = (byte) (0x80 >>> j);
|
||||
if ((b & mask) == mask) {
|
||||
available.add(i * 8 + j);
|
||||
}
|
||||
}
|
||||
}
|
||||
return available;
|
||||
}
|
||||
|
||||
public static Throwable findRoot(Throwable e) {
|
||||
while(e.getCause() != null)
|
||||
e = e.getCause();
|
||||
return e;
|
||||
}
|
||||
|
||||
public static void tryUnmap(ByteBuffer cb) {
|
||||
if (cb==null || !cb.isDirect()) return;
|
||||
// we could use this type cast and call functions without reflection code,
|
||||
// but static import from sun.* package is risky for non-SUN virtual machine.
|
||||
//try { ((sun.nio.ch.DirectBuffer)cb).cleaner().clean(); } catch (Exception ex) { }
|
||||
|
||||
// JavaSpecVer: 1.6, 1.7, 1.8, 9, 10
|
||||
boolean isOldJDK = System.getProperty("java.specification.version","99").startsWith("1.");
|
||||
try {
|
||||
if (isOldJDK) {
|
||||
Method cleaner = cb.getClass().getMethod("cleaner");
|
||||
cleaner.setAccessible(true);
|
||||
Method clean = Class.forName("sun.misc.Cleaner").getMethod("clean");
|
||||
clean.setAccessible(true);
|
||||
clean.invoke(cleaner.invoke(cb));
|
||||
} else {
|
||||
Class unsafeClass;
|
||||
try {
|
||||
unsafeClass = Class.forName("sun.misc.Unsafe");
|
||||
} catch(Exception ex) {
|
||||
// jdk.internal.misc.Unsafe doesn't yet have an invokeCleaner() method,
|
||||
// but that method should be added if sun.misc.Unsafe is removed.
|
||||
unsafeClass = Class.forName("jdk.internal.misc.Unsafe");
|
||||
}
|
||||
Method clean = unsafeClass.getMethod("invokeCleaner", ByteBuffer.class);
|
||||
clean.setAccessible(true);
|
||||
Field theUnsafeField = unsafeClass.getDeclaredField("theUnsafe");
|
||||
theUnsafeField.setAccessible(true);
|
||||
Object theUnsafe = theUnsafeField.get(null);
|
||||
clean.invoke(theUnsafe, cb);
|
||||
}
|
||||
} catch(Exception ex) { }
|
||||
cb = null;
|
||||
}
|
||||
}
|
@ -4,6 +4,6 @@ import net.i2p.data.Destination
|
||||
|
||||
class Destinations {
|
||||
|
||||
Destination dest1 = new Destination("KvwWPKMSAtzf7Yruj8TQaHi2jaQpSNsXJskbpmSBTxkcYlDB2GllH~QBu-cs4FSYdaRmKDUUx7793jjnYJgTMbrjqeIL5-BTORZ09n6PUfhSejDpJjdkUxaV1OHRatfYs70RNBv7rvdj1-nXUow5tMfOJtoWVocUoKefUGFQFbJLDDkBqjm1kFyKFZv6m6S6YqXxBgVB1qYicooy67cNQF5HLUFtP15pk5fMDNGz5eNCjPfC~2Gp8FF~OpSy92HT0XN7uAMJykPcbdnWfcvVwqD7eS0K4XEnsqnMPLEiMAhqsugEFiFqtB3Wmm7UHVc03lcAfRhr1e2uZBNFTtM2Uol4MD5sCCKRZVHGcH-WGPSEz0BM5YO~Xi~dQ~N3NVud32PVzhh8xoGcAlhTqMqAbRJndCv-H6NflX90pYmbirCTIDOaR9758mThrqX0d4CwCn4jFXer52l8Qe8CErGoLuB-4LL~Gwrn7R1k7ZQc2PthkqeW8MfigyiN7hZVkul9AAAA")
|
||||
Destination dest2 = new Destination("KvwWPKMSAtzf7Yruj8TQaHi2jaQpSNsXJskbpmSBTxkcYlDB2GllH~QBu-cs4FSYdaRmKDUUx7793jjnYJgTMbrjqeIL5-BTORZ09n6PUfhSejDpJjdkUxaV1OHRatfYs70RNBv7rvdj1-nXUow5tMfOJtoWVocUoKefUGFQFbJLDDkBqjm1kFyKFZv6m6S6YqXxBgVB1qYicooy67cNQF5HLUFtP15pk5fMDNGz5eNCjPfC~2Gp8FF~OpSy92HT0XN7uAMJykPcbdnWfcvVwqD7eS0K4XEnsqnMPLEiMAhqsugEFiFqtB3Wmm7UHVc03lcAfRhr1e2uZBNFTtM2Uol4MD5sCCKRZVHGcH-WGPSEz0BM5YO~Xi~dQ~N3NVud32PVzhh8xoGcAlhTqMqAbRJndCv-H6NflX90pYmbirCTIDOaR9758mThrqX0d4CwCn4jFXer52l8Qe8CErGoLuB-4LL~Gwrn7R1k7ZQc2PthkqeW8MfigyiN7hZVkul8AAAA")
|
||||
Destination dest1 = new Destination("KvwWPKMSAtzf7Yruj8TQaHi2jaQpSNsXJskbpmSBTxkcYlDB2GllH~QBu-cs4FSYdaRmKDUUx7793jjnYJgTMbrjqeIL5-BTORZ09n6PUfhSejDpJjdkUxaV1OHRatfYs70RNBv7rvdj1-nXUow5tMfOJtoWVocUoKefUGFQFbJLDDkBqjm1kFyKFZv6m6S6YqXxBgVB1qYicooy67cNQF5HLUFtP15pk5fMDNGz5eNCjPfC~2Gp8FF~OpSy92HT0XN7uAMJykPcbdnWfcvVwqD7eS0K4XEnsqnMPLEiMAhqsugEFiFqtB3Wmm7UHVc03lcAfRhr1e2uZBNFTtM2Uol4MD5sCCKRZVHGcH-WGPSEz0BM5YO~Xi~dQ~N3NVud32PVzhh8xoGcAlhTqMqAbRJndCv-H6NflX90pYmbirCTIDOaR9758mThrqX0d4CwCn4jFXer52l8Qe8CErGoLuB-4LL~Gwrn7R1k7ZQc2PthkqeW8MfigyiN7hZVkul9AAAA")
|
||||
Destination dest2 = new Destination("KvwWPKMSAtzf7Yruj8TQaHi2jaQpSNsXJskbpmSBTxkcYlDB2GllH~QBu-cs4FSYdaRmKDUUx7793jjnYJgTMbrjqeIL5-BTORZ09n6PUfhSejDpJjdkUxaV1OHRatfYs70RNBv7rvdj1-nXUow5tMfOJtoWVocUoKefUGFQFbJLDDkBqjm1kFyKFZv6m6S6YqXxBgVB1qYicooy67cNQF5HLUFtP15pk5fMDNGz5eNCjPfC~2Gp8FF~OpSy92HT0XN7uAMJykPcbdnWfcvVwqD7eS0K4XEnsqnMPLEiMAhqsugEFiFqtB3Wmm7UHVc03lcAfRhr1e2uZBNFTtM2Uol4MD5sCCKRZVHGcH-WGPSEz0BM5YO~Xi~dQ~N3NVud32PVzhh8xoGcAlhTqMqAbRJndCv-H6NflX90pYmbirCTIDOaR9758mThrqX0d4CwCn4jFXer52l8Qe8CErGoLuB-4LL~Gwrn7R1k7ZQc2PthkqeW8MfigyiN7hZVkul8AAAA")
|
||||
}
|
||||
|
@ -4,23 +4,23 @@ import org.junit.Test
|
||||
|
||||
class EventBusTest {
|
||||
|
||||
class FakeEvent extends Event {}
|
||||
class FakeEvent extends Event {}
|
||||
|
||||
class FakeEventHandler {
|
||||
def onFakeEvent(FakeEvent e) {
|
||||
assert e == fakeEvent
|
||||
}
|
||||
}
|
||||
class FakeEventHandler {
|
||||
def onFakeEvent(FakeEvent e) {
|
||||
assert e == fakeEvent
|
||||
}
|
||||
}
|
||||
|
||||
FakeEvent fakeEvent = new FakeEvent()
|
||||
FakeEvent fakeEvent = new FakeEvent()
|
||||
|
||||
EventBus bus = new EventBus()
|
||||
def handler = new FakeEventHandler()
|
||||
EventBus bus = new EventBus()
|
||||
def handler = new FakeEventHandler()
|
||||
|
||||
@Test
|
||||
void testDynamicEvent() {
|
||||
bus.register(FakeEvent.class, handler)
|
||||
bus.publish(fakeEvent)
|
||||
}
|
||||
@Test
|
||||
void testDynamicEvent() {
|
||||
bus.register(FakeEvent.class, handler)
|
||||
bus.publish(fakeEvent)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -6,11 +6,11 @@ import org.junit.Test
|
||||
|
||||
class InfoHashTest {
|
||||
|
||||
@Test
|
||||
void testEmpty() {
|
||||
byte [] empty = new byte[0x1 << 6];
|
||||
def ih = InfoHash.fromHashList(empty)
|
||||
def ih2 = new InfoHash("6ws72qwrniqdaj4y55xngcmxtnbqapjdedm7b2hktay2sj2z7nfq");
|
||||
assertEquals(ih, ih2);
|
||||
}
|
||||
@Test
|
||||
void testEmpty() {
|
||||
byte [] empty = new byte[0x1 << 6];
|
||||
def ih = InfoHash.fromHashList(empty)
|
||||
def ih2 = new InfoHash("6ws72qwrniqdaj4y55xngcmxtnbqapjdedm7b2hktay2sj2z7nfq");
|
||||
assertEquals(ih, ih2);
|
||||
}
|
||||
}
|
||||
|
@ -22,21 +22,21 @@ import groovy.mock.interceptor.MockFor
|
||||
|
||||
class ConnectionAcceptorTest {
|
||||
|
||||
EventBus eventBus
|
||||
final Destinations destinations = new Destinations()
|
||||
def settings
|
||||
EventBus eventBus
|
||||
final Destinations destinations = new Destinations()
|
||||
def settings
|
||||
|
||||
def connectionManagerMock
|
||||
UltrapeerConnectionManager connectionManager
|
||||
def connectionManagerMock
|
||||
UltrapeerConnectionManager connectionManager
|
||||
|
||||
def i2pAcceptorMock
|
||||
I2PAcceptor i2pAcceptor
|
||||
def i2pAcceptorMock
|
||||
I2PAcceptor i2pAcceptor
|
||||
|
||||
def hostCacheMock
|
||||
HostCache hostCache
|
||||
def hostCacheMock
|
||||
HostCache hostCache
|
||||
|
||||
def trustServiceMock
|
||||
TrustService trustService
|
||||
def trustServiceMock
|
||||
TrustService trustService
|
||||
|
||||
def searchManagerMock
|
||||
SearchManager searchManager
|
||||
@ -47,361 +47,361 @@ class ConnectionAcceptorTest {
|
||||
def connectionEstablisherMock
|
||||
ConnectionEstablisher connectionEstablisher
|
||||
|
||||
ConnectionAcceptor acceptor
|
||||
List<ConnectionEvent> connectionEvents
|
||||
InputStream inputStream
|
||||
OutputStream outputStream
|
||||
ConnectionAcceptor acceptor
|
||||
List<ConnectionEvent> connectionEvents
|
||||
InputStream inputStream
|
||||
OutputStream outputStream
|
||||
|
||||
@Before
|
||||
void before() {
|
||||
connectionManagerMock = new MockFor(UltrapeerConnectionManager.class)
|
||||
i2pAcceptorMock = new MockFor(I2PAcceptor.class)
|
||||
hostCacheMock = new MockFor(HostCache.class)
|
||||
trustServiceMock = new MockFor(TrustService.class)
|
||||
@Before
|
||||
void before() {
|
||||
connectionManagerMock = new MockFor(UltrapeerConnectionManager.class)
|
||||
i2pAcceptorMock = new MockFor(I2PAcceptor.class)
|
||||
hostCacheMock = new MockFor(HostCache.class)
|
||||
trustServiceMock = new MockFor(TrustService.class)
|
||||
searchManagerMock = new MockFor(SearchManager.class)
|
||||
uploadManagerMock = new MockFor(UploadManager.class)
|
||||
connectionEstablisherMock = new MockFor(ConnectionEstablisher.class)
|
||||
}
|
||||
}
|
||||
|
||||
@After
|
||||
void after() {
|
||||
acceptor?.stop()
|
||||
connectionManagerMock.verify connectionManager
|
||||
i2pAcceptorMock.verify i2pAcceptor
|
||||
hostCacheMock.verify hostCache
|
||||
trustServiceMock.verify trustService
|
||||
@After
|
||||
void after() {
|
||||
acceptor?.stop()
|
||||
connectionManagerMock.verify connectionManager
|
||||
i2pAcceptorMock.verify i2pAcceptor
|
||||
hostCacheMock.verify hostCache
|
||||
trustServiceMock.verify trustService
|
||||
searchManagerMock.verify searchManager
|
||||
uploadManagerMock.verify uploadManager
|
||||
connectionEstablisherMock.verify connectionEstablisher
|
||||
Thread.sleep(100)
|
||||
}
|
||||
Thread.sleep(100)
|
||||
}
|
||||
|
||||
private void initMocks() {
|
||||
connectionEvents = new CopyOnWriteArrayList()
|
||||
eventBus = new EventBus()
|
||||
def listener = new Object() {
|
||||
void onConnectionEvent(ConnectionEvent e) {
|
||||
connectionEvents.add e
|
||||
}
|
||||
}
|
||||
eventBus.register(ConnectionEvent.class, listener)
|
||||
private void initMocks() {
|
||||
connectionEvents = new CopyOnWriteArrayList()
|
||||
eventBus = new EventBus()
|
||||
def listener = new Object() {
|
||||
void onConnectionEvent(ConnectionEvent e) {
|
||||
connectionEvents.add e
|
||||
}
|
||||
}
|
||||
eventBus.register(ConnectionEvent.class, listener)
|
||||
|
||||
connectionManager = connectionManagerMock.proxyInstance()
|
||||
i2pAcceptor = i2pAcceptorMock.proxyInstance()
|
||||
hostCache = hostCacheMock.proxyInstance()
|
||||
trustService = trustServiceMock.proxyInstance()
|
||||
connectionManager = connectionManagerMock.proxyInstance()
|
||||
i2pAcceptor = i2pAcceptorMock.proxyInstance()
|
||||
hostCache = hostCacheMock.proxyInstance()
|
||||
trustService = trustServiceMock.proxyInstance()
|
||||
searchManager = searchManagerMock.proxyInstance()
|
||||
uploadManager = uploadManagerMock.proxyInstance()
|
||||
connectionEstablisher = connectionEstablisherMock.proxyInstance()
|
||||
|
||||
acceptor = new ConnectionAcceptor(eventBus, connectionManager, settings, i2pAcceptor,
|
||||
acceptor = new ConnectionAcceptor(eventBus, connectionManager, settings, i2pAcceptor,
|
||||
hostCache, trustService, searchManager, uploadManager, connectionEstablisher)
|
||||
acceptor.start()
|
||||
Thread.sleep(100)
|
||||
}
|
||||
acceptor.start()
|
||||
Thread.sleep(100)
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccessfulLeaf() {
|
||||
settings = new MuWireSettings() {
|
||||
boolean isLeaf() {
|
||||
false
|
||||
}
|
||||
}
|
||||
i2pAcceptorMock.demand.accept {
|
||||
def is = new PipedInputStream()
|
||||
outputStream = new PipedOutputStream(is)
|
||||
def os = new PipedOutputStream()
|
||||
inputStream = new PipedInputStream(os)
|
||||
new Endpoint(destinations.dest1, is, os, null)
|
||||
}
|
||||
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
|
||||
@Test
|
||||
void testSuccessfulLeaf() {
|
||||
settings = new MuWireSettings() {
|
||||
boolean isLeaf() {
|
||||
false
|
||||
}
|
||||
}
|
||||
i2pAcceptorMock.demand.accept {
|
||||
def is = new PipedInputStream()
|
||||
outputStream = new PipedOutputStream(is)
|
||||
def os = new PipedOutputStream()
|
||||
inputStream = new PipedInputStream(os)
|
||||
new Endpoint(destinations.dest1, is, os, null)
|
||||
}
|
||||
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
|
||||
connectionEstablisherMock.demand.isInProgress(destinations.dest1) { false }
|
||||
connectionManagerMock.demand.isConnected { dest ->
|
||||
assert dest == destinations.dest1
|
||||
false
|
||||
}
|
||||
connectionManagerMock.demand.hasLeafSlots() { true }
|
||||
trustServiceMock.demand.getLevel { dest ->
|
||||
assert dest == destinations.dest1
|
||||
TrustLevel.TRUSTED
|
||||
}
|
||||
trustServiceMock.demand.getLevel { dest ->
|
||||
assert dest == destinations.dest1
|
||||
TrustLevel.TRUSTED
|
||||
}
|
||||
|
||||
initMocks()
|
||||
initMocks()
|
||||
|
||||
outputStream.write("MuWire leaf".bytes)
|
||||
byte [] OK = new byte[2]
|
||||
def dis = new DataInputStream(inputStream)
|
||||
dis.readFully(OK)
|
||||
assert OK == "OK".bytes
|
||||
outputStream.write("MuWire leaf".bytes)
|
||||
byte [] OK = new byte[2]
|
||||
def dis = new DataInputStream(inputStream)
|
||||
dis.readFully(OK)
|
||||
assert OK == "OK".bytes
|
||||
|
||||
Thread.sleep(50)
|
||||
assert connectionEvents.size() == 1
|
||||
def event = connectionEvents[0]
|
||||
assert event.endpoint.destination == destinations.dest1
|
||||
assert event.status == ConnectionAttemptStatus.SUCCESSFUL
|
||||
assert event.incoming == true
|
||||
assert event.leaf == true
|
||||
}
|
||||
Thread.sleep(50)
|
||||
assert connectionEvents.size() == 1
|
||||
def event = connectionEvents[0]
|
||||
assert event.endpoint.destination == destinations.dest1
|
||||
assert event.status == ConnectionAttemptStatus.SUCCESSFUL
|
||||
assert event.incoming == true
|
||||
assert event.leaf == true
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccessfulPeer() {
|
||||
settings = new MuWireSettings() {
|
||||
boolean isLeaf() {
|
||||
false
|
||||
}
|
||||
}
|
||||
i2pAcceptorMock.demand.accept {
|
||||
def is = new PipedInputStream()
|
||||
outputStream = new PipedOutputStream(is)
|
||||
def os = new PipedOutputStream()
|
||||
inputStream = new PipedInputStream(os)
|
||||
new Endpoint(destinations.dest1, is, os, null)
|
||||
}
|
||||
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
|
||||
@Test
|
||||
void testSuccessfulPeer() {
|
||||
settings = new MuWireSettings() {
|
||||
boolean isLeaf() {
|
||||
false
|
||||
}
|
||||
}
|
||||
i2pAcceptorMock.demand.accept {
|
||||
def is = new PipedInputStream()
|
||||
outputStream = new PipedOutputStream(is)
|
||||
def os = new PipedOutputStream()
|
||||
inputStream = new PipedInputStream(os)
|
||||
new Endpoint(destinations.dest1, is, os, null)
|
||||
}
|
||||
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
|
||||
connectionEstablisherMock.demand.isInProgress(destinations.dest1) { false }
|
||||
connectionManagerMock.demand.isConnected { dest ->
|
||||
assert dest == destinations.dest1
|
||||
false
|
||||
}
|
||||
connectionManagerMock.demand.hasPeerSlots() { true }
|
||||
trustServiceMock.demand.getLevel { dest ->
|
||||
assert dest == destinations.dest1
|
||||
TrustLevel.TRUSTED
|
||||
}
|
||||
trustServiceMock.demand.getLevel { dest ->
|
||||
assert dest == destinations.dest1
|
||||
TrustLevel.TRUSTED
|
||||
}
|
||||
|
||||
initMocks()
|
||||
initMocks()
|
||||
|
||||
outputStream.write("MuWire peer".bytes)
|
||||
byte [] OK = new byte[2]
|
||||
def dis = new DataInputStream(inputStream)
|
||||
dis.readFully(OK)
|
||||
assert OK == "OK".bytes
|
||||
outputStream.write("MuWire peer".bytes)
|
||||
byte [] OK = new byte[2]
|
||||
def dis = new DataInputStream(inputStream)
|
||||
dis.readFully(OK)
|
||||
assert OK == "OK".bytes
|
||||
|
||||
Thread.sleep(50)
|
||||
assert connectionEvents.size() == 1
|
||||
def event = connectionEvents[0]
|
||||
assert event.endpoint.destination == destinations.dest1
|
||||
assert event.status == ConnectionAttemptStatus.SUCCESSFUL
|
||||
assert event.incoming == true
|
||||
assert event.leaf == false
|
||||
}
|
||||
Thread.sleep(50)
|
||||
assert connectionEvents.size() == 1
|
||||
def event = connectionEvents[0]
|
||||
assert event.endpoint.destination == destinations.dest1
|
||||
assert event.status == ConnectionAttemptStatus.SUCCESSFUL
|
||||
assert event.incoming == true
|
||||
assert event.leaf == false
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLeafRejectsLeaf() {
|
||||
settings = new MuWireSettings() {
|
||||
boolean isLeaf() {
|
||||
true
|
||||
}
|
||||
}
|
||||
i2pAcceptorMock.demand.accept {
|
||||
def is = new PipedInputStream()
|
||||
outputStream = new PipedOutputStream(is)
|
||||
def os = new PipedOutputStream()
|
||||
inputStream = new PipedInputStream(os)
|
||||
new Endpoint(destinations.dest1, is, os, null)
|
||||
}
|
||||
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
|
||||
trustServiceMock.demand.getLevel { dest ->
|
||||
assert dest == destinations.dest1
|
||||
TrustLevel.TRUSTED
|
||||
}
|
||||
@Test
|
||||
void testLeafRejectsLeaf() {
|
||||
settings = new MuWireSettings() {
|
||||
boolean isLeaf() {
|
||||
true
|
||||
}
|
||||
}
|
||||
i2pAcceptorMock.demand.accept {
|
||||
def is = new PipedInputStream()
|
||||
outputStream = new PipedOutputStream(is)
|
||||
def os = new PipedOutputStream()
|
||||
inputStream = new PipedInputStream(os)
|
||||
new Endpoint(destinations.dest1, is, os, null)
|
||||
}
|
||||
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
|
||||
trustServiceMock.demand.getLevel { dest ->
|
||||
assert dest == destinations.dest1
|
||||
TrustLevel.TRUSTED
|
||||
}
|
||||
|
||||
initMocks()
|
||||
initMocks()
|
||||
|
||||
outputStream.write("MuWire leaf".bytes)
|
||||
outputStream.flush()
|
||||
Thread.sleep(50)
|
||||
assert inputStream.read() == -1
|
||||
outputStream.write("MuWire leaf".bytes)
|
||||
outputStream.flush()
|
||||
Thread.sleep(50)
|
||||
assert inputStream.read() == -1
|
||||
|
||||
Thread.sleep(50)
|
||||
assert connectionEvents.size() == 1
|
||||
def event = connectionEvents[0]
|
||||
assert event.endpoint.destination == destinations.dest1
|
||||
assert event.status == ConnectionAttemptStatus.FAILED
|
||||
assert event.incoming == true
|
||||
assert event.leaf == null
|
||||
}
|
||||
Thread.sleep(50)
|
||||
assert connectionEvents.size() == 1
|
||||
def event = connectionEvents[0]
|
||||
assert event.endpoint.destination == destinations.dest1
|
||||
assert event.status == ConnectionAttemptStatus.FAILED
|
||||
assert event.incoming == true
|
||||
assert event.leaf == null
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLeafRejectsPeer() {
|
||||
settings = new MuWireSettings() {
|
||||
boolean isLeaf() {
|
||||
true
|
||||
}
|
||||
}
|
||||
i2pAcceptorMock.demand.accept {
|
||||
def is = new PipedInputStream()
|
||||
outputStream = new PipedOutputStream(is)
|
||||
def os = new PipedOutputStream()
|
||||
inputStream = new PipedInputStream(os)
|
||||
new Endpoint(destinations.dest1, is, os, null)
|
||||
}
|
||||
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
|
||||
trustServiceMock.demand.getLevel { dest ->
|
||||
assert dest == destinations.dest1
|
||||
TrustLevel.TRUSTED
|
||||
}
|
||||
@Test
|
||||
void testLeafRejectsPeer() {
|
||||
settings = new MuWireSettings() {
|
||||
boolean isLeaf() {
|
||||
true
|
||||
}
|
||||
}
|
||||
i2pAcceptorMock.demand.accept {
|
||||
def is = new PipedInputStream()
|
||||
outputStream = new PipedOutputStream(is)
|
||||
def os = new PipedOutputStream()
|
||||
inputStream = new PipedInputStream(os)
|
||||
new Endpoint(destinations.dest1, is, os, null)
|
||||
}
|
||||
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
|
||||
trustServiceMock.demand.getLevel { dest ->
|
||||
assert dest == destinations.dest1
|
||||
TrustLevel.TRUSTED
|
||||
}
|
||||
|
||||
initMocks()
|
||||
initMocks()
|
||||
|
||||
outputStream.write("MuWire peer".bytes)
|
||||
outputStream.flush()
|
||||
Thread.sleep(50)
|
||||
assert inputStream.read() == -1
|
||||
outputStream.write("MuWire peer".bytes)
|
||||
outputStream.flush()
|
||||
Thread.sleep(50)
|
||||
assert inputStream.read() == -1
|
||||
|
||||
Thread.sleep(50)
|
||||
assert connectionEvents.size() == 1
|
||||
def event = connectionEvents[0]
|
||||
assert event.endpoint.destination == destinations.dest1
|
||||
assert event.status == ConnectionAttemptStatus.FAILED
|
||||
assert event.incoming == true
|
||||
assert event.leaf == null
|
||||
}
|
||||
Thread.sleep(50)
|
||||
assert connectionEvents.size() == 1
|
||||
def event = connectionEvents[0]
|
||||
assert event.endpoint.destination == destinations.dest1
|
||||
assert event.status == ConnectionAttemptStatus.FAILED
|
||||
assert event.incoming == true
|
||||
assert event.leaf == null
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPeerRejectsPeerSlots() {
|
||||
settings = new MuWireSettings() {
|
||||
boolean isLeaf() {
|
||||
false
|
||||
}
|
||||
}
|
||||
i2pAcceptorMock.demand.accept {
|
||||
def is = new PipedInputStream()
|
||||
outputStream = new PipedOutputStream(is)
|
||||
def os = new PipedOutputStream()
|
||||
inputStream = new PipedInputStream(os)
|
||||
new Endpoint(destinations.dest1, is, os, null)
|
||||
}
|
||||
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
|
||||
@Test
|
||||
void testPeerRejectsPeerSlots() {
|
||||
settings = new MuWireSettings() {
|
||||
boolean isLeaf() {
|
||||
false
|
||||
}
|
||||
}
|
||||
i2pAcceptorMock.demand.accept {
|
||||
def is = new PipedInputStream()
|
||||
outputStream = new PipedOutputStream(is)
|
||||
def os = new PipedOutputStream()
|
||||
inputStream = new PipedInputStream(os)
|
||||
new Endpoint(destinations.dest1, is, os, null)
|
||||
}
|
||||
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
|
||||
connectionEstablisherMock.demand.isInProgress(destinations.dest1) { false }
|
||||
connectionManagerMock.demand.isConnected { dest ->
|
||||
assert dest == destinations.dest1
|
||||
false
|
||||
}
|
||||
connectionManagerMock.demand.hasPeerSlots() { false }
|
||||
trustServiceMock.demand.getLevel { dest ->
|
||||
assert dest == destinations.dest1
|
||||
TrustLevel.TRUSTED
|
||||
}
|
||||
hostCacheMock.ignore.getGoodHosts { n -> [] }
|
||||
trustServiceMock.demand.getLevel { dest ->
|
||||
assert dest == destinations.dest1
|
||||
TrustLevel.TRUSTED
|
||||
}
|
||||
hostCacheMock.ignore.getGoodHosts { n -> [] }
|
||||
|
||||
initMocks()
|
||||
initMocks()
|
||||
|
||||
outputStream.write("MuWire peer".bytes)
|
||||
byte [] OK = new byte[6]
|
||||
def dis = new DataInputStream(inputStream)
|
||||
dis.readFully(OK)
|
||||
assert OK == "REJECT".bytes
|
||||
outputStream.write("MuWire peer".bytes)
|
||||
byte [] OK = new byte[6]
|
||||
def dis = new DataInputStream(inputStream)
|
||||
dis.readFully(OK)
|
||||
assert OK == "REJECT".bytes
|
||||
|
||||
Thread.sleep(50)
|
||||
assert dis.read() == -1
|
||||
Thread.sleep(50)
|
||||
assert dis.read() == -1
|
||||
|
||||
Thread.sleep(50)
|
||||
assert connectionEvents.size() == 1
|
||||
def event = connectionEvents[0]
|
||||
assert event.endpoint.destination == destinations.dest1
|
||||
assert event.status == ConnectionAttemptStatus.REJECTED
|
||||
assert event.incoming == true
|
||||
assert event.leaf == false
|
||||
}
|
||||
Thread.sleep(50)
|
||||
assert connectionEvents.size() == 1
|
||||
def event = connectionEvents[0]
|
||||
assert event.endpoint.destination == destinations.dest1
|
||||
assert event.status == ConnectionAttemptStatus.REJECTED
|
||||
assert event.incoming == true
|
||||
assert event.leaf == false
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPeerRejectsLeafSlots() {
|
||||
settings = new MuWireSettings() {
|
||||
boolean isLeaf() {
|
||||
false
|
||||
}
|
||||
}
|
||||
i2pAcceptorMock.demand.accept {
|
||||
def is = new PipedInputStream()
|
||||
outputStream = new PipedOutputStream(is)
|
||||
def os = new PipedOutputStream()
|
||||
inputStream = new PipedInputStream(os)
|
||||
new Endpoint(destinations.dest1, is, os, null)
|
||||
}
|
||||
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
|
||||
@Test
|
||||
void testPeerRejectsLeafSlots() {
|
||||
settings = new MuWireSettings() {
|
||||
boolean isLeaf() {
|
||||
false
|
||||
}
|
||||
}
|
||||
i2pAcceptorMock.demand.accept {
|
||||
def is = new PipedInputStream()
|
||||
outputStream = new PipedOutputStream(is)
|
||||
def os = new PipedOutputStream()
|
||||
inputStream = new PipedInputStream(os)
|
||||
new Endpoint(destinations.dest1, is, os, null)
|
||||
}
|
||||
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
|
||||
connectionEstablisherMock.demand.isInProgress(destinations.dest1) { false }
|
||||
connectionManagerMock.demand.isConnected { dest ->
|
||||
assert dest == destinations.dest1
|
||||
false
|
||||
}
|
||||
connectionManagerMock.demand.hasLeafSlots() { false }
|
||||
trustServiceMock.demand.getLevel { dest ->
|
||||
assert dest == destinations.dest1
|
||||
TrustLevel.TRUSTED
|
||||
}
|
||||
hostCacheMock.ignore.getGoodHosts { n -> [] }
|
||||
trustServiceMock.demand.getLevel { dest ->
|
||||
assert dest == destinations.dest1
|
||||
TrustLevel.TRUSTED
|
||||
}
|
||||
hostCacheMock.ignore.getGoodHosts { n -> [] }
|
||||
|
||||
initMocks()
|
||||
initMocks()
|
||||
|
||||
outputStream.write("MuWire leaf".bytes)
|
||||
byte [] OK = new byte[6]
|
||||
def dis = new DataInputStream(inputStream)
|
||||
dis.readFully(OK)
|
||||
assert OK == "REJECT".bytes
|
||||
outputStream.write("MuWire leaf".bytes)
|
||||
byte [] OK = new byte[6]
|
||||
def dis = new DataInputStream(inputStream)
|
||||
dis.readFully(OK)
|
||||
assert OK == "REJECT".bytes
|
||||
|
||||
Thread.sleep(50)
|
||||
assert dis.read() == -1
|
||||
Thread.sleep(50)
|
||||
assert dis.read() == -1
|
||||
|
||||
Thread.sleep(50)
|
||||
assert connectionEvents.size() == 1
|
||||
def event = connectionEvents[0]
|
||||
assert event.endpoint.destination == destinations.dest1
|
||||
assert event.status == ConnectionAttemptStatus.REJECTED
|
||||
assert event.incoming == true
|
||||
assert event.leaf == true
|
||||
}
|
||||
Thread.sleep(50)
|
||||
assert connectionEvents.size() == 1
|
||||
def event = connectionEvents[0]
|
||||
assert event.endpoint.destination == destinations.dest1
|
||||
assert event.status == ConnectionAttemptStatus.REJECTED
|
||||
assert event.incoming == true
|
||||
assert event.leaf == true
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPeerRejectsPeerSuggests() {
|
||||
settings = new MuWireSettings() {
|
||||
boolean isLeaf() {
|
||||
false
|
||||
}
|
||||
}
|
||||
i2pAcceptorMock.demand.accept {
|
||||
def is = new PipedInputStream()
|
||||
outputStream = new PipedOutputStream(is)
|
||||
def os = new PipedOutputStream()
|
||||
inputStream = new PipedInputStream(os)
|
||||
new Endpoint(destinations.dest1, is, os, null)
|
||||
}
|
||||
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
|
||||
@Test
|
||||
void testPeerRejectsPeerSuggests() {
|
||||
settings = new MuWireSettings() {
|
||||
boolean isLeaf() {
|
||||
false
|
||||
}
|
||||
}
|
||||
i2pAcceptorMock.demand.accept {
|
||||
def is = new PipedInputStream()
|
||||
outputStream = new PipedOutputStream(is)
|
||||
def os = new PipedOutputStream()
|
||||
inputStream = new PipedInputStream(os)
|
||||
new Endpoint(destinations.dest1, is, os, null)
|
||||
}
|
||||
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
|
||||
connectionEstablisherMock.demand.isInProgress(destinations.dest1) { false }
|
||||
connectionManagerMock.demand.isConnected { dest ->
|
||||
assert dest == destinations.dest1
|
||||
false
|
||||
}
|
||||
connectionManagerMock.demand.hasPeerSlots() { false }
|
||||
trustServiceMock.demand.getLevel { dest ->
|
||||
assert dest == destinations.dest1
|
||||
TrustLevel.TRUSTED
|
||||
}
|
||||
hostCacheMock.ignore.getGoodHosts { n -> [destinations.dest2] }
|
||||
trustServiceMock.demand.getLevel { dest ->
|
||||
assert dest == destinations.dest1
|
||||
TrustLevel.TRUSTED
|
||||
}
|
||||
hostCacheMock.ignore.getGoodHosts { n -> [destinations.dest2] }
|
||||
|
||||
initMocks()
|
||||
initMocks()
|
||||
|
||||
outputStream.write("MuWire peer".bytes)
|
||||
byte [] OK = new byte[6]
|
||||
def dis = new DataInputStream(inputStream)
|
||||
dis.readFully(OK)
|
||||
assert OK == "REJECT".bytes
|
||||
outputStream.write("MuWire peer".bytes)
|
||||
byte [] OK = new byte[6]
|
||||
def dis = new DataInputStream(inputStream)
|
||||
dis.readFully(OK)
|
||||
assert OK == "REJECT".bytes
|
||||
|
||||
short payloadSize = dis.readUnsignedShort()
|
||||
byte[] payload = new byte[payloadSize]
|
||||
dis.readFully(payload)
|
||||
assert dis.read() == -1
|
||||
short payloadSize = dis.readUnsignedShort()
|
||||
byte[] payload = new byte[payloadSize]
|
||||
dis.readFully(payload)
|
||||
assert dis.read() == -1
|
||||
|
||||
def json = new JsonSlurper()
|
||||
json = json.parse(payload)
|
||||
assert json.tryHosts != null
|
||||
assert json.tryHosts.size() == 1
|
||||
assert json.tryHosts.contains(destinations.dest2.toBase64())
|
||||
def json = new JsonSlurper()
|
||||
json = json.parse(payload)
|
||||
assert json.tryHosts != null
|
||||
assert json.tryHosts.size() == 1
|
||||
assert json.tryHosts.contains(destinations.dest2.toBase64())
|
||||
|
||||
Thread.sleep(50)
|
||||
assert connectionEvents.size() == 1
|
||||
def event = connectionEvents[0]
|
||||
assert event.endpoint.destination == destinations.dest1
|
||||
assert event.status == ConnectionAttemptStatus.REJECTED
|
||||
}
|
||||
Thread.sleep(50)
|
||||
assert connectionEvents.size() == 1
|
||||
def event = connectionEvents[0]
|
||||
assert event.endpoint.destination == destinations.dest1
|
||||
assert event.status == ConnectionAttemptStatus.REJECTED
|
||||
}
|
||||
}
|
||||
|
@ -17,271 +17,271 @@ import groovy.mock.interceptor.MockFor
|
||||
|
||||
class ConnectionEstablisherTest {
|
||||
|
||||
EventBus eventBus
|
||||
final Destinations destinations = new Destinations()
|
||||
List<ConnectionEvent> connectionEvents
|
||||
List<HostDiscoveredEvent> discoveredEvents
|
||||
DataInputStream inputStream
|
||||
DataOutputStream outputStream
|
||||
EventBus eventBus
|
||||
final Destinations destinations = new Destinations()
|
||||
List<ConnectionEvent> connectionEvents
|
||||
List<HostDiscoveredEvent> discoveredEvents
|
||||
DataInputStream inputStream
|
||||
DataOutputStream outputStream
|
||||
|
||||
def i2pConnectorMock
|
||||
I2PConnector i2pConnector
|
||||
def i2pConnectorMock
|
||||
I2PConnector i2pConnector
|
||||
|
||||
MuWireSettings settings
|
||||
MuWireSettings settings
|
||||
|
||||
def connectionManagerMock
|
||||
ConnectionManager connectionManager
|
||||
def connectionManagerMock
|
||||
ConnectionManager connectionManager
|
||||
|
||||
def hostCacheMock
|
||||
HostCache hostCache
|
||||
def hostCacheMock
|
||||
HostCache hostCache
|
||||
|
||||
ConnectionEstablisher establisher
|
||||
ConnectionEstablisher establisher
|
||||
|
||||
|
||||
@Before
|
||||
void before() {
|
||||
connectionEvents = new CopyOnWriteArrayList()
|
||||
discoveredEvents = new CopyOnWriteArrayList()
|
||||
def listener = new Object() {
|
||||
void onConnectionEvent(ConnectionEvent e) {
|
||||
connectionEvents.add(e)
|
||||
}
|
||||
void onHostDiscoveredEvent(HostDiscoveredEvent e) {
|
||||
discoveredEvents.add e
|
||||
}
|
||||
}
|
||||
eventBus = new EventBus()
|
||||
eventBus.register(ConnectionEvent.class, listener)
|
||||
eventBus.register(HostDiscoveredEvent.class, listener)
|
||||
i2pConnectorMock = new MockFor(I2PConnector.class)
|
||||
connectionManagerMock = new MockFor(ConnectionManager.class)
|
||||
hostCacheMock = new MockFor(HostCache.class)
|
||||
}
|
||||
@Before
|
||||
void before() {
|
||||
connectionEvents = new CopyOnWriteArrayList()
|
||||
discoveredEvents = new CopyOnWriteArrayList()
|
||||
def listener = new Object() {
|
||||
void onConnectionEvent(ConnectionEvent e) {
|
||||
connectionEvents.add(e)
|
||||
}
|
||||
void onHostDiscoveredEvent(HostDiscoveredEvent e) {
|
||||
discoveredEvents.add e
|
||||
}
|
||||
}
|
||||
eventBus = new EventBus()
|
||||
eventBus.register(ConnectionEvent.class, listener)
|
||||
eventBus.register(HostDiscoveredEvent.class, listener)
|
||||
i2pConnectorMock = new MockFor(I2PConnector.class)
|
||||
connectionManagerMock = new MockFor(ConnectionManager.class)
|
||||
hostCacheMock = new MockFor(HostCache.class)
|
||||
}
|
||||
|
||||
@After
|
||||
void after() {
|
||||
establisher?.stop()
|
||||
i2pConnectorMock.verify i2pConnector
|
||||
connectionManagerMock.verify connectionManager
|
||||
hostCacheMock.verify hostCache
|
||||
Thread.sleep(100)
|
||||
}
|
||||
@After
|
||||
void after() {
|
||||
establisher?.stop()
|
||||
i2pConnectorMock.verify i2pConnector
|
||||
connectionManagerMock.verify connectionManager
|
||||
hostCacheMock.verify hostCache
|
||||
Thread.sleep(100)
|
||||
}
|
||||
|
||||
private void initMocks() {
|
||||
i2pConnector = i2pConnectorMock.proxyInstance()
|
||||
connectionManager = connectionManagerMock.proxyInstance()
|
||||
hostCache = hostCacheMock.proxyInstance()
|
||||
establisher = new ConnectionEstablisher(eventBus, i2pConnector, settings, connectionManager, hostCache)
|
||||
establisher.start()
|
||||
Thread.sleep(250)
|
||||
}
|
||||
private void initMocks() {
|
||||
i2pConnector = i2pConnectorMock.proxyInstance()
|
||||
connectionManager = connectionManagerMock.proxyInstance()
|
||||
hostCache = hostCacheMock.proxyInstance()
|
||||
establisher = new ConnectionEstablisher(eventBus, i2pConnector, settings, connectionManager, hostCache)
|
||||
establisher.start()
|
||||
Thread.sleep(250)
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testConnectFails() {
|
||||
settings = new MuWireSettings()
|
||||
connectionManagerMock.ignore.needsConnections {
|
||||
true
|
||||
}
|
||||
hostCacheMock.ignore.getHosts { num ->
|
||||
assert num == 1
|
||||
[destinations.dest1]
|
||||
}
|
||||
connectionManagerMock.ignore.isConnected { dest ->
|
||||
assert dest == destinations.dest1
|
||||
false
|
||||
}
|
||||
i2pConnectorMock.demand.connect { dest ->
|
||||
assert dest == destinations.dest1
|
||||
throw new IOException()
|
||||
}
|
||||
@Test
|
||||
void testConnectFails() {
|
||||
settings = new MuWireSettings()
|
||||
connectionManagerMock.ignore.needsConnections {
|
||||
true
|
||||
}
|
||||
hostCacheMock.ignore.getHosts { num ->
|
||||
assert num == 1
|
||||
[destinations.dest1]
|
||||
}
|
||||
connectionManagerMock.ignore.isConnected { dest ->
|
||||
assert dest == destinations.dest1
|
||||
false
|
||||
}
|
||||
i2pConnectorMock.demand.connect { dest ->
|
||||
assert dest == destinations.dest1
|
||||
throw new IOException()
|
||||
}
|
||||
|
||||
initMocks()
|
||||
initMocks()
|
||||
|
||||
assert connectionEvents.size() == 1
|
||||
def event = connectionEvents[0]
|
||||
assert event.endpoint.destination == destinations.dest1
|
||||
assert event.incoming == false
|
||||
assert event.status == ConnectionAttemptStatus.FAILED
|
||||
}
|
||||
assert connectionEvents.size() == 1
|
||||
def event = connectionEvents[0]
|
||||
assert event.endpoint.destination == destinations.dest1
|
||||
assert event.incoming == false
|
||||
assert event.status == ConnectionAttemptStatus.FAILED
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConnectionSucceedsPeer() {
|
||||
settings = new MuWireSettings() {
|
||||
boolean isLeaf() {false}
|
||||
}
|
||||
connectionManagerMock.ignore.needsConnections {
|
||||
true
|
||||
}
|
||||
hostCacheMock.ignore.getHosts { num ->
|
||||
assert num == 1
|
||||
[destinations.dest1]
|
||||
}
|
||||
connectionManagerMock.ignore.isConnected { dest ->
|
||||
assert dest == destinations.dest1
|
||||
false
|
||||
}
|
||||
i2pConnectorMock.demand.connect { dest ->
|
||||
PipedOutputStream os = new PipedOutputStream()
|
||||
inputStream = new DataInputStream(new PipedInputStream(os))
|
||||
PipedInputStream is = new PipedInputStream()
|
||||
outputStream = new DataOutputStream(new PipedOutputStream(is))
|
||||
new Endpoint(dest, is, os, null)
|
||||
}
|
||||
@Test
|
||||
void testConnectionSucceedsPeer() {
|
||||
settings = new MuWireSettings() {
|
||||
boolean isLeaf() {false}
|
||||
}
|
||||
connectionManagerMock.ignore.needsConnections {
|
||||
true
|
||||
}
|
||||
hostCacheMock.ignore.getHosts { num ->
|
||||
assert num == 1
|
||||
[destinations.dest1]
|
||||
}
|
||||
connectionManagerMock.ignore.isConnected { dest ->
|
||||
assert dest == destinations.dest1
|
||||
false
|
||||
}
|
||||
i2pConnectorMock.demand.connect { dest ->
|
||||
PipedOutputStream os = new PipedOutputStream()
|
||||
inputStream = new DataInputStream(new PipedInputStream(os))
|
||||
PipedInputStream is = new PipedInputStream()
|
||||
outputStream = new DataOutputStream(new PipedOutputStream(is))
|
||||
new Endpoint(dest, is, os, null)
|
||||
}
|
||||
|
||||
initMocks()
|
||||
initMocks()
|
||||
|
||||
byte [] header = new byte[11]
|
||||
inputStream.readFully(header)
|
||||
assert header == "MuWire peer".bytes
|
||||
byte [] header = new byte[11]
|
||||
inputStream.readFully(header)
|
||||
assert header == "MuWire peer".bytes
|
||||
|
||||
outputStream.write("OK".bytes)
|
||||
outputStream.flush()
|
||||
outputStream.write("OK".bytes)
|
||||
outputStream.flush()
|
||||
|
||||
Thread.sleep(100)
|
||||
Thread.sleep(100)
|
||||
|
||||
assert connectionEvents.size() == 1
|
||||
def event = connectionEvents[0]
|
||||
assert event.endpoint.destination == destinations.dest1
|
||||
assert event.incoming == false
|
||||
assert event.status == ConnectionAttemptStatus.SUCCESSFUL
|
||||
assert connectionEvents.size() == 1
|
||||
def event = connectionEvents[0]
|
||||
assert event.endpoint.destination == destinations.dest1
|
||||
assert event.incoming == false
|
||||
assert event.status == ConnectionAttemptStatus.SUCCESSFUL
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConnectionSucceedsLeaf() {
|
||||
settings = new MuWireSettings() {
|
||||
boolean isLeaf() {true}
|
||||
}
|
||||
connectionManagerMock.ignore.needsConnections {
|
||||
true
|
||||
}
|
||||
hostCacheMock.ignore.getHosts { num ->
|
||||
assert num == 1
|
||||
[destinations.dest1]
|
||||
}
|
||||
connectionManagerMock.ignore.isConnected { dest ->
|
||||
assert dest == destinations.dest1
|
||||
false
|
||||
}
|
||||
i2pConnectorMock.demand.connect { dest ->
|
||||
PipedOutputStream os = new PipedOutputStream()
|
||||
inputStream = new DataInputStream(new PipedInputStream(os))
|
||||
PipedInputStream is = new PipedInputStream()
|
||||
outputStream = new DataOutputStream(new PipedOutputStream(is))
|
||||
new Endpoint(dest, is, os, null)
|
||||
}
|
||||
@Test
|
||||
void testConnectionSucceedsLeaf() {
|
||||
settings = new MuWireSettings() {
|
||||
boolean isLeaf() {true}
|
||||
}
|
||||
connectionManagerMock.ignore.needsConnections {
|
||||
true
|
||||
}
|
||||
hostCacheMock.ignore.getHosts { num ->
|
||||
assert num == 1
|
||||
[destinations.dest1]
|
||||
}
|
||||
connectionManagerMock.ignore.isConnected { dest ->
|
||||
assert dest == destinations.dest1
|
||||
false
|
||||
}
|
||||
i2pConnectorMock.demand.connect { dest ->
|
||||
PipedOutputStream os = new PipedOutputStream()
|
||||
inputStream = new DataInputStream(new PipedInputStream(os))
|
||||
PipedInputStream is = new PipedInputStream()
|
||||
outputStream = new DataOutputStream(new PipedOutputStream(is))
|
||||
new Endpoint(dest, is, os, null)
|
||||
}
|
||||
|
||||
initMocks()
|
||||
initMocks()
|
||||
|
||||
byte [] header = new byte[11]
|
||||
inputStream.readFully(header)
|
||||
assert header == "MuWire leaf".bytes
|
||||
byte [] header = new byte[11]
|
||||
inputStream.readFully(header)
|
||||
assert header == "MuWire leaf".bytes
|
||||
|
||||
outputStream.write("OK".bytes)
|
||||
outputStream.flush()
|
||||
outputStream.write("OK".bytes)
|
||||
outputStream.flush()
|
||||
|
||||
Thread.sleep(100)
|
||||
Thread.sleep(100)
|
||||
|
||||
assert connectionEvents.size() == 1
|
||||
def event = connectionEvents[0]
|
||||
assert event.endpoint.destination == destinations.dest1
|
||||
assert event.incoming == false
|
||||
assert event.status == ConnectionAttemptStatus.SUCCESSFUL
|
||||
}
|
||||
assert connectionEvents.size() == 1
|
||||
def event = connectionEvents[0]
|
||||
assert event.endpoint.destination == destinations.dest1
|
||||
assert event.incoming == false
|
||||
assert event.status == ConnectionAttemptStatus.SUCCESSFUL
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConnectionRejected() {
|
||||
settings = new MuWireSettings() {
|
||||
boolean isLeaf() {false}
|
||||
}
|
||||
connectionManagerMock.ignore.needsConnections {
|
||||
true
|
||||
}
|
||||
hostCacheMock.ignore.getHosts { num ->
|
||||
assert num == 1
|
||||
[destinations.dest1]
|
||||
}
|
||||
connectionManagerMock.ignore.isConnected { dest ->
|
||||
assert dest == destinations.dest1
|
||||
false
|
||||
}
|
||||
i2pConnectorMock.demand.connect { dest ->
|
||||
PipedOutputStream os = new PipedOutputStream()
|
||||
inputStream = new DataInputStream(new PipedInputStream(os))
|
||||
PipedInputStream is = new PipedInputStream()
|
||||
outputStream = new DataOutputStream(new PipedOutputStream(is))
|
||||
new Endpoint(dest, is, os, null)
|
||||
}
|
||||
@Test
|
||||
void testConnectionRejected() {
|
||||
settings = new MuWireSettings() {
|
||||
boolean isLeaf() {false}
|
||||
}
|
||||
connectionManagerMock.ignore.needsConnections {
|
||||
true
|
||||
}
|
||||
hostCacheMock.ignore.getHosts { num ->
|
||||
assert num == 1
|
||||
[destinations.dest1]
|
||||
}
|
||||
connectionManagerMock.ignore.isConnected { dest ->
|
||||
assert dest == destinations.dest1
|
||||
false
|
||||
}
|
||||
i2pConnectorMock.demand.connect { dest ->
|
||||
PipedOutputStream os = new PipedOutputStream()
|
||||
inputStream = new DataInputStream(new PipedInputStream(os))
|
||||
PipedInputStream is = new PipedInputStream()
|
||||
outputStream = new DataOutputStream(new PipedOutputStream(is))
|
||||
new Endpoint(dest, is, os, null)
|
||||
}
|
||||
|
||||
initMocks()
|
||||
initMocks()
|
||||
|
||||
byte [] header = new byte[11]
|
||||
inputStream.readFully(header)
|
||||
assert header == "MuWire peer".bytes
|
||||
byte [] header = new byte[11]
|
||||
inputStream.readFully(header)
|
||||
assert header == "MuWire peer".bytes
|
||||
|
||||
outputStream.write("REJECT".bytes)
|
||||
outputStream.flush()
|
||||
outputStream.write("REJECT".bytes)
|
||||
outputStream.flush()
|
||||
|
||||
Thread.sleep(100)
|
||||
Thread.sleep(100)
|
||||
|
||||
assert connectionEvents.size() == 1
|
||||
def event = connectionEvents[0]
|
||||
assert event.endpoint.destination == destinations.dest1
|
||||
assert event.incoming == false
|
||||
assert event.status == ConnectionAttemptStatus.REJECTED
|
||||
assert discoveredEvents.isEmpty()
|
||||
}
|
||||
assert connectionEvents.size() == 1
|
||||
def event = connectionEvents[0]
|
||||
assert event.endpoint.destination == destinations.dest1
|
||||
assert event.incoming == false
|
||||
assert event.status == ConnectionAttemptStatus.REJECTED
|
||||
assert discoveredEvents.isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConnectionRejectedSuggestions() {
|
||||
settings = new MuWireSettings() {
|
||||
boolean isLeaf() {false}
|
||||
}
|
||||
connectionManagerMock.ignore.needsConnections {
|
||||
true
|
||||
}
|
||||
hostCacheMock.ignore.getHosts { num ->
|
||||
assert num == 1
|
||||
[destinations.dest1]
|
||||
}
|
||||
connectionManagerMock.ignore.isConnected { dest ->
|
||||
assert dest == destinations.dest1
|
||||
false
|
||||
}
|
||||
i2pConnectorMock.demand.connect { dest ->
|
||||
PipedOutputStream os = new PipedOutputStream()
|
||||
inputStream = new DataInputStream(new PipedInputStream(os))
|
||||
PipedInputStream is = new PipedInputStream()
|
||||
outputStream = new DataOutputStream(new PipedOutputStream(is))
|
||||
new Endpoint(dest, is, os, null)
|
||||
}
|
||||
@Test
|
||||
void testConnectionRejectedSuggestions() {
|
||||
settings = new MuWireSettings() {
|
||||
boolean isLeaf() {false}
|
||||
}
|
||||
connectionManagerMock.ignore.needsConnections {
|
||||
true
|
||||
}
|
||||
hostCacheMock.ignore.getHosts { num ->
|
||||
assert num == 1
|
||||
[destinations.dest1]
|
||||
}
|
||||
connectionManagerMock.ignore.isConnected { dest ->
|
||||
assert dest == destinations.dest1
|
||||
false
|
||||
}
|
||||
i2pConnectorMock.demand.connect { dest ->
|
||||
PipedOutputStream os = new PipedOutputStream()
|
||||
inputStream = new DataInputStream(new PipedInputStream(os))
|
||||
PipedInputStream is = new PipedInputStream()
|
||||
outputStream = new DataOutputStream(new PipedOutputStream(is))
|
||||
new Endpoint(dest, is, os, null)
|
||||
}
|
||||
|
||||
initMocks()
|
||||
initMocks()
|
||||
|
||||
byte [] header = new byte[11]
|
||||
inputStream.readFully(header)
|
||||
assert header == "MuWire peer".bytes
|
||||
byte [] header = new byte[11]
|
||||
inputStream.readFully(header)
|
||||
assert header == "MuWire peer".bytes
|
||||
|
||||
outputStream.write("REJECT".bytes)
|
||||
outputStream.flush()
|
||||
outputStream.write("REJECT".bytes)
|
||||
outputStream.flush()
|
||||
|
||||
def json = [:]
|
||||
json.tryHosts = [destinations.dest2.toBase64()]
|
||||
json = JsonOutput.toJson(json)
|
||||
outputStream.writeShort(json.bytes.length)
|
||||
outputStream.write(json.bytes)
|
||||
outputStream.flush()
|
||||
Thread.sleep(100)
|
||||
def json = [:]
|
||||
json.tryHosts = [destinations.dest2.toBase64()]
|
||||
json = JsonOutput.toJson(json)
|
||||
outputStream.writeShort(json.bytes.length)
|
||||
outputStream.write(json.bytes)
|
||||
outputStream.flush()
|
||||
Thread.sleep(100)
|
||||
|
||||
assert connectionEvents.size() == 1
|
||||
def event = connectionEvents[0]
|
||||
assert event.endpoint.destination == destinations.dest1
|
||||
assert event.incoming == false
|
||||
assert event.status == ConnectionAttemptStatus.REJECTED
|
||||
assert connectionEvents.size() == 1
|
||||
def event = connectionEvents[0]
|
||||
assert event.endpoint.destination == destinations.dest1
|
||||
assert event.incoming == false
|
||||
assert event.status == ConnectionAttemptStatus.REJECTED
|
||||
|
||||
assert discoveredEvents.size() == 1
|
||||
event = discoveredEvents[0]
|
||||
assert event.destination == destinations.dest2
|
||||
}
|
||||
assert discoveredEvents.size() == 1
|
||||
event = discoveredEvents[0]
|
||||
assert event.destination == destinations.dest2
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,27 @@
|
||||
package com.muwire.core.download
|
||||
|
||||
import static org.junit.Assert.fail
|
||||
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.Personas
|
||||
import com.muwire.core.connection.Endpoint
|
||||
import com.muwire.core.files.FileHasher
|
||||
import static com.muwire.core.util.DataUtil.readTillRN
|
||||
import static com.muwire.core.util.DataUtil.encodeXHave
|
||||
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.util.ConcurrentHashSet
|
||||
|
||||
class DownloadSessionTest {
|
||||
|
||||
private EventBus eventBus
|
||||
private File source, target
|
||||
private InfoHash infoHash
|
||||
private Endpoint endpoint
|
||||
@ -24,6 +34,16 @@ class DownloadSessionTest {
|
||||
private InputStream fromDownloader, fromUploader
|
||||
private OutputStream toDownloader, toUploader
|
||||
|
||||
private volatile boolean performed
|
||||
private Set<Integer> available = new ConcurrentHashSet<>()
|
||||
private volatile IOException thrown
|
||||
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
eventBus = new EventBus()
|
||||
}
|
||||
|
||||
private void initSession(int size, def claimedPieces = []) {
|
||||
Random r = new Random()
|
||||
byte [] content = new byte[size]
|
||||
@ -56,12 +76,20 @@ class DownloadSessionTest {
|
||||
toUploader = new PipedOutputStream(fromDownloader)
|
||||
endpoint = new Endpoint(null, fromUploader, toUploader, null)
|
||||
|
||||
session = new DownloadSession("",pieces, infoHash, endpoint, target, pieceSize, size)
|
||||
downloadThread = new Thread( { session.request() } as Runnable)
|
||||
session = new DownloadSession(eventBus, "",pieces, infoHash, endpoint, target, pieceSize, size, available)
|
||||
downloadThread = new Thread( { perform() } as Runnable)
|
||||
downloadThread.setDaemon(true)
|
||||
downloadThread.start()
|
||||
}
|
||||
|
||||
private void perform() {
|
||||
try {
|
||||
performed = session.request()
|
||||
} catch (IOException e) {
|
||||
thrown = e
|
||||
}
|
||||
}
|
||||
|
||||
@After
|
||||
public void teardown() {
|
||||
source?.delete()
|
||||
@ -76,6 +104,7 @@ class DownloadSessionTest {
|
||||
assert "GET $rootBase64" == readTillRN(fromDownloader)
|
||||
assert "Range: 0-19" == readTillRN(fromDownloader)
|
||||
readTillRN(fromDownloader)
|
||||
readTillRN(fromDownloader)
|
||||
assert "" == readTillRN(fromDownloader)
|
||||
|
||||
toDownloader.write("200 OK\r\n".bytes)
|
||||
@ -87,6 +116,9 @@ class DownloadSessionTest {
|
||||
|
||||
assert pieces.isComplete()
|
||||
assert target.bytes == source.bytes
|
||||
assert performed
|
||||
assert available.isEmpty()
|
||||
assert thrown == null
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -98,6 +130,7 @@ class DownloadSessionTest {
|
||||
assert "GET $rootBase64" == readTillRN(fromDownloader)
|
||||
readTillRN(fromDownloader)
|
||||
readTillRN(fromDownloader)
|
||||
readTillRN(fromDownloader)
|
||||
assert "" == readTillRN(fromDownloader)
|
||||
|
||||
toDownloader.write("200 OK\r\n".bytes)
|
||||
@ -108,6 +141,9 @@ class DownloadSessionTest {
|
||||
Thread.sleep(150)
|
||||
assert pieces.isComplete()
|
||||
assert target.bytes == source.bytes
|
||||
assert performed
|
||||
assert available.isEmpty()
|
||||
assert thrown == null
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -125,6 +161,7 @@ class DownloadSessionTest {
|
||||
assert (start == 0 && end == ((1 << pieceSize) - 1)) ||
|
||||
(start == (1 << pieceSize) && end == (1 << pieceSize))
|
||||
|
||||
readTillRN(fromDownloader)
|
||||
readTillRN(fromDownloader)
|
||||
assert "" == readTillRN(fromDownloader)
|
||||
|
||||
@ -138,14 +175,21 @@ class DownloadSessionTest {
|
||||
Thread.sleep(150)
|
||||
assert !pieces.isComplete()
|
||||
assert 1 == pieces.donePieces()
|
||||
assert performed
|
||||
assert available.isEmpty()
|
||||
assert thrown == null
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore // this needs to be rewritten with stealing in mind
|
||||
public void testSmallFileClaimed() {
|
||||
initSession(20, [0])
|
||||
long now = System.currentTimeMillis()
|
||||
downloadThread.join(100)
|
||||
assert 100 > (System.currentTimeMillis() - now)
|
||||
downloadThread.join(150)
|
||||
assert 100 >= (System.currentTimeMillis() - now)
|
||||
assert !performed
|
||||
assert available.isEmpty()
|
||||
assert thrown == null
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -164,4 +208,128 @@ class DownloadSessionTest {
|
||||
assert pieces.claimed.get(0)
|
||||
assert start == 0 && end == (1 << pieceSize) - 1
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test416NoHave() {
|
||||
initSession(20)
|
||||
readAllHeaders(fromDownloader)
|
||||
|
||||
toDownloader.write("416 don't have it\r\n\r\n".bytes)
|
||||
toDownloader.flush()
|
||||
Thread.sleep(150)
|
||||
assert !performed
|
||||
assert available.isEmpty()
|
||||
assert thrown != null
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test416Have() {
|
||||
initSession(20)
|
||||
readAllHeaders(fromDownloader)
|
||||
|
||||
toDownloader.write("416 don't have it\r\n".bytes)
|
||||
toDownloader.write("X-Have: ${encodeXHave([0], 1)}\r\n\r\n".bytes)
|
||||
toDownloader.flush()
|
||||
|
||||
Thread.sleep(150)
|
||||
assert performed
|
||||
assert available.contains(0)
|
||||
assert thrown == null
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test416Have2Pieces() {
|
||||
int pieceSize = FileHasher.getPieceSize(1)
|
||||
int size = (1 << pieceSize) + 1
|
||||
initSession(size)
|
||||
readAllHeaders(fromDownloader)
|
||||
|
||||
toDownloader.write("416 don't have it\r\n".bytes)
|
||||
toDownloader.write("X-Have: ${encodeXHave([1], 2)}\r\n\r\n".bytes)
|
||||
toDownloader.flush()
|
||||
|
||||
Thread.sleep(150)
|
||||
assert performed
|
||||
assert available.contains(1)
|
||||
assert thrown == null
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test200TwoPieces1Available() {
|
||||
int pieceSize = FileHasher.getPieceSize(1)
|
||||
int size = (1 << pieceSize) * 9 + 1
|
||||
initSession(size)
|
||||
|
||||
Set<String> headers = readAllHeaders(fromDownloader)
|
||||
def matcher = null
|
||||
headers.each {
|
||||
if (it.startsWith("Range"))
|
||||
matcher = (it =~ /^Range: (\d+)-(\d+)$/)
|
||||
}
|
||||
assert matcher.groupCount() > 0
|
||||
int start = Integer.parseInt(matcher[0][1])
|
||||
int end = Integer.parseInt(matcher[0][2])
|
||||
|
||||
if (start == 0)
|
||||
fail("inconlcusive")
|
||||
|
||||
toDownloader.write("416 don't have it \r\n".bytes)
|
||||
toDownloader.write("X-Have: ${encodeXHave([0],2)}\r\n\r\n".bytes)
|
||||
toDownloader.flush()
|
||||
downloadThread.join()
|
||||
|
||||
assert performed
|
||||
performed = false
|
||||
assert available.contains(0)
|
||||
assert thrown == null
|
||||
|
||||
// request same session
|
||||
downloadThread = new Thread( { perform() } as Runnable)
|
||||
downloadThread.setDaemon(true)
|
||||
downloadThread.start()
|
||||
|
||||
Thread.sleep(150)
|
||||
|
||||
headers = readAllHeaders(fromDownloader)
|
||||
matcher = null
|
||||
headers.each {
|
||||
if (it.startsWith("Range"))
|
||||
matcher = (it =~ /^Range: (\d+)-(\d+)$/)
|
||||
}
|
||||
assert matcher.groupCount() > 0
|
||||
start = Integer.parseInt(matcher[0][1])
|
||||
end = Integer.parseInt(matcher[0][2])
|
||||
assert start == 0
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testXAlt() throws Exception {
|
||||
Personas personas = new Personas()
|
||||
def sources = []
|
||||
def listener = new Object() {
|
||||
public void onSourceDiscoveredEvent(SourceDiscoveredEvent e) {
|
||||
sources << e.source
|
||||
}
|
||||
}
|
||||
eventBus.register(SourceDiscoveredEvent.class, listener)
|
||||
|
||||
initSession(20)
|
||||
readAllHeaders(fromDownloader)
|
||||
toDownloader.write("416 don't have it\r\n".bytes)
|
||||
toDownloader.write("X-Alt: ${personas.persona1.toBase64()},${personas.persona2.toBase64()}\r\n\r\n".bytes)
|
||||
toDownloader.flush()
|
||||
|
||||
Thread.sleep(150)
|
||||
assert sources.contains(personas.persona1)
|
||||
assert sources.contains(personas.persona2)
|
||||
assert 2 == sources.size()
|
||||
}
|
||||
|
||||
private static Set<String> readAllHeaders(InputStream is) {
|
||||
Set<String> rv = new HashSet<>()
|
||||
String header
|
||||
while((header = readTillRN(is)) != "")
|
||||
rv.add(header)
|
||||
rv
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user