Compare commits
182 Commits
muwire-0.6
...
muwire-0.6
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ec41985d31 | ||
![]() |
5daad35ee2 | ||
![]() |
8df9f63bc7 | ||
![]() |
367a43825f | ||
![]() |
7b34b0cffc | ||
![]() |
bb6692c38e | ||
![]() |
f1a2b103a8 | ||
![]() |
c1324c92ba | ||
![]() |
179c3438cd | ||
![]() |
7fa6812ee9 | ||
![]() |
a1c714b46e | ||
![]() |
4f7cf4fbfc | ||
![]() |
2d3e843d64 | ||
![]() |
2e36812740 | ||
![]() |
61340f346a | ||
![]() |
992daa1e45 | ||
![]() |
3b825263a7 | ||
![]() |
e1bf6c0821 | ||
![]() |
a6eca11479 | ||
![]() |
11aa6dda70 | ||
![]() |
3116e20c7c | ||
![]() |
58a92e7442 | ||
![]() |
d18cdb15cd | ||
![]() |
ed02b718d9 | ||
![]() |
564db3473c | ||
![]() |
6d6063829a | ||
![]() |
ecaec1df3b | ||
![]() |
8b99f83db8 | ||
![]() |
33b159477a | ||
![]() |
91d8175cc5 | ||
![]() |
b4c6c77167 | ||
![]() |
fb59d1ca0c | ||
![]() |
3de4c65d2f | ||
![]() |
91ea2c0184 | ||
![]() |
4a81a3539e | ||
fcfb506787 | |||
![]() |
44dc7b808f | ||
![]() |
339f4aaa3e | ||
![]() |
bf06c3b15f | ||
![]() |
b5e41d72b8 | ||
![]() |
2fe9309519 | ||
![]() |
2410ed7199 | ||
![]() |
9167c9edf7 | ||
![]() |
028a8d5044 | ||
![]() |
356d7fe2ff | ||
![]() |
9da7a90653 | ||
![]() |
2001419f1a | ||
![]() |
eec9bab081 | ||
![]() |
0a66267264 | ||
![]() |
ad698cf1b9 | ||
![]() |
fd9866c519 | ||
![]() |
83bea0c823 | ||
![]() |
71789d96d2 | ||
![]() |
7860aa2b1c | ||
![]() |
301c2ec0e2 | ||
![]() |
c306864781 | ||
![]() |
acee9a5805 | ||
![]() |
d34c4e1990 | ||
![]() |
7be3821e53 | ||
![]() |
872e932629 | ||
![]() |
84c7da1fe0 | ||
![]() |
4aed958319 | ||
![]() |
5fc0283da7 | ||
![]() |
c4d908f571 | ||
![]() |
4d5497c12f | ||
![]() |
1d22abfa88 | ||
![]() |
7a7ebc9690 | ||
![]() |
16d3a109ca | ||
![]() |
7864eebb24 | ||
![]() |
9f7aaec991 | ||
![]() |
1c214ad68a | ||
![]() |
3436af75bf | ||
![]() |
9b6a2fd952 | ||
![]() |
85ad3109f9 | ||
![]() |
293ff76ae9 | ||
![]() |
acb70f72d6 | ||
![]() |
62bb4f9e5f | ||
![]() |
03d6fb15f2 | ||
![]() |
699f3ce1b6 | ||
![]() |
7f9c8bddb6 | ||
![]() |
d111983d68 | ||
![]() |
50148e5603 | ||
![]() |
1054fe0935 | ||
![]() |
2de2badb0b | ||
![]() |
424922f2e3 | ||
![]() |
adce4b1574 | ||
![]() |
355535e660 | ||
![]() |
09db68182c | ||
![]() |
1e67139e74 | ||
![]() |
9837e1e3d7 | ||
![]() |
2c52486476 | ||
![]() |
a88dc17064 | ||
![]() |
862967bf8e | ||
![]() |
9f1f718870 | ||
![]() |
2fd0a3833f | ||
![]() |
435170cb1b | ||
![]() |
1c5fec7e9a | ||
![]() |
e2a0a37abf | ||
![]() |
a4bee73b8a | ||
![]() |
056e5800c2 | ||
![]() |
6e0d51c221 | ||
![]() |
496e2e7f91 | ||
![]() |
a560b14d91 | ||
![]() |
faad6b6b0e | ||
![]() |
dfc62b943f | ||
![]() |
244ce43794 | ||
![]() |
f0c8c11094 | ||
![]() |
11e320ef53 | ||
![]() |
aae88e80ee | ||
![]() |
bbf97311d1 | ||
![]() |
23b6995bf2 | ||
![]() |
518bdc44e6 | ||
![]() |
5368dbe181 | ||
![]() |
e216678d9a | ||
![]() |
4582cfa0b5 | ||
![]() |
5ea64ecb90 | ||
![]() |
bd9315954a | ||
![]() |
83bdf76c08 | ||
![]() |
a2ed308cd0 | ||
![]() |
4020df0a77 | ||
![]() |
6f4b4a2c2d | ||
![]() |
83cd5e57a2 | ||
![]() |
bb69535874 | ||
![]() |
b7033e3277 | ||
![]() |
4a9cea7d2e | ||
![]() |
2aea965d72 | ||
![]() |
9a6a1c8371 | ||
![]() |
2042bfccb7 | ||
![]() |
0d4b0df19d | ||
![]() |
f363296ed1 | ||
![]() |
8b33a5a284 | ||
![]() |
7e70dbda86 | ||
![]() |
c23db1293f | ||
![]() |
54f4874ad6 | ||
![]() |
886effa3b6 | ||
![]() |
64d8b98ee2 | ||
![]() |
2f2f620ae5 | ||
![]() |
9a74cc5026 | ||
![]() |
e3c5fe291d | ||
![]() |
c77b848d44 | ||
![]() |
cf5b5b164d | ||
![]() |
3a340e40c8 | ||
![]() |
e9eafe9380 | ||
![]() |
270a8519b4 | ||
![]() |
f8bbeb8ac0 | ||
![]() |
2a4db868aa | ||
![]() |
59219da1a2 | ||
![]() |
a5fb824f71 | ||
![]() |
68bc0bbf30 | ||
![]() |
c6c1ac1d93 | ||
![]() |
9646eadcb1 | ||
![]() |
db91c9171d | ||
![]() |
e542a50260 | ||
![]() |
a9539c5999 | ||
![]() |
d93dbbeb8b | ||
![]() |
45659f0dca | ||
![]() |
31a607ed7d | ||
![]() |
7a6538beff | ||
![]() |
509b5c3b99 | ||
![]() |
fbb710cfc8 | ||
![]() |
244015465a | ||
![]() |
7285c12b97 | ||
![]() |
aac259c0fe | ||
![]() |
e3f58f8f5a | ||
![]() |
045859fe04 | ||
![]() |
3a8c66e857 | ||
![]() |
773513b257 | ||
![]() |
83fe2e9b75 | ||
![]() |
455b0ea48e | ||
![]() |
f4c96db841 | ||
![]() |
fca8870283 | ||
![]() |
3efb04d7bb | ||
![]() |
62ce8ffa46 | ||
![]() |
05b70a4573 | ||
![]() |
b339784826 | ||
![]() |
488f2964ee | ||
![]() |
369779ab6a | ||
![]() |
f5fe3da09d | ||
![]() |
392deee34c | ||
![]() |
7183f15c5c | ||
![]() |
ca33535630 | ||
![]() |
54abf82a91 |
4
.gitignore
vendored
@@ -2,7 +2,7 @@
|
||||
**/.settings
|
||||
**/build
|
||||
.gradle
|
||||
.project
|
||||
.classpath
|
||||
**/.project
|
||||
**/.classpath
|
||||
**/*.rej
|
||||
**/*.orig
|
||||
|
10
README.md
@@ -1,3 +1,5 @@
|
||||
The GitHub repo is mirrored from the in-I2P GitLab repo. Please open PRs and issues at http://git.idk.i2p/zlatinb/muwire
|
||||
|
||||
# MuWire - Easy Anonymous File-Sharing
|
||||
|
||||
MuWire is an easy to use file-sharing program which offers anonymity using [I2P technology](http://geti2p.net). It works on any platform Java works on, including Windows,MacOS,Linux.
|
||||
@@ -49,6 +51,12 @@ MuWire is available as a Docker image. For more information see the [Docker] pa
|
||||
## Translations
|
||||
If you want to help translate MuWire, instructions are on the wiki https://github.com/zlatinb/muwire/wiki/Translate
|
||||
|
||||
## Related Projects
|
||||
### MuWire Tracker Daemon
|
||||
The MuWire Tracker Daemon (or mwtrackerd for short) is a project to bring functionality similar to BitTorrent tracking to MuWire. For more info see the [Tracker] page.
|
||||
### MuCats
|
||||
MuCats is a project to create a website for hosting hashes of files shared on the MuWire network. For more info see the [MuCats] project.
|
||||
|
||||
## GPG Fingerprint
|
||||
|
||||
```
|
||||
@@ -67,3 +75,5 @@ You can find the full key at https://keybase.io/zlatinb
|
||||
[Plugin]: https://github.com/zlatinb/muwire/wiki/Plugin
|
||||
[Docker]: https://github.com/zlatinb/muwire/wiki/Docker
|
||||
[jlesage/docker-baseimage-gui]: https://github.com/jlesage/docker-baseimage-gui
|
||||
[Tracker]: https://github.com/zlatinb/muwire/wiki/Tracker-Daemon
|
||||
[MuCats]: https://github.com/zlatinb/mucats
|
||||
|
5
TODO.md
@@ -19,7 +19,8 @@ This helps with scalability
|
||||
* Enum i18n
|
||||
* Ability to share trust list only with trusted users
|
||||
* Confidential files visible only to certain users
|
||||
* Public Feed feature
|
||||
* Advertise file feed and browseability in upload headers
|
||||
* Manual polling / shared folder re-scan (because polling NAS doesn't work)
|
||||
|
||||
### Chat
|
||||
* echo "unknown/innappropriate command" in the console
|
||||
@@ -32,6 +33,8 @@ This helps with scalability
|
||||
### Swing GUI
|
||||
* I2P Status panel - display message when connected to external router
|
||||
* Search box - left identation
|
||||
* Ability to disable switching of tabs on actions
|
||||
* Ability to trust/browse/subscribe from uploads tab
|
||||
|
||||
### Web UI/Plugin
|
||||
* HTML 5 media players
|
||||
|
13
build.gradle
@@ -9,6 +9,19 @@ subprojects {
|
||||
|
||||
compileGroovy {
|
||||
groovyOptions.optimizationOptions.indy = true
|
||||
sourceCompatibility = project.sourceCompatibility
|
||||
targetCompatibility = project.targetCompatibility
|
||||
options.compilerArgs += project.compilerArgs
|
||||
options.deprecation = true
|
||||
options.encoding = 'UTF-8'
|
||||
}
|
||||
|
||||
compileJava {
|
||||
sourceCompatibility = project.sourceCompatibility
|
||||
targetCompatibility = project.targetCompatibility
|
||||
options.compilerArgs += project.compilerArgs
|
||||
options.deprecation = true
|
||||
options.encoding = 'UTF-8'
|
||||
}
|
||||
|
||||
repositories {
|
||||
|
@@ -32,7 +32,7 @@ import com.muwire.core.UILoadedEvent
|
||||
import com.muwire.core.files.AllFilesLoadedEvent
|
||||
|
||||
class CliLanterna {
|
||||
private static final String MW_VERSION = "0.6.11"
|
||||
private static final String MW_VERSION = "0.6.15"
|
||||
|
||||
private static volatile Core core
|
||||
|
||||
|
@@ -28,7 +28,6 @@ class FilesModel {
|
||||
core.eventBus.register(FileLoadedEvent.class, this)
|
||||
core.eventBus.register(FileUnsharedEvent.class, this)
|
||||
core.eventBus.register(FileHashedEvent.class, this)
|
||||
core.eventBus.register(AllFilesLoadedEvent.class, this)
|
||||
|
||||
Runnable refreshModel = {refreshModel()}
|
||||
Timer timer = new Timer(true)
|
||||
@@ -38,15 +37,6 @@ class FilesModel {
|
||||
|
||||
}
|
||||
|
||||
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
|
||||
def eventBus = core.eventBus
|
||||
guiThread.invokeLater {
|
||||
core.muOptions.watchedDirectories.each {
|
||||
eventBus.publish(new FileSharedEvent(file: new File(it)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void onFileLoadedEvent(FileLoadedEvent e) {
|
||||
guiThread.invokeLater {
|
||||
sharedFiles.add(e.loadedFile)
|
||||
|
@@ -1,12 +1,38 @@
|
||||
apply plugin : 'application'
|
||||
mainClassName = 'com.muwire.core.Core'
|
||||
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
|
||||
dependencies {
|
||||
compile "net.i2p:i2p:${i2pVersion}"
|
||||
compile "net.i2p:router:${i2pVersion}"
|
||||
compile "net.i2p.client:mstreaming:${i2pVersion}"
|
||||
compile "net.i2p.client:streaming:${i2pVersion}"
|
||||
|
||||
testCompile 'org.junit.jupiter:junit-jupiter-api:5.4.2'
|
||||
testCompile 'junit:junit:4.12'
|
||||
plugins {
|
||||
id 'java-library'
|
||||
id 'maven-publish'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api "net.i2p:i2p:${i2pVersion}"
|
||||
api "net.i2p:router:${i2pVersion}"
|
||||
implementation "net.i2p.client:mstreaming:${i2pVersion}"
|
||||
implementation "net.i2p.client:streaming:${i2pVersion}"
|
||||
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.4.2'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.codehaus.groovy:groovy-all:2.4.15'
|
||||
}
|
||||
|
||||
|
||||
// this is necessary because applying both groovy and java-library doesn't work well
|
||||
configurations {
|
||||
apiElements.outgoing.variants {
|
||||
classes {
|
||||
artifact file: compileGroovy.destinationDir, builtBy: compileGroovy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// publish core to local maven repo for sister projects
|
||||
publishing {
|
||||
publications {
|
||||
muCore(MavenPublication) {
|
||||
from components.java
|
||||
}
|
||||
}
|
||||
repositories {
|
||||
mavenLocal()
|
||||
}
|
||||
}
|
||||
|
@@ -5,6 +5,8 @@ import com.muwire.core.files.PersisterFolderService
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.logging.Level
|
||||
import java.util.zip.ZipException
|
||||
|
||||
import com.muwire.core.chat.ChatDisconnectionEvent
|
||||
import com.muwire.core.chat.ChatManager
|
||||
@@ -53,7 +55,11 @@ import com.muwire.core.files.HasherService
|
||||
import com.muwire.core.files.PersisterService
|
||||
import com.muwire.core.files.SideCarFileEvent
|
||||
import com.muwire.core.files.UICommentEvent
|
||||
|
||||
import com.muwire.core.files.directories.UISyncDirectoryEvent
|
||||
import com.muwire.core.files.directories.WatchedDirectoryConfigurationEvent
|
||||
import com.muwire.core.files.directories.WatchedDirectoryConvertedEvent
|
||||
import com.muwire.core.files.directories.WatchedDirectoryConverter
|
||||
import com.muwire.core.files.directories.WatchedDirectoryManager
|
||||
import com.muwire.core.files.AllFilesLoadedEvent
|
||||
import com.muwire.core.files.DirectoryUnsharedEvent
|
||||
import com.muwire.core.files.DirectoryWatchedEvent
|
||||
@@ -79,6 +85,7 @@ 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 com.muwire.core.tracker.TrackerResponder
|
||||
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.I2PAppContext
|
||||
@@ -106,19 +113,19 @@ public class Core {
|
||||
final Properties i2pOptions
|
||||
final MuWireSettings muOptions
|
||||
|
||||
private final I2PSession i2pSession;
|
||||
final I2PSession i2pSession;
|
||||
final TrustService trustService
|
||||
final TrustSubscriber trustSubscriber
|
||||
private final PersisterService persisterService
|
||||
private final PersisterFolderService persisterFolderService
|
||||
private final HostCache hostCache
|
||||
private final ConnectionManager connectionManager
|
||||
final HostCache hostCache
|
||||
final ConnectionManager connectionManager
|
||||
private final CacheClient cacheClient
|
||||
private final UpdateClient updateClient
|
||||
private final ConnectionAcceptor connectionAcceptor
|
||||
final ConnectionAcceptor connectionAcceptor
|
||||
private final ConnectionEstablisher connectionEstablisher
|
||||
private final HasherService hasherService
|
||||
private final DownloadManager downloadManager
|
||||
final DownloadManager downloadManager
|
||||
private final DirectoryWatcher directoryWatcher
|
||||
final FileManager fileManager
|
||||
final UploadManager uploadManager
|
||||
@@ -128,6 +135,9 @@ public class Core {
|
||||
final ChatManager chatManager
|
||||
final FeedManager feedManager
|
||||
private final FeedClient feedClient
|
||||
private final WatchedDirectoryConverter watchedDirectoryConverter
|
||||
final WatchedDirectoryManager watchedDirectoryManager
|
||||
private final TrackerResponder trackerResponder
|
||||
|
||||
private final Router router
|
||||
|
||||
@@ -144,22 +154,26 @@ public class Core {
|
||||
// Read defaults
|
||||
def defaultI2PFile = getClass()
|
||||
.getClassLoader().getResource("defaults/i2p.properties");
|
||||
defaultI2PFile.withInputStream { i2pOptions.load(it) }
|
||||
try {
|
||||
defaultI2PFile.withInputStream { i2pOptions.load(it) }
|
||||
} catch (ZipException mystery) {
|
||||
log.log(Level.SEVERE, "couldn't load default i2p properties", mystery)
|
||||
}
|
||||
|
||||
def i2pOptionsFile = new File(home, "i2p.properties")
|
||||
if (i2pOptionsFile.exists()) {
|
||||
i2pOptionsFile.withInputStream { i2pOptions.load(it) }
|
||||
|
||||
if (!i2pOptions.containsKey("inbound.nickname"))
|
||||
i2pOptions["inbound.nickname"] = "MuWire"
|
||||
i2pOptions["inbound.nickname"] = tunnelName
|
||||
if (!i2pOptions.containsKey("outbound.nickname"))
|
||||
i2pOptions["outbound.nickname"] = "MuWire"
|
||||
i2pOptions["outbound.nickname"] = tunnelName
|
||||
}
|
||||
if (!(i2pOptions.hasProperty("i2np.ntcp.port")
|
||||
&& i2pOptions.hasProperty("i2np.udp.port")
|
||||
if (!(i2pOptions.containsKey("i2np.ntcp.port")
|
||||
&& i2pOptions.containsKey("i2np.udp.port")
|
||||
)) {
|
||||
Random r = new Random()
|
||||
int port = r.nextInt(60000) + 4000
|
||||
int port = 9151 + r.nextInt(1 + 30777 - 9151) // this range matches what the i2p router would choose
|
||||
i2pOptions["i2np.ntcp.port"] = String.valueOf(port)
|
||||
i2pOptions["i2np.udp.port"] = String.valueOf(port)
|
||||
i2pOptionsFile.withOutputStream { i2pOptions.store(it, "") }
|
||||
@@ -359,6 +373,9 @@ public class Core {
|
||||
|
||||
log.info("initializing upload manager")
|
||||
uploadManager = new UploadManager(eventBus, fileManager, meshManager, downloadManager, persisterFolderService, props)
|
||||
|
||||
log.info("initializing tracker responder")
|
||||
trackerResponder = new TrackerResponder(i2pSession, props, fileManager, downloadManager, meshManager, trustService, me)
|
||||
|
||||
log.info("initializing connection establisher")
|
||||
connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache)
|
||||
@@ -379,11 +396,6 @@ public class Core {
|
||||
i2pAcceptor, hostCache, trustService, searchManager, uploadManager, fileManager, connectionEstablisher,
|
||||
certificateManager, chatServer)
|
||||
|
||||
log.info("initializing directory watcher")
|
||||
directoryWatcher = new DirectoryWatcher(eventBus, fileManager, home, props)
|
||||
eventBus.register(DirectoryWatchedEvent.class, directoryWatcher)
|
||||
eventBus.register(AllFilesLoadedEvent.class, directoryWatcher)
|
||||
eventBus.register(DirectoryUnsharedEvent.class, directoryWatcher)
|
||||
|
||||
log.info("initializing hasher service")
|
||||
hasherService = new HasherService(new FileHasher(), eventBus, fileManager, props)
|
||||
@@ -405,6 +417,28 @@ public class Core {
|
||||
BrowseManager browseManager = new BrowseManager(i2pConnector, eventBus, me)
|
||||
eventBus.register(UIBrowseEvent.class, browseManager)
|
||||
|
||||
log.info("initializing watched directory converter")
|
||||
watchedDirectoryConverter = new WatchedDirectoryConverter(this)
|
||||
eventBus.register(AllFilesLoadedEvent.class, watchedDirectoryConverter)
|
||||
|
||||
log.info("initializing watched directory manager")
|
||||
watchedDirectoryManager = new WatchedDirectoryManager(home, eventBus, fileManager)
|
||||
eventBus.with {
|
||||
register(WatchedDirectoryConfigurationEvent.class, watchedDirectoryManager)
|
||||
register(WatchedDirectoryConvertedEvent.class, watchedDirectoryManager)
|
||||
register(FileSharedEvent.class, watchedDirectoryManager)
|
||||
register(DirectoryUnsharedEvent.class, watchedDirectoryManager)
|
||||
register(UISyncDirectoryEvent.class, watchedDirectoryManager)
|
||||
}
|
||||
|
||||
log.info("initializing directory watcher")
|
||||
directoryWatcher = new DirectoryWatcher(eventBus, fileManager, home, watchedDirectoryManager)
|
||||
eventBus.with {
|
||||
register(DirectoryWatchedEvent.class, directoryWatcher)
|
||||
register(WatchedDirectoryConvertedEvent.class, directoryWatcher)
|
||||
register(DirectoryUnsharedEvent.class, directoryWatcher)
|
||||
register(WatchedDirectoryConfigurationEvent.class, directoryWatcher)
|
||||
}
|
||||
}
|
||||
|
||||
public void startServices() {
|
||||
@@ -421,6 +455,7 @@ public class Core {
|
||||
updateClient?.start()
|
||||
feedManager.start()
|
||||
feedClient.start()
|
||||
trackerResponder.start()
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
@@ -448,6 +483,8 @@ public class Core {
|
||||
connectionEstablisher.stop()
|
||||
log.info("shutting down directory watcher")
|
||||
directoryWatcher.stop()
|
||||
log.info("shutting down watch directory manager")
|
||||
watchedDirectoryManager.shutdown()
|
||||
log.info("shutting down cache client")
|
||||
cacheClient.stop()
|
||||
log.info("shutting down chat server")
|
||||
@@ -458,6 +495,8 @@ public class Core {
|
||||
feedManager.stop()
|
||||
log.info("shutting down feed client")
|
||||
feedClient.stop()
|
||||
log.info("shutting down tracker responder")
|
||||
trackerResponder.stop()
|
||||
log.info("shutting down connection manager")
|
||||
connectionManager.shutdown()
|
||||
log.info("killing i2p session")
|
||||
@@ -505,7 +544,7 @@ public class Core {
|
||||
}
|
||||
}
|
||||
|
||||
Core core = new Core(props, home, "0.6.11")
|
||||
Core core = new Core(props, home, "0.6.15")
|
||||
core.startServices()
|
||||
|
||||
// ... at the end, sleep or execute script
|
||||
|
@@ -31,6 +31,7 @@ class MuWireSettings {
|
||||
boolean shareHiddenFiles
|
||||
boolean searchComments
|
||||
boolean browseFiles
|
||||
boolean allowTracking
|
||||
|
||||
boolean fileFeed
|
||||
boolean advertiseFeed
|
||||
@@ -92,6 +93,7 @@ class MuWireSettings {
|
||||
outBw = Integer.valueOf(props.getProperty("outBw","128"))
|
||||
searchComments = Boolean.valueOf(props.getProperty("searchComments","true"))
|
||||
browseFiles = Boolean.valueOf(props.getProperty("browseFiles","true"))
|
||||
allowTracking = Boolean.valueOf(props.getProperty("allowTracking","true"))
|
||||
|
||||
// feed settings
|
||||
fileFeed = Boolean.valueOf(props.getProperty("fileFeed","true"))
|
||||
@@ -100,7 +102,7 @@ class MuWireSettings {
|
||||
defaultFeedAutoDownload = Boolean.valueOf(props.getProperty("defaultFeedAutoDownload", "false"))
|
||||
defaultFeedItemsToKeep = Integer.valueOf(props.getProperty("defaultFeedItemsToKeep", "1000"))
|
||||
defaultFeedSequential = Boolean.valueOf(props.getProperty("defaultFeedSequential", "false"))
|
||||
defaultFeedUpdateInterval = Integer.valueOf(props.getProperty("defaultFeedUpdateInterval", "60"))
|
||||
defaultFeedUpdateInterval = Integer.valueOf(props.getProperty("defaultFeedUpdateInterval", "60000"))
|
||||
|
||||
speedSmoothSeconds = Integer.valueOf(props.getProperty("speedSmoothSeconds","60"))
|
||||
totalUploadSlots = Integer.valueOf(props.getProperty("totalUploadSlots","-1"))
|
||||
@@ -157,6 +159,7 @@ class MuWireSettings {
|
||||
props.setProperty("outBw", String.valueOf(outBw))
|
||||
props.setProperty("searchComments", String.valueOf(searchComments))
|
||||
props.setProperty("browseFiles", String.valueOf(browseFiles))
|
||||
props.setProperty("allowTracking", String.valueOf(allowTracking))
|
||||
|
||||
// feed settings
|
||||
props.setProperty("fileFeed", String.valueOf(fileFeed))
|
||||
|
@@ -233,7 +233,7 @@ class ChatConnection implements ChatLink {
|
||||
daos.close()
|
||||
byte [] signed = baos.toByteArray()
|
||||
def spk = sender.destination.getSigningPublicKey()
|
||||
def signature = new Signature(Constants.SIG_TYPE, sig)
|
||||
def signature = new Signature(spk.getType(), sig)
|
||||
DSAEngine.getInstance().verifySignature(signature, signed, spk)
|
||||
}
|
||||
|
||||
|
@@ -244,7 +244,7 @@ abstract class Connection implements Closeable {
|
||||
else
|
||||
payload = String.join(" ",search.keywords).getBytes(StandardCharsets.UTF_8)
|
||||
def spk = originator.destination.getSigningPublicKey()
|
||||
def signature = new Signature(Constants.SIG_TYPE, sig)
|
||||
def signature = new Signature(spk.getType(), sig)
|
||||
if (!DSAEngine.getInstance().verifySignature(signature, payload, spk)) {
|
||||
log.info("signature didn't match keywords")
|
||||
return
|
||||
@@ -266,7 +266,7 @@ abstract class Connection implements Closeable {
|
||||
queryTime = search.queryTime
|
||||
byte [] payload = (search.uuid + String.valueOf(queryTime)).getBytes(StandardCharsets.US_ASCII)
|
||||
def spk = originator.destination.getSigningPublicKey()
|
||||
def signature = new Signature(Constants.SIG_TYPE, sig2)
|
||||
def signature = new Signature(spk.getType(), sig2)
|
||||
if (!DSAEngine.getInstance().verifySignature(signature, payload, spk)) {
|
||||
log.info("extended signature didn't match uuid and timestamp")
|
||||
return
|
||||
|
@@ -60,7 +60,7 @@ class ConnectionAcceptor {
|
||||
|
||||
private volatile shutdown
|
||||
|
||||
private volatile int browsed
|
||||
volatile int browsed
|
||||
|
||||
ConnectionAcceptor(EventBus eventBus, UltrapeerConnectionManager manager,
|
||||
MuWireSettings settings, I2PAcceptor acceptor, HostCache hostCache,
|
||||
@@ -574,7 +574,9 @@ class ConnectionAcceptor {
|
||||
|
||||
DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))
|
||||
JsonOutput jsonOutput = new JsonOutput()
|
||||
final long now = System.currentTimeMillis();
|
||||
published.each {
|
||||
it.hit(requestor, now, "Feed Update");
|
||||
int certificates = certificateManager.getByInfoHash(new InfoHash(it.getRoot())).size()
|
||||
def obj = FeedItems.sharedFileToObj(it, certificates)
|
||||
def json = jsonOutput.toJson(obj)
|
||||
|
@@ -242,4 +242,8 @@ public class DownloadManager {
|
||||
downloaders.values().each { it.stop() }
|
||||
Downloader.executorService.shutdownNow()
|
||||
}
|
||||
|
||||
public boolean isDownloading(InfoHash infoHash) {
|
||||
downloaders.containsKey(infoHash)
|
||||
}
|
||||
}
|
||||
|
@@ -183,15 +183,14 @@ class DownloadSession {
|
||||
mapped.position(position)
|
||||
|
||||
byte[] tmp = new byte[0x1 << 13]
|
||||
DataInputStream dis = new DataInputStream(is)
|
||||
while(mapped.hasRemaining()) {
|
||||
if (mapped.remaining() < tmp.length)
|
||||
tmp = new byte[mapped.remaining()]
|
||||
int read = is.read(tmp)
|
||||
if (read == -1)
|
||||
throw new IOException()
|
||||
dis.readFully(tmp)
|
||||
synchronized(this) {
|
||||
mapped.put(tmp, 0, read)
|
||||
dataSinceLastRead.addAndGet(read)
|
||||
mapped.put(tmp)
|
||||
dataSinceLastRead.addAndGet(tmp.length)
|
||||
pieces.markPartial(piece, mapped.position())
|
||||
}
|
||||
}
|
||||
|
@@ -160,7 +160,7 @@ public class Downloader {
|
||||
long dataRead = dataSinceLastRead.getAndSet(0)
|
||||
long now = System.currentTimeMillis()
|
||||
if (now > lastSpeedRead)
|
||||
currSpeed = (int) (dataRead * 1000.0 / (now - lastSpeedRead))
|
||||
currSpeed = (int) (dataRead * 1000.0d / (now - lastSpeedRead))
|
||||
lastSpeedRead = now
|
||||
}
|
||||
|
||||
|
@@ -2,10 +2,11 @@ package com.muwire.core.download
|
||||
|
||||
class Pieces {
|
||||
private final BitSet done, claimed
|
||||
private final int nPieces
|
||||
final int nPieces
|
||||
private final float ratio
|
||||
private final Random random = new Random()
|
||||
private final Map<Integer,Integer> partials = new HashMap<>()
|
||||
private int cachedDone;
|
||||
|
||||
Pieces(int nPieces) {
|
||||
this(nPieces, 1.0f)
|
||||
@@ -78,6 +79,7 @@ class Pieces {
|
||||
if (piece >= nPieces)
|
||||
throw new IllegalArgumentException("invalid piece marked as downloaded? $piece/$nPieces")
|
||||
done.set(piece)
|
||||
cachedDone = done.cardinality();
|
||||
claimed.set(piece)
|
||||
partials.remove(piece)
|
||||
}
|
||||
@@ -91,11 +93,11 @@ class Pieces {
|
||||
}
|
||||
|
||||
synchronized boolean isComplete() {
|
||||
done.cardinality() == nPieces
|
||||
cachedDone == nPieces
|
||||
}
|
||||
|
||||
synchronized int donePieces() {
|
||||
done.cardinality()
|
||||
cachedDone
|
||||
}
|
||||
|
||||
synchronized boolean isDownloaded(int piece) {
|
||||
@@ -104,6 +106,7 @@ class Pieces {
|
||||
|
||||
synchronized void clearAll() {
|
||||
done.clear()
|
||||
cachedDone = 0
|
||||
claimed.clear()
|
||||
partials.clear()
|
||||
}
|
||||
|
@@ -105,7 +105,7 @@ class Certificate {
|
||||
|
||||
byte [] payload = baos.toByteArray()
|
||||
SigningPublicKey spk = issuer.destination.getSigningPublicKey()
|
||||
Signature signature = new Signature(Constants.SIG_TYPE, sig)
|
||||
Signature signature = new Signature(spk.getType(), sig)
|
||||
DSAEngine.getInstance().verifySignature(signature, payload, spk)
|
||||
}
|
||||
|
||||
|
@@ -121,8 +121,13 @@ abstract class BasePersisterService extends Service{
|
||||
if (json.searchers != null) {
|
||||
json.searchers.each {
|
||||
Persona searcher = null
|
||||
if (it.searcher != null)
|
||||
searcher = new Persona(new ByteArrayInputStream(Base64.decode(it.searcher)))
|
||||
if (it.searcher != null) {
|
||||
try {
|
||||
searcher = new Persona(new ByteArrayInputStream(Base64.decode(it.searcher)))
|
||||
} catch (Exception ignore) {
|
||||
return
|
||||
}
|
||||
}
|
||||
long timestamp = it.timestamp
|
||||
String query = it.query
|
||||
sf.hit(searcher, timestamp, query)
|
||||
|
@@ -4,8 +4,9 @@ import com.muwire.core.Event
|
||||
|
||||
class DirectoryUnsharedEvent extends Event {
|
||||
File directory
|
||||
boolean deleted
|
||||
|
||||
public String toString() {
|
||||
super.toString() + " unshared directory "+ directory.toString()
|
||||
super.toString() + " unshared directory "+ directory.toString() + " deleted $deleted"
|
||||
}
|
||||
}
|
||||
|
@@ -15,6 +15,9 @@ import java.util.concurrent.ConcurrentHashMap
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.files.directories.WatchedDirectoryConfigurationEvent
|
||||
import com.muwire.core.files.directories.WatchedDirectoryConvertedEvent
|
||||
import com.muwire.core.files.directories.WatchedDirectoryManager
|
||||
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.util.SystemVersion
|
||||
@@ -33,27 +36,27 @@ class DirectoryWatcher {
|
||||
}
|
||||
|
||||
private final File home
|
||||
private final MuWireSettings muOptions
|
||||
private final EventBus eventBus
|
||||
private final FileManager fileManager
|
||||
private final WatchedDirectoryManager watchedDirectoryManager
|
||||
private final Thread watcherThread, publisherThread
|
||||
private final Map<File, Long> waitingFiles = new ConcurrentHashMap<>()
|
||||
private final Map<File, WatchKey> watchedDirectories = new ConcurrentHashMap<>()
|
||||
private WatchService watchService
|
||||
private volatile boolean shutdown
|
||||
|
||||
DirectoryWatcher(EventBus eventBus, FileManager fileManager, File home, MuWireSettings muOptions) {
|
||||
DirectoryWatcher(EventBus eventBus, FileManager fileManager, File home, WatchedDirectoryManager watchedDirectoryManager) {
|
||||
this.home = home
|
||||
this.muOptions = muOptions
|
||||
this.eventBus = eventBus
|
||||
this.fileManager = fileManager
|
||||
this.watchedDirectoryManager = watchedDirectoryManager
|
||||
this.watcherThread = new Thread({watch() } as Runnable, "directory-watcher")
|
||||
watcherThread.setDaemon(true)
|
||||
this.publisherThread = new Thread({publish()} as Runnable, "watched-files-publisher")
|
||||
publisherThread.setDaemon(true)
|
||||
}
|
||||
|
||||
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
|
||||
void onWatchedDirectoryConvertedEvent(WatchedDirectoryConvertedEvent e) {
|
||||
watchService = FileSystems.getDefault().newWatchService()
|
||||
watcherThread.start()
|
||||
publisherThread.start()
|
||||
@@ -71,26 +74,26 @@ class DirectoryWatcher {
|
||||
Path path = canonical.toPath()
|
||||
WatchKey wk = path.register(watchService, kinds)
|
||||
watchedDirectories.put(canonical, wk)
|
||||
|
||||
if (muOptions.watchedDirectories.add(canonical.toString()))
|
||||
saveMuSettings()
|
||||
}
|
||||
|
||||
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent e) {
|
||||
WatchKey wk = watchedDirectories.remove(e.directory)
|
||||
wk?.cancel()
|
||||
|
||||
if (muOptions.watchedDirectories.remove(e.directory.toString()))
|
||||
saveMuSettings()
|
||||
}
|
||||
|
||||
private void saveMuSettings() {
|
||||
File muSettingsFile = new File(home, "MuWire.properties")
|
||||
muSettingsFile.withPrintWriter("UTF-8", {
|
||||
muOptions.write(it)
|
||||
})
|
||||
void onWatchedDirectoryConfigurationEvent(WatchedDirectoryConfigurationEvent e) {
|
||||
if (watchService == null)
|
||||
return // still converting
|
||||
if (!e.autoWatch) {
|
||||
WatchKey wk = watchedDirectories.remove(e.directory)
|
||||
wk?.cancel()
|
||||
} else if (!watchedDirectories.containsKey(e.directory)) {
|
||||
Path path = e.directory.toPath()
|
||||
def wk = path.register(watchService, kinds)
|
||||
watchedDirectories.put(e.directory, wk)
|
||||
} // else it was already watched
|
||||
}
|
||||
|
||||
|
||||
private void watch() {
|
||||
try {
|
||||
while(!shutdown) {
|
||||
@@ -115,7 +118,7 @@ class DirectoryWatcher {
|
||||
File f= join(parent, path)
|
||||
log.fine("created entry $f")
|
||||
if (f.isDirectory())
|
||||
f.toPath().register(watchService, kinds)
|
||||
eventBus.publish(new FileSharedEvent(file : f, fromWatch : true))
|
||||
else
|
||||
waitingFiles.put(f, System.currentTimeMillis())
|
||||
}
|
||||
@@ -133,6 +136,10 @@ class DirectoryWatcher {
|
||||
SharedFile sf = fileManager.fileToSharedFile.get(f)
|
||||
if (sf != null)
|
||||
eventBus.publish(new FileUnsharedEvent(unsharedFile : sf, deleted : true))
|
||||
else if (watchedDirectoryManager.isWatched(f))
|
||||
eventBus.publish(new DirectoryUnsharedEvent(directory : f, deleted : true))
|
||||
else
|
||||
log.fine("Entry was not relevant");
|
||||
}
|
||||
|
||||
private static File join(Path parent, Path path) {
|
||||
@@ -149,7 +156,7 @@ class DirectoryWatcher {
|
||||
waitingFiles.each { file, timestamp ->
|
||||
if (now - timestamp > WAIT_TIME) {
|
||||
log.fine("publishing file $file")
|
||||
eventBus.publish new FileSharedEvent(file : file)
|
||||
eventBus.publish new FileSharedEvent(file : file, fromWatch: true)
|
||||
published << file
|
||||
}
|
||||
}
|
||||
|
@@ -28,6 +28,7 @@ class FileManager {
|
||||
final Map<String, Set<File>> commentToFile = new HashMap<>()
|
||||
final SearchIndex index = new SearchIndex()
|
||||
final FileTree<Void> negativeTree = new FileTree<>()
|
||||
final FileTree<SharedFile> positiveTree = new FileTree<>()
|
||||
final Set<File> sideCarFiles = new HashSet<>()
|
||||
|
||||
FileManager(EventBus eventBus, MuWireSettings settings) {
|
||||
@@ -87,6 +88,7 @@ class FileManager {
|
||||
}
|
||||
existing.add(sf)
|
||||
fileToSharedFile.put(sf.file, sf)
|
||||
positiveTree.add(sf.file, sf);
|
||||
|
||||
negativeTree.remove(sf.file)
|
||||
String parent = sf.getFile().getParent()
|
||||
@@ -130,6 +132,7 @@ class FileManager {
|
||||
}
|
||||
|
||||
fileToSharedFile.remove(sf.file)
|
||||
positiveTree.remove(sf.file)
|
||||
if (!e.deleted && negativeTree.fileToNode.containsKey(sf.file.getParentFile())) {
|
||||
negativeTree.add(sf.file,null)
|
||||
saveNegativeTree()
|
||||
@@ -246,14 +249,26 @@ class FileManager {
|
||||
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent e) {
|
||||
negativeTree.remove(e.directory)
|
||||
saveNegativeTree()
|
||||
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))
|
||||
if (!e.deleted) {
|
||||
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))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
def cb = new DirDeletionCallback()
|
||||
positiveTree.traverse(e.directory, cb)
|
||||
positiveTree.remove(e.directory)
|
||||
cb.unsharedFiles.each {
|
||||
eventBus.publish(new FileUnsharedEvent(unsharedFile : it, deleted: true))
|
||||
}
|
||||
cb.subDirs.each {
|
||||
eventBus.publish(new DirectoryUnsharedEvent(directory : it, deleted : true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,4 +285,25 @@ class FileManager {
|
||||
collect(Collectors.toList())
|
||||
}
|
||||
}
|
||||
|
||||
private static class DirDeletionCallback implements FileTreeCallback<SharedFile> {
|
||||
|
||||
final List<File> subDirs = new ArrayList<>()
|
||||
final List<SharedFile> unsharedFiles = new ArrayList<>()
|
||||
|
||||
@Override
|
||||
public void onDirectoryEnter(File file) {
|
||||
subDirs.add(file)
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDirectoryLeave() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFile(File file, SharedFile value) {
|
||||
unsharedFiles << value
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -5,9 +5,10 @@ import com.muwire.core.Event
|
||||
class FileSharedEvent extends Event {
|
||||
|
||||
File file
|
||||
boolean fromWatch
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString() + " file: "+file.getAbsolutePath()
|
||||
return super.toString() + " file: "+file.getAbsolutePath() + " fromWatch: $fromWatch"
|
||||
}
|
||||
}
|
||||
|
@@ -23,6 +23,7 @@ class FileTree<T> {
|
||||
if (existing == null) {
|
||||
existing = new TreeNode()
|
||||
existing.file = element
|
||||
existing.isFile = element.isFile()
|
||||
existing.parent = current
|
||||
fileToNode.put(element, existing)
|
||||
current.children.add(existing)
|
||||
@@ -64,7 +65,7 @@ class FileTree<T> {
|
||||
private void doTraverse(TreeNode<T> node, FileTreeCallback<T> callback) {
|
||||
boolean leave = false
|
||||
if (node.file != null) {
|
||||
if (node.file.isFile())
|
||||
if (node.isFile)
|
||||
callback.onFile(node.file, node.value)
|
||||
else {
|
||||
leave = true
|
||||
@@ -88,7 +89,7 @@ class FileTree<T> {
|
||||
node = fileToNode.get(parent)
|
||||
|
||||
node.children.each {
|
||||
if (it.file.isFile())
|
||||
if (it.isFile)
|
||||
callback.onFile(it.file, it.value)
|
||||
else
|
||||
callback.onDirectory(it.file)
|
||||
@@ -98,6 +99,7 @@ class FileTree<T> {
|
||||
public static class TreeNode<T> {
|
||||
TreeNode parent
|
||||
File file
|
||||
boolean isFile
|
||||
T value;
|
||||
final Set<TreeNode> children = new HashSet<>()
|
||||
|
||||
|
@@ -53,7 +53,6 @@ class HasherService {
|
||||
|
||||
private void process(File f) {
|
||||
if (f.isDirectory()) {
|
||||
eventBus.publish(new DirectoryWatchedEvent(directory : f))
|
||||
f.listFiles().each {
|
||||
eventBus.publish new FileSharedEvent(file: it)
|
||||
}
|
||||
|
@@ -116,7 +116,7 @@ class PersisterFolderService extends BasePersisterService {
|
||||
try {
|
||||
_load()
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
catch (Exception e) {
|
||||
log.log(Level.WARNING, "couldn't load files", e)
|
||||
}
|
||||
} else {
|
||||
|
@@ -62,7 +62,7 @@ class PersisterService extends BasePersisterService {
|
||||
new File(location.absolutePath + ".bak")
|
||||
)
|
||||
listener.publish(new PersisterDoneEvent())
|
||||
} catch (IllegalArgumentException e) {
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING, "couldn't load files",e)
|
||||
}
|
||||
} else {
|
||||
|
@@ -0,0 +1,7 @@
|
||||
package com.muwire.core.files.directories
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class UISyncDirectoryEvent extends Event {
|
||||
File directory
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
package com.muwire.core.files.directories
|
||||
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import net.i2p.data.Base64
|
||||
|
||||
class WatchedDirectory {
|
||||
final File directory
|
||||
final String encodedName
|
||||
boolean autoWatch
|
||||
int syncInterval
|
||||
long lastSync
|
||||
|
||||
WatchedDirectory(File directory) {
|
||||
this.directory = directory.getCanonicalFile()
|
||||
this.encodedName = Base64.encode(DataUtil.encodei18nString(directory.getAbsolutePath()))
|
||||
}
|
||||
|
||||
def toJson() {
|
||||
def rv = [:]
|
||||
rv.directory = encodedName
|
||||
rv.autoWatch = autoWatch
|
||||
rv.syncInterval = syncInterval
|
||||
rv.lastSync = lastSync
|
||||
rv
|
||||
}
|
||||
|
||||
static WatchedDirectory fromJson(def json) {
|
||||
String dirName = DataUtil.readi18nString(Base64.decode(json.directory))
|
||||
File dir = new File(dirName)
|
||||
def rv = new WatchedDirectory(dir)
|
||||
rv.autoWatch = json.autoWatch
|
||||
rv.syncInterval = json.syncInterval
|
||||
rv.lastSync = json.lastSync
|
||||
rv
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
package com.muwire.core.files.directories
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class WatchedDirectoryConfigurationEvent extends Event {
|
||||
File directory
|
||||
boolean autoWatch
|
||||
int syncInterval
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
package com.muwire.core.files.directories
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
/**
|
||||
* Emitted when converting an old watched directory entry to the
|
||||
* new format.
|
||||
*/
|
||||
class WatchedDirectoryConvertedEvent extends Event {
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
package com.muwire.core.files.directories
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.files.AllFilesLoadedEvent
|
||||
|
||||
/**
|
||||
* converts the setting-based format to new folder-based format.
|
||||
*/
|
||||
class WatchedDirectoryConverter {
|
||||
|
||||
private final Core core
|
||||
|
||||
WatchedDirectoryConverter(Core core) {
|
||||
this.core = core
|
||||
}
|
||||
|
||||
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
|
||||
core.getMuOptions().getWatchedDirectories().each {
|
||||
File directory = new File(it)
|
||||
directory = directory.getCanonicalFile()
|
||||
core.eventBus.publish(new WatchedDirectoryConfigurationEvent(directory : directory, autoWatch: true))
|
||||
}
|
||||
core.getMuOptions().getWatchedDirectories().clear()
|
||||
core.saveMuSettings()
|
||||
core.eventBus.publish(new WatchedDirectoryConvertedEvent())
|
||||
}
|
||||
}
|
@@ -0,0 +1,220 @@
|
||||
package com.muwire.core.files.directories
|
||||
|
||||
import java.nio.file.Files
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ThreadFactory
|
||||
import java.util.stream.Stream
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.files.DirectoryUnsharedEvent
|
||||
import com.muwire.core.files.DirectoryWatchedEvent
|
||||
import com.muwire.core.files.FileListCallback
|
||||
import com.muwire.core.files.FileManager
|
||||
import com.muwire.core.files.FileSharedEvent
|
||||
import com.muwire.core.files.FileUnsharedEvent
|
||||
|
||||
import groovy.json.JsonOutput
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.util.logging.Log
|
||||
|
||||
@Log
|
||||
class WatchedDirectoryManager {
|
||||
|
||||
private final File home
|
||||
private final EventBus eventBus
|
||||
private final FileManager fileManager
|
||||
|
||||
private final Map<File, WatchedDirectory> watchedDirs = new ConcurrentHashMap<>()
|
||||
|
||||
private final ExecutorService diskIO = Executors.newSingleThreadExecutor({r ->
|
||||
Thread t = new Thread(r, "disk-io")
|
||||
t.setDaemon(true)
|
||||
t
|
||||
} as ThreadFactory)
|
||||
|
||||
private final Timer timer = new Timer("directory-timer", true)
|
||||
|
||||
private boolean converting = true
|
||||
|
||||
WatchedDirectoryManager(File home, EventBus eventBus, FileManager fileManager) {
|
||||
this.home = new File(home, "directories")
|
||||
this.home.mkdir()
|
||||
this.eventBus = eventBus
|
||||
this.fileManager = fileManager
|
||||
}
|
||||
|
||||
public boolean isWatched(File f) {
|
||||
watchedDirs.containsKey(f)
|
||||
}
|
||||
|
||||
public Stream<WatchedDirectory> getWatchedDirsStream() {
|
||||
watchedDirs.values().stream()
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
diskIO.shutdown()
|
||||
timer.cancel()
|
||||
}
|
||||
|
||||
void onUISyncDirectoryEvent(UISyncDirectoryEvent e) {
|
||||
def wd = watchedDirs.get(e.directory)
|
||||
if (wd == null) {
|
||||
log.warning("Got a sync event for non-watched dir ${e.directory}")
|
||||
return
|
||||
}
|
||||
diskIO.submit({sync(wd, System.currentTimeMillis())} as Runnable)
|
||||
}
|
||||
|
||||
void onWatchedDirectoryConfigurationEvent(WatchedDirectoryConfigurationEvent e) {
|
||||
if (converting) {
|
||||
def newDir = new WatchedDirectory(e.directory)
|
||||
// conversion is always autowatch really
|
||||
newDir.autoWatch = e.autoWatch
|
||||
persist(newDir)
|
||||
} else {
|
||||
def wd = watchedDirs.get(e.directory)
|
||||
if (wd == null) {
|
||||
log.severe("got a configuration event for a non-watched directory ${e.directory}")
|
||||
return
|
||||
}
|
||||
wd.autoWatch = e.autoWatch
|
||||
wd.syncInterval = e.syncInterval
|
||||
persist(wd)
|
||||
}
|
||||
}
|
||||
|
||||
void onWatchedDirectoryConvertedEvent(WatchedDirectoryConvertedEvent e) {
|
||||
converting = false
|
||||
diskIO.submit({
|
||||
def slurper = new JsonSlurper()
|
||||
Files.walk(home.toPath()).filter({
|
||||
it.getFileName().toString().endsWith(".json")
|
||||
}).
|
||||
forEach {
|
||||
def parsed = slurper.parse(it.toFile())
|
||||
WatchedDirectory wd = WatchedDirectory.fromJson(parsed)
|
||||
watchedDirs.put(wd.directory, wd)
|
||||
}
|
||||
watchedDirs.values().stream().filter({it.autoWatch}).forEach {
|
||||
eventBus.publish(new DirectoryWatchedEvent(directory : it.directory))
|
||||
eventBus.publish(new FileSharedEvent(file : it.directory))
|
||||
}
|
||||
timer.schedule({sync()} as TimerTask, 1000, 1000)
|
||||
} as Runnable)
|
||||
}
|
||||
|
||||
private void persist(WatchedDirectory dir) {
|
||||
diskIO.submit({doPersist(dir)} as Runnable)
|
||||
}
|
||||
|
||||
private void doPersist(WatchedDirectory dir) {
|
||||
def json = JsonOutput.toJson(dir.toJson())
|
||||
def targetFile = new File(home, dir.getEncodedName() + ".json")
|
||||
targetFile.text = json
|
||||
}
|
||||
|
||||
void onFileSharedEvent(FileSharedEvent e) {
|
||||
if (e.file.isFile() || watchedDirs.containsKey(e.file))
|
||||
return
|
||||
|
||||
def wd = new WatchedDirectory(e.file)
|
||||
if (e.fromWatch) {
|
||||
// parent should be already watched, copy settings
|
||||
def parent = watchedDirs.get(e.file.getParentFile())
|
||||
if (parent == null) {
|
||||
log.severe("watching found a directory without a watched parent? ${e.file}")
|
||||
return
|
||||
}
|
||||
wd.autoWatch = parent.autoWatch
|
||||
wd.syncInterval = parent.syncInterval
|
||||
} else
|
||||
wd.autoWatch = true
|
||||
|
||||
watchedDirs.put(wd.directory, wd)
|
||||
persist(wd)
|
||||
if (wd.autoWatch)
|
||||
eventBus.publish(new DirectoryWatchedEvent(directory: wd.directory))
|
||||
}
|
||||
|
||||
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent e) {
|
||||
def wd = watchedDirs.remove(e.directory)
|
||||
if (wd == null) {
|
||||
log.warning("unshared a directory that wasn't watched? ${e.directory}")
|
||||
return
|
||||
}
|
||||
|
||||
File persistFile = new File(home, wd.getEncodedName() + ".json")
|
||||
persistFile.delete()
|
||||
}
|
||||
|
||||
private void sync() {
|
||||
long now = System.currentTimeMillis()
|
||||
watchedDirs.values().stream().
|
||||
filter({!it.autoWatch}).
|
||||
filter({it.syncInterval > 0}).
|
||||
filter({it.lastSync + it.syncInterval * 1000 < now}).
|
||||
forEach({wd -> diskIO.submit({sync(wd, now)} as Runnable )})
|
||||
}
|
||||
|
||||
private void sync(WatchedDirectory wd, long now) {
|
||||
log.fine("syncing ${wd.directory}")
|
||||
wd.lastSync = now
|
||||
doPersist(wd)
|
||||
eventBus.publish(new WatchedDirectorySyncEvent(directory: wd.directory, when: now))
|
||||
|
||||
def cb = new DirSyncCallback()
|
||||
fileManager.positiveTree.list(wd.directory, cb)
|
||||
|
||||
Set<File> filesOnFS = new HashSet<>()
|
||||
Set<File> dirsOnFS = new HashSet<>()
|
||||
wd.directory.listFiles().each {
|
||||
File canonical = it.getCanonicalFile()
|
||||
if (canonical.isFile())
|
||||
filesOnFS.add(canonical)
|
||||
else
|
||||
dirsOnFS.add(canonical)
|
||||
}
|
||||
|
||||
Set<File> addedFiles = new HashSet<>(filesOnFS)
|
||||
addedFiles.removeAll(cb.files)
|
||||
addedFiles.each {
|
||||
eventBus.publish(new FileSharedEvent(file : it, fromWatch : true))
|
||||
}
|
||||
Set<File> addedDirs = new HashSet<>(dirsOnFS)
|
||||
addedDirs.removeAll(cb.dirs)
|
||||
addedDirs.each {
|
||||
eventBus.publish(new FileSharedEvent(file : it, fromWatch : true))
|
||||
}
|
||||
|
||||
Set<File> deletedFiles = new HashSet<>(cb.files)
|
||||
deletedFiles.removeAll(filesOnFS)
|
||||
deletedFiles.each {
|
||||
eventBus.publish(new FileUnsharedEvent(unsharedFile : fileManager.getFileToSharedFile().get(it), deleted : true))
|
||||
}
|
||||
Set<File> deletedDirs = new HashSet<>(cb.dirs)
|
||||
deletedDirs.removeAll(dirsOnFS)
|
||||
deletedDirs.each {
|
||||
eventBus.publish(new DirectoryUnsharedEvent(directory : it, deleted: true))
|
||||
}
|
||||
}
|
||||
|
||||
private static class DirSyncCallback implements FileListCallback<SharedFile> {
|
||||
|
||||
private final Set<File> files = new HashSet<>()
|
||||
private final Set<File> dirs = new HashSet<>()
|
||||
|
||||
@Override
|
||||
public void onFile(File f, SharedFile value) {
|
||||
files.add(f)
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDirectory(File f) {
|
||||
dirs.add(f)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
package com.muwire.core.files.directories
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class WatchedDirectorySyncEvent extends Event {
|
||||
File directory
|
||||
long when
|
||||
}
|
@@ -8,10 +8,8 @@ class CacheServers {
|
||||
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==")
|
||||
// echelon
|
||||
new Destination("2MJTl8gYVPK43iJZJa~-5K1OchgPaPHXpqZmKIiKFvxyy8BlIJzUSrF4mazdta--shFHISfT0PEeI95j1yDyKMpGxatUyjSt3ZnyTfAehQR-H2kYV9FvjHo68uA9X5AaGYHKRYLuWMkihMXygd8ywoLjZtFP0UbKMPggfOZaWmjHF4081XoUXt~7MEAeYSQowndiUx0AH3HxNEiv0N373JJS61OsIXb5ctqVKkwIiX1R0ZxESzpP9Xwp8-T0ou8fsLksygbKyH~3K1CyTHjTS51Ux-U-CjOPH9rtCOjjAaifdyMpK0PxW1fVdoGswFywTz9Q-6DUMsIu5TsPMF0-UO1Wn8vCpVAWbBJAOtKCfBrGzp-E~GCbfCNs5xY19nLobMD5ehjsBdI1lXwGDCQ7kBOwC58uuC3BOoazgrB6IrGskyMTexawtthO9mhuPm91bq4xhNaCYHAe059xg5emnM7jFBVzQgjaZ5lOLn~HqcWofJ7oc0doE6XI6kOo~YncBQAEAAcAAA==")
|
||||
]
|
||||
|
||||
static List<Destination> getCacheServers() {
|
||||
|
@@ -10,7 +10,7 @@ import net.i2p.util.ConcurrentHashSet
|
||||
class Mesh {
|
||||
private final InfoHash infoHash
|
||||
private final Set<Persona> sources = new ConcurrentHashSet<>()
|
||||
private final Pieces pieces
|
||||
final Pieces pieces
|
||||
|
||||
Mesh(InfoHash infoHash, Pieces pieces) {
|
||||
this.infoHash = infoHash
|
||||
|
@@ -0,0 +1,214 @@
|
||||
package com.muwire.core.tracker
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.logging.Level
|
||||
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.DownloadManager
|
||||
import com.muwire.core.download.Pieces
|
||||
import com.muwire.core.files.FileManager
|
||||
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.JsonOutput
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.client.I2PSession
|
||||
import net.i2p.client.I2PSessionMuxedListener
|
||||
import net.i2p.client.SendMessageOptions
|
||||
import net.i2p.client.datagram.I2PDatagramDissector
|
||||
import net.i2p.client.datagram.I2PDatagramMaker
|
||||
import net.i2p.data.Base64
|
||||
|
||||
@Log
|
||||
class TrackerResponder {
|
||||
private final I2PSession i2pSession
|
||||
private final MuWireSettings muSettings
|
||||
private final FileManager fileManager
|
||||
private final DownloadManager downloadManager
|
||||
private final MeshManager meshManager
|
||||
private final TrustService trustService
|
||||
private final Persona me
|
||||
|
||||
private final Map<UUID,Long> uuids = new HashMap<>()
|
||||
private final Timer expireTimer = new Timer("tracker-responder-timer", true)
|
||||
|
||||
private static final long UUID_LIFETIME = 10 * 60 * 1000
|
||||
|
||||
TrackerResponder(I2PSession i2pSession, MuWireSettings muSettings,
|
||||
FileManager fileManager, DownloadManager downloadManager,
|
||||
MeshManager meshManager, TrustService trustService,
|
||||
Persona me) {
|
||||
this.i2pSession = i2pSession
|
||||
this.muSettings = muSettings
|
||||
this.fileManager = fileManager
|
||||
this.downloadManager = downloadManager
|
||||
this.meshManager = meshManager
|
||||
this.trustService = trustService
|
||||
this.me = me
|
||||
}
|
||||
|
||||
void start() {
|
||||
i2pSession.addMuxedSessionListener(new Listener(), I2PSession.PROTO_DATAGRAM, Constants.TRACKER_PORT)
|
||||
expireTimer.schedule({expireUUIDs()} as TimerTask, UUID_LIFETIME, UUID_LIFETIME)
|
||||
}
|
||||
|
||||
void stop() {
|
||||
expireTimer.cancel()
|
||||
}
|
||||
|
||||
private void expireUUIDs() {
|
||||
final long now = System.currentTimeMillis()
|
||||
synchronized(uuids) {
|
||||
for (Iterator<UUID> iter = uuids.keySet().iterator(); iter.hasNext();) {
|
||||
UUID uuid = iter.next();
|
||||
Long time = uuids.get(uuid)
|
||||
if (now - time > UUID_LIFETIME)
|
||||
iter.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void respond(host, json) {
|
||||
log.info("responding to host $host with json $json")
|
||||
|
||||
def message = JsonOutput.toJson(json)
|
||||
def maker = new I2PDatagramMaker(i2pSession)
|
||||
message = maker.makeI2PDatagram(message.bytes)
|
||||
def options = new SendMessageOptions()
|
||||
options.setSendLeaseSet(false)
|
||||
i2pSession.sendMessage(host, message, 0, message.length, I2PSession.PROTO_DATAGRAM, Constants.TRACKER_PORT, Constants.TRACKER_PORT, options)
|
||||
}
|
||||
|
||||
class Listener implements I2PSessionMuxedListener {
|
||||
|
||||
@Override
|
||||
public void messageAvailable(I2PSession session, int msgId, long size) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void messageAvailable(I2PSession session, int msgId, long size, int proto, int fromport, int toport) {
|
||||
if (proto != I2PSession.PROTO_DATAGRAM) {
|
||||
log.warning "Received unexpected protocol $proto"
|
||||
return
|
||||
}
|
||||
|
||||
byte[] payload = session.receiveMessage(msgId)
|
||||
def dissector = new I2PDatagramDissector()
|
||||
try {
|
||||
dissector.loadI2PDatagram(payload)
|
||||
def sender = dissector.getSender()
|
||||
|
||||
log.info("got a tracker datagram from ${sender.toBase32()}")
|
||||
|
||||
// if not trusted, just drop it
|
||||
TrustLevel trustLevel = trustService.getLevel(sender)
|
||||
|
||||
if (trustLevel == TrustLevel.DISTRUSTED ||
|
||||
(trustLevel == TrustLevel.NEUTRAL && !muSettings.allowUntrusted)) {
|
||||
log.info("dropping, untrusted")
|
||||
return
|
||||
}
|
||||
|
||||
payload = dissector.getPayload()
|
||||
def slurper = new JsonSlurper()
|
||||
def json = slurper.parse(payload)
|
||||
|
||||
if (json.type != "TrackerPing") {
|
||||
log.warning("unknown type $json.type")
|
||||
return
|
||||
}
|
||||
|
||||
def response = [:]
|
||||
response.type = "TrackerPong"
|
||||
response.me = me.toBase64()
|
||||
|
||||
if (json.infoHash == null) {
|
||||
log.warning("infoHash missing")
|
||||
return
|
||||
}
|
||||
|
||||
if (json.uuid == null) {
|
||||
log.warning("uuid missing")
|
||||
return
|
||||
}
|
||||
|
||||
UUID uuid = UUID.fromString(json.uuid)
|
||||
synchronized(uuids) {
|
||||
if (uuids.containsKey(uuid)) {
|
||||
log.warning("duplicate uuid $uuid")
|
||||
return
|
||||
}
|
||||
uuids.put(uuid, System.currentTimeMillis())
|
||||
}
|
||||
response.uuid = json.uuid
|
||||
|
||||
if (!muSettings.allowTracking) {
|
||||
response.code = 403
|
||||
respond(sender, response)
|
||||
return
|
||||
}
|
||||
|
||||
if (json.version != 1) {
|
||||
log.warning("unknown version $json.version")
|
||||
response.code = 400
|
||||
response.message = "I only support version 1"
|
||||
respond(sender,response)
|
||||
return
|
||||
}
|
||||
|
||||
byte[] infoHashBytes = Base64.decode(json.infoHash)
|
||||
InfoHash infoHash = new InfoHash(infoHashBytes)
|
||||
|
||||
log.info("servicing request for infoHash ${json.infoHash} with uuid ${json.uuid}")
|
||||
|
||||
if (!(fileManager.isShared(infoHash) || downloadManager.isDownloading(infoHash))) {
|
||||
response.code = 404
|
||||
respond(sender, response)
|
||||
return
|
||||
}
|
||||
|
||||
Mesh mesh = meshManager.get(infoHash)
|
||||
|
||||
if (fileManager.isShared(infoHash))
|
||||
response.code = 200
|
||||
else if (mesh != null) {
|
||||
response.code = 206
|
||||
Pieces pieces = mesh.getPieces()
|
||||
response.xHave = DataUtil.encodeXHave(pieces, pieces.getnPieces())
|
||||
}
|
||||
|
||||
if (mesh != null)
|
||||
response.altlocs = mesh.getRandom(10, me).stream().map({it.toBase64()}).collect(Collectors.toList())
|
||||
|
||||
respond(sender,response)
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING, "invalid datagram", e)
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportAbuse(I2PSession session, int severity) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnected(I2PSession session) {
|
||||
log.severe("session disconnected")
|
||||
}
|
||||
|
||||
@Override
|
||||
public void errorOccurred(I2PSession session, String message, Throwable error) {
|
||||
log.log(Level.SEVERE, message, error)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -2,6 +2,7 @@ package com.muwire.core.update
|
||||
|
||||
import java.util.logging.Level
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.MuWireSettings
|
||||
@@ -63,7 +64,7 @@ class UpdateClient {
|
||||
}
|
||||
|
||||
void start() {
|
||||
session.addMuxedSessionListener(new Listener(), I2PSession.PROTO_DATAGRAM, 2)
|
||||
session.addMuxedSessionListener(new Listener(), I2PSession.PROTO_DATAGRAM, Constants.UPDATE_PORT)
|
||||
timer.schedule({checkUpdate()} as TimerTask, 60000, 60 * 60 * 1000)
|
||||
}
|
||||
|
||||
@@ -108,7 +109,7 @@ class UpdateClient {
|
||||
ping = maker.makeI2PDatagram(ping.bytes)
|
||||
def options = new SendMessageOptions()
|
||||
options.setSendLeaseSet(true)
|
||||
session.sendMessage(UpdateServers.UPDATE_SERVER, ping, 0, ping.length, I2PSession.PROTO_DATAGRAM, 2, 0, options)
|
||||
session.sendMessage(UpdateServers.UPDATE_SERVER, ping, 0, ping.length, I2PSession.PROTO_DATAGRAM, Constants.UPDATE_PORT, 0, options)
|
||||
}
|
||||
|
||||
class Listener implements I2PSessionMuxedListener {
|
||||
|
@@ -106,7 +106,7 @@ class ContentUploader extends Uploader {
|
||||
return done ? 100 : 0
|
||||
int position = mapped.position()
|
||||
int total = request.getRange().end - request.getRange().start
|
||||
(int)(position * 100.0 / total)
|
||||
(int)(position * 100.0d / total)
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -45,7 +45,7 @@ class HashListUploader extends Uploader {
|
||||
|
||||
@Override
|
||||
public synchronized int getProgress() {
|
||||
(int)(mapped.position() * 100.0 / mapped.capacity())
|
||||
(int)(mapped.position() * 100.0d / mapped.capacity())
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -22,7 +22,7 @@ class Request {
|
||||
|
||||
static Request parseContentRequest(InfoHash infoHash, InputStream is) throws IOException {
|
||||
|
||||
Map<String, String> headers = parseHeaders(is)
|
||||
Map<String, String> headers = DataUtil.readAllHeaders(is)
|
||||
|
||||
if (!headers.containsKey("Range"))
|
||||
throw new IOException("Range header not found")
|
||||
@@ -60,7 +60,7 @@ class Request {
|
||||
}
|
||||
|
||||
static Request parseHashListRequest(InfoHash infoHash, InputStream is) throws IOException {
|
||||
Map<String,String> headers = parseHeaders(is)
|
||||
Map<String,String> headers = DataUtil.readAllHeaders(is)
|
||||
Persona downloader = null
|
||||
if (headers.containsKey("X-Persona")) {
|
||||
def encoded = headers["X-Persona"].trim()
|
||||
@@ -69,55 +69,4 @@ class Request {
|
||||
}
|
||||
new HashListRequest(infoHash : infoHash, headers : headers, downloader : downloader)
|
||||
}
|
||||
|
||||
private static Map<String, String> parseHeaders(InputStream is) {
|
||||
Map<String,String> headers = new HashMap<>()
|
||||
byte [] tmp = new byte[Constants.MAX_HEADER_SIZE]
|
||||
while(headers.size() < Constants.MAX_HEADERS) {
|
||||
boolean r = false
|
||||
boolean n = false
|
||||
int idx = 0
|
||||
while (true) {
|
||||
byte read = is.read()
|
||||
if (read == -1)
|
||||
throw new IOException("Stream closed")
|
||||
|
||||
if (!r && read == N)
|
||||
throw new IOException("Received N before R")
|
||||
if (read == R) {
|
||||
if (r)
|
||||
throw new IOException("double R")
|
||||
r = true
|
||||
continue
|
||||
}
|
||||
|
||||
if (r && !n) {
|
||||
if (read != N)
|
||||
throw new IOException("R not followed by N")
|
||||
n = true
|
||||
break
|
||||
}
|
||||
if (idx == 0x1 << 14)
|
||||
throw new IOException("Header too long")
|
||||
tmp[idx++] = read
|
||||
}
|
||||
|
||||
if (idx == 0)
|
||||
break
|
||||
|
||||
String header = new String(tmp, 0, idx, StandardCharsets.US_ASCII)
|
||||
log.fine("Read header $header")
|
||||
|
||||
int keyIdx = header.indexOf(":")
|
||||
if (keyIdx < 1)
|
||||
throw new IOException("Header key not found")
|
||||
if (keyIdx == header.length())
|
||||
throw new IOException("Header value not found")
|
||||
String key = header.substring(0, keyIdx)
|
||||
String value = header.substring(keyIdx + 1)
|
||||
headers.put(key, value)
|
||||
}
|
||||
headers
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -49,7 +49,7 @@ abstract class Uploader {
|
||||
final long now = System.currentTimeMillis()
|
||||
long interval = Math.max(1000, now - lastSpeedRead)
|
||||
lastSpeedRead = now;
|
||||
int currSpeed = (int) (dataSinceLastRead * 1000.0 / interval)
|
||||
int currSpeed = (int) (dataSinceLastRead * 1000.0d / interval)
|
||||
dataSinceLastRead = 0
|
||||
|
||||
// normalize to speedArr.size
|
||||
|
@@ -4,6 +4,8 @@ import net.i2p.crypto.SigType;
|
||||
|
||||
public class Constants {
|
||||
public static final byte PERSONA_VERSION = (byte)1;
|
||||
public static final String INVALID_NICKNAME_CHARS = "'\"();<>=@$%";
|
||||
public static final int MAX_NICKNAME_LENGTH = 30;
|
||||
public static final byte FILE_CERT_VERSION = (byte)2;
|
||||
public static final int CHAT_VERSION = 1;
|
||||
|
||||
@@ -17,5 +19,8 @@ public class Constants {
|
||||
|
||||
public static final int MAX_COMMENT_LENGTH = 0x1 << 15;
|
||||
|
||||
public static final long MAX_QUERY_AGE = 5 * 60 * 1000L;
|
||||
public static final long MAX_QUERY_AGE = 5 * 60 * 1000L;
|
||||
|
||||
public static final int UPDATE_PORT = 2;
|
||||
public static final int TRACKER_PORT = 3;
|
||||
}
|
||||
|
@@ -0,0 +1,25 @@
|
||||
package com.muwire.core;
|
||||
|
||||
public class InvalidNicknameException extends Exception {
|
||||
|
||||
public InvalidNicknameException() {
|
||||
}
|
||||
|
||||
public InvalidNicknameException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public InvalidNicknameException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public InvalidNicknameException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public InvalidNicknameException(String message, Throwable cause, boolean enableSuppression,
|
||||
boolean writableStackTrace) {
|
||||
super(message, cause, enableSuppression, writableStackTrace);
|
||||
}
|
||||
|
||||
}
|
@@ -7,6 +7,8 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import com.muwire.core.util.DataUtil;
|
||||
|
||||
import net.i2p.crypto.DSAEngine;
|
||||
import net.i2p.data.Base64;
|
||||
import net.i2p.data.DataFormatException;
|
||||
@@ -25,12 +27,15 @@ public class Persona {
|
||||
private volatile String base64;
|
||||
private volatile byte[] payload;
|
||||
|
||||
public Persona(InputStream personaStream) throws IOException, DataFormatException, InvalidSignatureException {
|
||||
public Persona(InputStream personaStream) throws IOException, DataFormatException, InvalidSignatureException, InvalidNicknameException {
|
||||
version = (byte) (personaStream.read() & 0xFF);
|
||||
if (version != Constants.PERSONA_VERSION)
|
||||
throw new IOException("Unknown version "+version);
|
||||
|
||||
name = new Name(personaStream);
|
||||
if (!DataUtil.isValidName(name.name))
|
||||
throw new InvalidNicknameException(name.name + " is not a valid nickname");
|
||||
|
||||
destination = Destination.create(personaStream);
|
||||
sig = new byte[SIG_LEN];
|
||||
DataInputStream dis = new DataInputStream(personaStream);
|
||||
@@ -38,7 +43,7 @@ public class Persona {
|
||||
if (!verify(version, name, destination, sig))
|
||||
throw new InvalidSignatureException(getHumanReadableName() + " didn't verify");
|
||||
}
|
||||
|
||||
|
||||
private static boolean verify(byte version, Name name, Destination destination, byte [] sig)
|
||||
throws IOException, DataFormatException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
@@ -47,7 +52,7 @@ public class Persona {
|
||||
destination.writeBytes(baos);
|
||||
byte[] payload = baos.toByteArray();
|
||||
SigningPublicKey spk = destination.getSigningPublicKey();
|
||||
Signature signature = new Signature(Constants.SIG_TYPE, sig);
|
||||
Signature signature = new Signature(spk.getType(), sig);
|
||||
return DSAEngine.getInstance().verifySignature(signature, payload, spk);
|
||||
}
|
||||
|
||||
|
@@ -159,6 +159,18 @@ public class SharedFile {
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
public Persona getSearcher() {
|
||||
return searcher;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public String getQuery() {
|
||||
return query;
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
return Objects.hash(searcher) ^ Objects.hash(timestamp) ^ query.hashCode();
|
||||
}
|
||||
|
@@ -58,9 +58,9 @@ public class DataUtil {
|
||||
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);
|
||||
return ((header[0] & 0x7F) << 16) |
|
||||
((header[1] & 0xFF) << 8) |
|
||||
(header[2] & 0xFF);
|
||||
}
|
||||
|
||||
public static String readi18nString(byte [] encoded) {
|
||||
@@ -174,7 +174,7 @@ public class DataUtil {
|
||||
clean.setAccessible(true);
|
||||
clean.invoke(cleaner.invoke(cb));
|
||||
} else {
|
||||
Class unsafeClass;
|
||||
Class<?> unsafeClass;
|
||||
try {
|
||||
unsafeClass = Class.forName("sun.misc.Unsafe");
|
||||
} catch(Exception ex) {
|
||||
@@ -216,4 +216,13 @@ public class DataUtil {
|
||||
Signature sig = DSAEngine.getInstance().sign(payload, spk);
|
||||
return sig.getData();
|
||||
}
|
||||
|
||||
public static boolean isValidName(String name) {
|
||||
if (name.length() > Constants.MAX_NICKNAME_LENGTH)
|
||||
return false;
|
||||
for (int i = 0; i < Constants.INVALID_NICKNAME_CHARS.length(); i++)
|
||||
if (name.indexOf(Constants.INVALID_NICKNAME_CHARS.charAt(i)) >= 0)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@@ -39,13 +39,13 @@ class FileManagerTest {
|
||||
@Test
|
||||
void testHash1Result() {
|
||||
File f = new File("a b.c")
|
||||
InfoHash ih = InfoHash.fromHashList(new byte[32])
|
||||
SharedFile sf = new SharedFile(f,ih, 0)
|
||||
byte [] root = new byte[32]
|
||||
SharedFile sf = new SharedFile(f,root, 0)
|
||||
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
|
||||
manager.onFileHashedEvent(fhe)
|
||||
|
||||
UUID uuid = UUID.randomUUID()
|
||||
SearchEvent se = new SearchEvent(searchHash: ih.getRoot(), uuid: uuid)
|
||||
SearchEvent se = new SearchEvent(searchHash: root, uuid: uuid)
|
||||
|
||||
manager.onSearchEvent(se)
|
||||
Thread.sleep(20)
|
||||
@@ -58,14 +58,14 @@ class FileManagerTest {
|
||||
|
||||
@Test
|
||||
void testHash2Results() {
|
||||
InfoHash ih = InfoHash.fromHashList(new byte[32])
|
||||
SharedFile sf1 = new SharedFile(new File("a b.c"), ih, 0)
|
||||
SharedFile sf2 = new SharedFile(new File("d e.f"), ih, 0)
|
||||
byte [] root = new byte[32]
|
||||
SharedFile sf1 = new SharedFile(new File("a b.c"), root, 0)
|
||||
SharedFile sf2 = new SharedFile(new File("d e.f"), root, 0)
|
||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf1)
|
||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf2)
|
||||
|
||||
UUID uuid = UUID.randomUUID()
|
||||
SearchEvent se = new SearchEvent(searchHash: ih.getRoot(), uuid: uuid)
|
||||
SearchEvent se = new SearchEvent(searchHash: root, uuid: uuid)
|
||||
|
||||
manager.onSearchEvent(se)
|
||||
Thread.sleep(20)
|
||||
@@ -81,7 +81,7 @@ class FileManagerTest {
|
||||
void testHash0Results() {
|
||||
File f = new File("a b.c")
|
||||
InfoHash ih = InfoHash.fromHashList(new byte[32])
|
||||
SharedFile sf = new SharedFile(f,ih, 0)
|
||||
SharedFile sf = new SharedFile(f,ih.getRoot(), 0)
|
||||
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
|
||||
manager.onFileHashedEvent(fhe)
|
||||
|
||||
@@ -95,7 +95,7 @@ class FileManagerTest {
|
||||
void testKeyword1Result() {
|
||||
File f = new File("a b.c")
|
||||
InfoHash ih = InfoHash.fromHashList(new byte[32])
|
||||
SharedFile sf = new SharedFile(f,ih,0)
|
||||
SharedFile sf = new SharedFile(f,ih.getRoot(),0)
|
||||
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
|
||||
manager.onFileHashedEvent(fhe)
|
||||
|
||||
@@ -113,12 +113,12 @@ class FileManagerTest {
|
||||
void testKeyword2Results() {
|
||||
File f1 = new File("a b.c")
|
||||
InfoHash ih1 = InfoHash.fromHashList(new byte[32])
|
||||
SharedFile sf1 = new SharedFile(f1, ih1, 0)
|
||||
SharedFile sf1 = new SharedFile(f1, ih1.getRoot(), 0)
|
||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf1)
|
||||
|
||||
File f2 = new File("c d.e")
|
||||
InfoHash ih2 = InfoHash.fromHashList(new byte[64])
|
||||
SharedFile sf2 = new SharedFile(f2, ih2, 0)
|
||||
SharedFile sf2 = new SharedFile(f2, ih2.getRoot(), 0)
|
||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf2)
|
||||
|
||||
UUID uuid = UUID.randomUUID()
|
||||
@@ -136,7 +136,7 @@ class FileManagerTest {
|
||||
void testKeyword0Results() {
|
||||
File f = new File("a b.c")
|
||||
InfoHash ih = InfoHash.fromHashList(new byte[32])
|
||||
SharedFile sf = new SharedFile(f,ih,0)
|
||||
SharedFile sf = new SharedFile(f,ih.getRoot(),0)
|
||||
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
|
||||
manager.onFileHashedEvent(fhe)
|
||||
|
||||
@@ -149,8 +149,8 @@ class FileManagerTest {
|
||||
@Test
|
||||
void testRemoveFileExistingHash() {
|
||||
InfoHash ih = InfoHash.fromHashList(new byte[32])
|
||||
SharedFile sf1 = new SharedFile(new File("a b.c"), ih, 0)
|
||||
SharedFile sf2 = new SharedFile(new File("d e.f"), ih, 0)
|
||||
SharedFile sf1 = new SharedFile(new File("a b.c"), ih.getRoot(), 0)
|
||||
SharedFile sf2 = new SharedFile(new File("d e.f"), ih.getRoot(), 0)
|
||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf1)
|
||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf2)
|
||||
|
||||
@@ -167,12 +167,12 @@ class FileManagerTest {
|
||||
void testRemoveFile() {
|
||||
File f1 = new File("a b.c")
|
||||
InfoHash ih1 = InfoHash.fromHashList(new byte[32])
|
||||
SharedFile sf1 = new SharedFile(f1, ih1, 0)
|
||||
SharedFile sf1 = new SharedFile(f1, ih1.getRoot(), 0)
|
||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf1)
|
||||
|
||||
File f2 = new File("c d.e")
|
||||
InfoHash ih2 = InfoHash.fromHashList(new byte[64])
|
||||
SharedFile sf2 = new SharedFile(f2, ih2, 0)
|
||||
SharedFile sf2 = new SharedFile(f2, ih2.getRoot(), 0)
|
||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf2)
|
||||
|
||||
manager.onFileUnsharedEvent new FileUnsharedEvent(deleted : true, unsharedFile: sf2)
|
||||
@@ -198,7 +198,7 @@ class FileManagerTest {
|
||||
comment = Base64.encode(DataUtil.encodei18nString(comment))
|
||||
File f1 = new File("MuWire-0.5.10.AppImage")
|
||||
InfoHash ih1 = InfoHash.fromHashList(new byte[32])
|
||||
SharedFile sf1 = new SharedFile(f1, ih1, 0)
|
||||
SharedFile sf1 = new SharedFile(f1, ih1.getRoot(), 0)
|
||||
sf1.setComment(comment)
|
||||
|
||||
manager.onFileLoadedEvent(new FileLoadedEvent(loadedFile : sf1))
|
||||
@@ -206,7 +206,7 @@ class FileManagerTest {
|
||||
|
||||
File f2 = new File("MuWire-0.6.0.AppImage")
|
||||
InfoHash ih2 = InfoHash.fromHashList(new byte[64])
|
||||
SharedFile sf2 = new SharedFile(f2, ih2, 0)
|
||||
SharedFile sf2 = new SharedFile(f2, ih2.getRoot(), 0)
|
||||
sf2.setComment(comment)
|
||||
|
||||
manager.onFileLoadedEvent(new FileLoadedEvent(loadedFile : sf2))
|
||||
|
@@ -45,7 +45,7 @@ class HasherServiceTest {
|
||||
def hashed = listener.poll()
|
||||
assert hashed instanceof FileHashedEvent
|
||||
assert hashed.sharedFile.file == f.getCanonicalFile()
|
||||
assert hashed.sharedFile.infoHash != null
|
||||
assert hashed.sharedFile.root != null
|
||||
assert listener.isEmpty()
|
||||
}
|
||||
|
||||
|
@@ -85,7 +85,7 @@ class PersisterServiceLoadingTest {
|
||||
def loadedFile = listener.publishedFiles[0]
|
||||
assert loadedFile != null
|
||||
assert loadedFile.file == sharedFile1.getCanonicalFile()
|
||||
assert loadedFile.infoHash == ih1
|
||||
assert loadedFile.root == ih1.getRoot()
|
||||
}
|
||||
|
||||
private static String getSharedFileJsonName(File sharedFile) {
|
||||
@@ -128,7 +128,7 @@ class PersisterServiceLoadingTest {
|
||||
def loadedFile = listener.publishedFiles[0]
|
||||
assert loadedFile != null
|
||||
assert loadedFile.file == sharedFile1.getCanonicalFile()
|
||||
assert loadedFile.infoHash == ih1
|
||||
assert loadedFile.root == ih1.getRoot()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -169,10 +169,10 @@ class PersisterServiceLoadingTest {
|
||||
assert listener.publishedFiles.size() == 2
|
||||
def loadedFile1 = listener.publishedFiles[0]
|
||||
assert loadedFile1.file == sharedFile1.getCanonicalFile()
|
||||
assert loadedFile1.infoHash == ih1
|
||||
assert loadedFile1.root == ih1.getRoot()
|
||||
def loadedFile2 = listener.publishedFiles[1]
|
||||
assert loadedFile2.file == sharedFile2.getCanonicalFile()
|
||||
assert loadedFile2.infoHash == ih2
|
||||
assert loadedFile2.root == ih2.getRoot()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@@ -2,6 +2,7 @@ package com.muwire.core.files
|
||||
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
|
||||
import com.muwire.core.Destinations
|
||||
@@ -16,6 +17,7 @@ import groovy.json.JsonSlurper
|
||||
import net.i2p.data.Base32
|
||||
import net.i2p.data.Base64
|
||||
|
||||
@Ignore
|
||||
class PersisterServiceSavingTest {
|
||||
|
||||
File f
|
||||
|
@@ -1,6 +1,6 @@
|
||||
group = com.muwire
|
||||
version = 0.6.11
|
||||
i2pVersion = 0.9.44
|
||||
version = 0.6.15
|
||||
i2pVersion = 0.9.45
|
||||
groovyVersion = 2.4.15
|
||||
slf4jVersion = 1.7.25
|
||||
spockVersion = 1.1-groovy-2.4
|
||||
@@ -8,8 +8,10 @@ grailsVersion=4.0.0
|
||||
gorm.version=7.0.2.RELEASE
|
||||
griffonEnv=prod
|
||||
|
||||
# javac properties
|
||||
sourceCompatibility=1.8
|
||||
targetCompatibility=1.8
|
||||
compilerArgs=-Xlint:unchecked,cast,path,divzero,empty,path,finally,overrides
|
||||
|
||||
# plugin properties
|
||||
author = zab@mail.i2p
|
||||
|
@@ -131,4 +131,14 @@ mvcGroups {
|
||||
view = 'com.muwire.gui.FeedConfigurationView'
|
||||
controller = 'com.muwire.gui.FeedConfigurationController'
|
||||
}
|
||||
'watched-directory' {
|
||||
model = 'com.muwire.gui.WatchedDirectoryModel'
|
||||
view = 'com.muwire.gui.WatchedDirectoryView'
|
||||
controller = 'com.muwire.gui.WatchedDirectoryController'
|
||||
}
|
||||
'sign' {
|
||||
model = 'com.muwire.gui.SignModel'
|
||||
view = 'com.muwire.gui.SignView'
|
||||
controller = 'com.muwire.gui.SignController'
|
||||
}
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ import griffon.metadata.ArtifactProviderFor
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.files.directories.UISyncDirectoryEvent
|
||||
|
||||
@ArtifactProviderFor(GriffonController)
|
||||
class AdvancedSharingController {
|
||||
@@ -14,4 +15,25 @@ class AdvancedSharingController {
|
||||
AdvancedSharingModel model
|
||||
@MVCMember @Nonnull
|
||||
AdvancedSharingView view
|
||||
|
||||
@ControllerAction
|
||||
void configure() {
|
||||
def wd = view.selectedWatchedDirectory()
|
||||
if (wd == null)
|
||||
return
|
||||
|
||||
def params = [:]
|
||||
params['core'] = model.core
|
||||
params['directory'] = wd
|
||||
mvcGroup.createMVCGroup("watched-directory",params)
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void sync() {
|
||||
def wd = view.selectedWatchedDirectory()
|
||||
if (wd == null)
|
||||
return
|
||||
def event = new UISyncDirectoryEvent(directory : wd.directory)
|
||||
model.core.eventBus.publish(event)
|
||||
}
|
||||
}
|
@@ -104,6 +104,10 @@ class OptionsController {
|
||||
model.browseFiles = browseFiles
|
||||
settings.browseFiles = browseFiles
|
||||
|
||||
boolean allowTracking = view.allowTrackingCheckbox.model.isSelected()
|
||||
model.allowTracking = allowTracking
|
||||
settings.allowTracking = allowTracking
|
||||
|
||||
text = view.speedSmoothSecondsField.text
|
||||
model.speedSmoothSeconds = Integer.valueOf(text)
|
||||
settings.speedSmoothSeconds = Integer.valueOf(text)
|
||||
|
@@ -0,0 +1,50 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.artifact.GriffonController
|
||||
import griffon.core.controller.ControllerAction
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import net.i2p.crypto.DSAEngine
|
||||
import net.i2p.data.Base64
|
||||
|
||||
import java.awt.Toolkit
|
||||
import java.awt.datatransfer.StringSelection
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
import javax.swing.JOptionPane
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
@ArtifactProviderFor(GriffonController)
|
||||
class SignController {
|
||||
|
||||
Core core
|
||||
|
||||
@MVCMember @Nonnull
|
||||
SignView view
|
||||
|
||||
@ControllerAction
|
||||
void sign() {
|
||||
String plain = view.plainTextArea.getText()
|
||||
byte[] payload = plain.trim().getBytes(StandardCharsets.UTF_8)
|
||||
def sig = DSAEngine.getInstance().sign(payload, core.spk)
|
||||
view.signedTextArea.setText(Base64.encode(sig.data))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void copy() {
|
||||
String signed = view.signedTextArea.getText()
|
||||
StringSelection selection = new StringSelection(signed)
|
||||
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
|
||||
clipboard.setContents(selection, null)
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void close() {
|
||||
view.dialog.setVisible(false)
|
||||
mvcGroup.destroy()
|
||||
}
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.artifact.GriffonController
|
||||
import griffon.core.controller.ControllerAction
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
import com.muwire.core.files.directories.WatchedDirectoryConfigurationEvent
|
||||
|
||||
@ArtifactProviderFor(GriffonController)
|
||||
class WatchedDirectoryController {
|
||||
@MVCMember @Nonnull
|
||||
WatchedDirectoryModel model
|
||||
@MVCMember @Nonnull
|
||||
WatchedDirectoryView view
|
||||
|
||||
@ControllerAction
|
||||
void save() {
|
||||
def event = new WatchedDirectoryConfigurationEvent(
|
||||
directory : model.directory.directory,
|
||||
autoWatch : view.autoWatchCheckbox.model.isSelected(),
|
||||
syncInterval : Integer.parseInt(view.syncIntervalField.text))
|
||||
model.core.eventBus.publish(event)
|
||||
cancel()
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void cancel() {
|
||||
view.dialog.setVisible(false)
|
||||
mvcGroup.destroy()
|
||||
}
|
||||
}
|
@@ -6,10 +6,12 @@ import net.i2p.util.SystemVersion
|
||||
|
||||
import org.codehaus.griffon.runtime.core.AbstractLifecycleHandler
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.UILoadedEvent
|
||||
import com.muwire.core.files.FileSharedEvent
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
import javax.inject.Inject
|
||||
@@ -116,8 +118,9 @@ class Ready extends AbstractLifecycleHandler {
|
||||
JOptionPane.WARNING_MESSAGE)
|
||||
continue
|
||||
}
|
||||
if (nickname.contains("@")) {
|
||||
JOptionPane.showMessageDialog(null, "Nickname cannot contain @, choose another",
|
||||
if (!DataUtil.isValidName(nickname)) {
|
||||
JOptionPane.showMessageDialog(null,
|
||||
"Nickname cannot contain any of ${Constants.INVALID_NICKNAME_CHARS} and must be no longer than ${Constants.MAX_NICKNAME_LENGTH} characters. Choose another.",
|
||||
"Select another nickname", JOptionPane.WARNING_MESSAGE)
|
||||
continue
|
||||
}
|
||||
|
@@ -1,32 +1,49 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
import javax.swing.tree.DefaultMutableTreeNode
|
||||
import javax.swing.tree.DefaultTreeModel
|
||||
import javax.swing.tree.MutableTreeNode
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.files.FileTree
|
||||
import com.muwire.core.files.directories.WatchedDirectoryConfigurationEvent
|
||||
import com.muwire.core.files.directories.WatchedDirectorySyncEvent
|
||||
|
||||
import griffon.core.artifact.GriffonModel
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.transform.Observable
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
@ArtifactProviderFor(GriffonModel)
|
||||
class AdvancedSharingModel {
|
||||
|
||||
@MVCMember @Nonnull
|
||||
AdvancedSharingView view
|
||||
|
||||
def watchedDirectories = []
|
||||
def treeRoot
|
||||
def negativeTree
|
||||
|
||||
Core core
|
||||
|
||||
@Observable boolean syncActionEnabled
|
||||
|
||||
void mvcGroupInit(Map<String,String> args) {
|
||||
watchedDirectories.addAll(core.muOptions.watchedDirectories)
|
||||
watchedDirectories.addAll(core.watchedDirectoryManager.watchedDirs.values())
|
||||
core.eventBus.register(WatchedDirectorySyncEvent.class, this)
|
||||
core.eventBus.register(WatchedDirectoryConfigurationEvent.class, this)
|
||||
|
||||
treeRoot = new DefaultMutableTreeNode()
|
||||
negativeTree = new DefaultTreeModel(treeRoot)
|
||||
copyTree(treeRoot, core.fileManager.negativeTree.root)
|
||||
}
|
||||
|
||||
void mvcGroupDestroy() {
|
||||
core.eventBus.unregister(WatchedDirectorySyncEvent.class, this)
|
||||
core.eventBus.unregister(WatchedDirectoryConfigurationEvent.class, this)
|
||||
}
|
||||
|
||||
private void copyTree(DefaultMutableTreeNode jtreeNode, FileTree.TreeNode fileTreeNode) {
|
||||
jtreeNode.setUserObject(fileTreeNode.file?.getName())
|
||||
fileTreeNode.children.each {
|
||||
@@ -36,4 +53,16 @@ class AdvancedSharingModel {
|
||||
}
|
||||
}
|
||||
|
||||
void onWatchedDirectorySyncEvent(WatchedDirectorySyncEvent e) {
|
||||
runInsideUIAsync {
|
||||
view.watchedDirsTable.model.fireTableDataChanged()
|
||||
}
|
||||
}
|
||||
|
||||
void onWatchedDirectoryConfigurationEvent(WatchedDirectoryConfigurationEvent e) {
|
||||
runInsideUIAsync {
|
||||
view.watchedDirsTable.model.fireTableDataChanged()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -294,8 +294,6 @@ class MainFrameModel {
|
||||
|
||||
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
|
||||
runInsideUIAsync {
|
||||
core.muOptions.watchedDirectories.each { core.eventBus.publish(new FileSharedEvent(file : new File(it))) }
|
||||
|
||||
core.muOptions.trustSubscriptions.each {
|
||||
core.eventBus.publish(new TrustSubscriptionEvent(persona : it, subscribe : true))
|
||||
}
|
||||
@@ -415,7 +413,7 @@ class MainFrameModel {
|
||||
break
|
||||
if (parent.getChildCount() == 0) {
|
||||
File file = parent.getUserObject().file
|
||||
if (core.muOptions.watchedDirectories.contains(file.toString()))
|
||||
if (core.watchedDirectoryManager.isWatched(file))
|
||||
unshared.add(file)
|
||||
dmtn = parent
|
||||
continue
|
||||
|
@@ -18,6 +18,7 @@ class OptionsModel {
|
||||
@Observable String incompleteLocation
|
||||
@Observable boolean searchComments
|
||||
@Observable boolean browseFiles
|
||||
@Observable boolean allowTracking
|
||||
@Observable int speedSmoothSeconds
|
||||
@Observable int totalUploadSlots
|
||||
@Observable int uploadSlotsPerUser
|
||||
@@ -83,6 +84,7 @@ class OptionsModel {
|
||||
incompleteLocation = settings.incompleteLocation.getAbsolutePath()
|
||||
searchComments = settings.searchComments
|
||||
browseFiles = settings.browseFiles
|
||||
allowTracking = settings.allowTracking
|
||||
speedSmoothSeconds = settings.speedSmoothSeconds
|
||||
totalUploadSlots = settings.totalUploadSlots
|
||||
uploadSlotsPerUser = settings.uploadSlotsPerUser
|
||||
|
9
gui/griffon-app/models/com/muwire/gui/SignModel.groovy
Normal file
@@ -0,0 +1,9 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.artifact.GriffonModel
|
||||
import griffon.transform.Observable
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
@ArtifactProviderFor(GriffonModel)
|
||||
class SignModel {
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.files.directories.WatchedDirectory
|
||||
|
||||
import griffon.core.artifact.GriffonModel
|
||||
import griffon.transform.Observable
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
@ArtifactProviderFor(GriffonModel)
|
||||
class WatchedDirectoryModel {
|
||||
Core core
|
||||
WatchedDirectory directory
|
||||
|
||||
@Observable boolean autoWatch
|
||||
@Observable int syncInterval
|
||||
|
||||
void mvcGroupInit(Map<String,String> args) {
|
||||
autoWatch = directory.autoWatch
|
||||
syncInterval = directory.syncInterval
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 344 B After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 459 B After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 1003 B After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 2.2 KiB |
@@ -3,13 +3,23 @@ package com.muwire.gui
|
||||
import griffon.core.artifact.GriffonView
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import net.i2p.data.DataHelper
|
||||
|
||||
import javax.swing.JDialog
|
||||
import javax.swing.JLabel
|
||||
import javax.swing.JMenuItem
|
||||
import javax.swing.JPopupMenu
|
||||
import javax.swing.JTabbedPane
|
||||
import javax.swing.JTree
|
||||
import javax.swing.ListSelectionModel
|
||||
import javax.swing.SwingConstants
|
||||
import javax.swing.table.DefaultTableCellRenderer
|
||||
|
||||
import com.muwire.core.files.directories.WatchedDirectory
|
||||
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
|
||||
@@ -21,6 +31,8 @@ class AdvancedSharingView {
|
||||
FactoryBuilderSupport builder
|
||||
@MVCMember @Nonnull
|
||||
AdvancedSharingModel model
|
||||
@MVCMember @Nonnull
|
||||
AdvancedSharingController controller
|
||||
|
||||
def mainFrame
|
||||
def dialog
|
||||
@@ -28,6 +40,7 @@ class AdvancedSharingView {
|
||||
def negativeTreePanel
|
||||
|
||||
def watchedDirsTable
|
||||
def watchedDirsTableSortEvent
|
||||
|
||||
void initUI() {
|
||||
mainFrame = application.windowManager.findWindow("main-frame")
|
||||
@@ -43,10 +56,17 @@ class AdvancedSharingView {
|
||||
scrollPane( constraints : BorderLayout.CENTER ) {
|
||||
watchedDirsTable = table(autoCreateRowSorter : true, rowHeight : rowHeight) {
|
||||
tableModel(list : model.watchedDirectories) {
|
||||
closureColumn(header : "Directory", type : String, read : {it})
|
||||
closureColumn(header : "Directory", preferredWidth: 350, type : String, read : {it.directory.toString()})
|
||||
closureColumn(header : "Auto", preferredWidth: 100, type : Boolean, read : {it.autoWatch})
|
||||
closureColumn(header : "Interval", preferredWidth : 100, type : Integer, read : {it.syncInterval})
|
||||
closureColumn(header : "Last Sync", preferredWidth: 250, type : Long, read : {it.lastSync})
|
||||
}
|
||||
}
|
||||
}
|
||||
panel (constraints : BorderLayout.SOUTH) {
|
||||
button(text : "Configure", configureAction)
|
||||
button(text : "Sync", enabled : bind{model.syncActionEnabled}, syncAction)
|
||||
}
|
||||
}
|
||||
|
||||
negativeTreePanel = builder.panel {
|
||||
@@ -59,6 +79,54 @@ class AdvancedSharingView {
|
||||
tree(rootVisible : false, rowHeight : rowHeight,jtree)
|
||||
}
|
||||
}
|
||||
|
||||
def centerRenderer = new DefaultTableCellRenderer()
|
||||
centerRenderer.setHorizontalAlignment(JLabel.CENTER)
|
||||
watchedDirsTable.setDefaultRenderer(Long.class, new DateRenderer())
|
||||
watchedDirsTable.setDefaultRenderer(Integer.class, centerRenderer)
|
||||
|
||||
watchedDirsTable.rowSorter.addRowSorterListener({evt -> watchedDirsTableSortEvent = evt})
|
||||
def selectionModel = watchedDirsTable.getSelectionModel()
|
||||
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
|
||||
selectionModel.addListSelectionListener({
|
||||
def directory = selectedWatchedDirectory()
|
||||
model.syncActionEnabled = !(directory == null || directory.autoWatch)
|
||||
})
|
||||
|
||||
watchedDirsTable.addMouseListener(new MouseAdapter() {
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
if (e.isPopupTrigger())
|
||||
showMenu(e)
|
||||
}
|
||||
public void mousePressed(MouseEvent e) {
|
||||
if (e.isPopupTrigger())
|
||||
showMenu(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private void showMenu(MouseEvent e) {
|
||||
JPopupMenu menu = new JPopupMenu()
|
||||
JMenuItem configure = new JMenuItem("Configure")
|
||||
configure.addActionListener({controller.configure()})
|
||||
menu.add(configure)
|
||||
|
||||
if (model.syncActionEnabled) {
|
||||
JMenuItem sync = new JMenuItem("Sync")
|
||||
sync.addActionListener({controller.sync()})
|
||||
menu.add(sync)
|
||||
}
|
||||
|
||||
menu.show(e.getComponent(), e.getX(), e.getY())
|
||||
}
|
||||
|
||||
WatchedDirectory selectedWatchedDirectory() {
|
||||
int row = watchedDirsTable.getSelectedRow()
|
||||
if (row < 0)
|
||||
return null
|
||||
if (watchedDirsTableSortEvent != null)
|
||||
row = watchedDirsTable.rowSorter.convertRowIndexToModel(row)
|
||||
model.watchedDirectories[row]
|
||||
}
|
||||
|
||||
void mvcGroupInit(Map<String,String> args) {
|
||||
|
@@ -144,6 +144,11 @@ class MainFrameView {
|
||||
mvcGroup.createMVCGroup("chat-monitor","chat-monitor",env)
|
||||
}
|
||||
})
|
||||
menuItem("Sign Tool", actionPerformed : {
|
||||
def env = [:]
|
||||
env['core'] = model.core
|
||||
mvcGroup.createMVCGroup("sign",env)
|
||||
})
|
||||
}
|
||||
}
|
||||
borderLayout()
|
||||
@@ -447,7 +452,7 @@ class MainFrameView {
|
||||
closureColumn(header : "Publisher", preferredWidth: 350, type : String, read : {it.getPublisher().getHumanReadableName()})
|
||||
closureColumn(header : "Files", preferredWidth: 10, type : Integer, read : {model.core.feedManager.getFeedItems(it.getPublisher()).size()})
|
||||
closureColumn(header : "Last Updated", type : Long, read : {it.getLastUpdated()})
|
||||
closureColumn(header : "Status", preferredWidth: 10, type : String, read : {it.getStatus()})
|
||||
closureColumn(header : "Status", preferredWidth: 10, type : String, read : {it.getStatus().toString()})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -43,6 +43,7 @@ class OptionsView {
|
||||
def shareHiddenCheckbox
|
||||
def searchCommentsCheckbox
|
||||
def browseFilesCheckbox
|
||||
def allowTrackingCheckbox
|
||||
def speedSmoothSecondsField
|
||||
def totalUploadSlotsField
|
||||
def uploadSlotsPerUserField
|
||||
@@ -107,6 +108,10 @@ class OptionsView {
|
||||
fill : GridBagConstraints.HORIZONTAL, weightx: 100))
|
||||
browseFilesCheckbox = checkBox(selected : bind {model.browseFiles}, constraints : gbc(gridx : 1, gridy : 1,
|
||||
anchor : GridBagConstraints.LINE_END, fill : GridBagConstraints.HORIZONTAL, weightx: 0))
|
||||
label(text : "Allow tracking", constraints : gbc(gridx: 0, gridy: 2, anchor: GridBagConstraints.LINE_START,
|
||||
fill : GridBagConstraints.HORIZONTAL, weightx: 100))
|
||||
allowTrackingCheckbox = checkBox(selected : bind {model.allowTracking}, constraints : gbc(gridx: 1, gridy : 2,
|
||||
anchor : GridBagConstraints.LINE_END, fill : GridBagConstraints.HORIZONTAL, weightx : 0))
|
||||
}
|
||||
|
||||
panel (border : titledBorder(title : "Download Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP,
|
||||
|
66
gui/griffon-app/views/com/muwire/gui/SignView.groovy
Normal file
@@ -0,0 +1,66 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.artifact.GriffonView
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
import javax.swing.JDialog
|
||||
import javax.swing.SwingConstants
|
||||
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
@ArtifactProviderFor(GriffonView)
|
||||
class SignView {
|
||||
@MVCMember @Nonnull
|
||||
FactoryBuilderSupport builder
|
||||
|
||||
def mainFrame
|
||||
def dialog
|
||||
def p
|
||||
def plainTextArea
|
||||
def signedTextArea
|
||||
|
||||
void initUI() {
|
||||
mainFrame = application.windowManager.findWindow("main-frame")
|
||||
|
||||
dialog = new JDialog(mainFrame, "Sign Text", true)
|
||||
|
||||
p = builder.panel {
|
||||
borderLayout()
|
||||
panel (constraints : BorderLayout.NORTH) {
|
||||
label("Enter text to be signed")
|
||||
}
|
||||
panel (constraints : BorderLayout.CENTER) {
|
||||
gridLayout(rows : 2, cols: 1)
|
||||
scrollPane {
|
||||
plainTextArea = textArea(rows : 10, columns : 50, editable : true, lineWrap: true, wrapStyleWord : true)
|
||||
}
|
||||
scrollPane {
|
||||
signedTextArea = textArea(rows : 10, columns : 50, editable : false, lineWrap : true, wrapStyleWord : true)
|
||||
}
|
||||
}
|
||||
panel (constraints : BorderLayout.SOUTH) {
|
||||
button(text : "Sign", signAction)
|
||||
button(text : "Copy To Clipboard", copyAction)
|
||||
button(text : "Dismiss", closeAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void mvcGroupInit(Map<String,String> args) {
|
||||
dialog.getContentPane().add(p)
|
||||
dialog.pack()
|
||||
dialog.setLocationRelativeTo(mainFrame)
|
||||
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
|
||||
dialog.addWindowListener( new WindowAdapter() {
|
||||
public void windowClosed(WindowEvent e) {
|
||||
mvcGroup.destroy()
|
||||
}
|
||||
})
|
||||
dialog.show()
|
||||
}
|
||||
}
|
@@ -0,0 +1,74 @@
|
||||
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.artifact.GriffonView
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
import javax.swing.JDialog
|
||||
import javax.swing.SwingConstants
|
||||
import javax.swing.event.ChangeListener
|
||||
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.GridBagConstraints
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
@ArtifactProviderFor(GriffonView)
|
||||
class WatchedDirectoryView {
|
||||
@MVCMember @Nonnull
|
||||
FactoryBuilderSupport builder
|
||||
@MVCMember @Nonnull
|
||||
WatchedDirectoryModel model
|
||||
|
||||
def dialog
|
||||
def p
|
||||
def mainFrame
|
||||
|
||||
def autoWatchCheckbox
|
||||
def syncIntervalField
|
||||
|
||||
void initUI() {
|
||||
mainFrame = application.windowManager.findWindow("main-frame")
|
||||
dialog = new JDialog(mainFrame, "Watched Directory Configuration", true)
|
||||
dialog.setResizable(false)
|
||||
|
||||
p = builder.panel {
|
||||
borderLayout()
|
||||
panel (constraints : BorderLayout.NORTH) {
|
||||
label("Configuration for directory " + model.directory.directory.toString())
|
||||
}
|
||||
panel (constraints : BorderLayout.CENTER) {
|
||||
gridBagLayout()
|
||||
label(text : "Auto-watch directory using operating system", constraints : gbc(gridx: 0, gridy : 0, anchor : GridBagConstraints.LINE_START, weightx: 100))
|
||||
autoWatchCheckbox = checkBox(selected : bind {model.autoWatch}, constraints : gbc(gridx: 1, gridy : 0, anchor : GridBagConstraints.LINE_END))
|
||||
label(text : "Directory sync frequency (seconds, 0 means never)", enabled : bind {!model.autoWatch}, constraints : gbc(gridx: 0, gridy : 1, anchor : GridBagConstraints.LINE_START, weightx: 100))
|
||||
syncIntervalField = textField(text : bind {model.syncInterval}, columns: 4, enabled : bind {!model.autoWatch},
|
||||
constraints: gbc(gridx: 1, gridy : 1, anchor : GridBagConstraints.LINE_END, insets : [0,10,0,0]))
|
||||
}
|
||||
panel (constraints : BorderLayout.SOUTH) {
|
||||
button(text : "Save", saveAction)
|
||||
button(text : "Cancel", cancelAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void mvcGroupInit(Map<String,String> args) {
|
||||
autoWatchCheckbox.addChangeListener({e ->
|
||||
model.autoWatch = autoWatchCheckbox.model.isSelected()
|
||||
} as ChangeListener)
|
||||
|
||||
dialog.getContentPane().add(p)
|
||||
dialog.pack()
|
||||
dialog.setLocationRelativeTo(mainFrame)
|
||||
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
|
||||
dialog.addWindowListener(new WindowAdapter() {
|
||||
public void windowClosed(WindowEvent e) {
|
||||
mvcGroup.destroy()
|
||||
}
|
||||
})
|
||||
dialog.show()
|
||||
}
|
||||
}
|
@@ -6,8 +6,8 @@ class DownloaderComparator implements Comparator<Downloader>{
|
||||
|
||||
@Override
|
||||
public int compare(Downloader o1, Downloader o2) {
|
||||
double d1 = o1.donePieces() * 1.0 / o1.nPieces
|
||||
double d2 = o2.donePieces() * 1.0 / o2.nPieces
|
||||
double d1 = o1.donePieces().toDouble() / o1.nPieces
|
||||
double d2 = o2.donePieces().toDouble() / o2.nPieces
|
||||
return Double.compare(d1, d2);
|
||||
}
|
||||
}
|
||||
|
@@ -6,4 +6,5 @@ dependencies {
|
||||
compile "net.i2p:i2p:${i2pVersion}"
|
||||
testCompile 'org.junit.jupiter:junit-jupiter-api:5.4.2'
|
||||
testCompile 'junit:junit:4.12'
|
||||
testCompile 'org.codehaus.groovy:groovy-all:2.4.15'
|
||||
}
|
||||
|
62
host-cache/logging/logging.properties
Normal file
@@ -0,0 +1,62 @@
|
||||
############################################################
|
||||
# Default Logging Configuration File
|
||||
#
|
||||
# You can use a different file by specifying a filename
|
||||
# with the java.util.logging.config.file system property.
|
||||
# For example java -Djava.util.logging.config.file=myfile
|
||||
############################################################
|
||||
|
||||
############################################################
|
||||
# Global properties
|
||||
############################################################
|
||||
|
||||
# "handlers" specifies a comma separated list of log Handler
|
||||
# classes. These handlers will be installed during VM startup.
|
||||
# Note that these classes must be on the system classpath.
|
||||
# By default we only configure a ConsoleHandler, which will only
|
||||
# show messages at the INFO and above levels.
|
||||
handlers= java.util.logging.FileHandler
|
||||
|
||||
# To also add the FileHandler, use the following line instead.
|
||||
#handlers= java.util.logging.FileHandler, java.util.logging.ConsoleHandler
|
||||
|
||||
# Default global logging level.
|
||||
# This specifies which kinds of events are logged across
|
||||
# all loggers. For any given facility this global level
|
||||
# can be overriden by a facility specific level
|
||||
# Note that the ConsoleHandler also has a separate level
|
||||
# setting to limit messages printed to the console.
|
||||
.level= INFO
|
||||
|
||||
############################################################
|
||||
# Handler specific properties.
|
||||
# Describes specific configuration info for Handlers.
|
||||
############################################################
|
||||
|
||||
# default file output is in user's home directory.
|
||||
java.util.logging.FileHandler.pattern = hostcache.log
|
||||
java.util.logging.FileHandler.limit = 5000000
|
||||
java.util.logging.FileHandler.count = 1
|
||||
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter
|
||||
|
||||
# Limit the message that are printed on the console to INFO and above.
|
||||
java.util.logging.ConsoleHandler.level = INFO
|
||||
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
|
||||
|
||||
# Example to customize the SimpleFormatter output format
|
||||
# to print one-line log message like this:
|
||||
# <level>: <log message> [<date/time>]
|
||||
#
|
||||
#java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n
|
||||
|
||||
java.util.logging.SimpleFormatter.format=%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$s %2$s %5$s %6$s %n
|
||||
|
||||
############################################################
|
||||
# Facility specific properties.
|
||||
# Provides extra control for each logger.
|
||||
############################################################
|
||||
|
||||
# For example, set the com.xyz.foo logger to only log SEVERE
|
||||
# messages:
|
||||
com.xyz.foo.level = SEVERE
|
||||
net.i2p.client.streaming.impl.level = SEVERE
|
23
host-cache/scripts/count_total.py
Executable file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import os,sys,json
|
||||
|
||||
if len(sys.argv) < 2 :
|
||||
print("This script counts unique hosts in the MuWire network",file = sys.stderr)
|
||||
print("Pass the prefix of the files to analyse. For example:",file = sys.stderr)
|
||||
print("\"20200427\" will count unique hosts on 27th of April 2020",file = sys.stderr)
|
||||
print("\"202004\" will count unique hosts during all of April 2020",file = sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
day = sys.argv[1]
|
||||
files = os.listdir(".")
|
||||
files = [x for x in files if x.startswith(day)]
|
||||
|
||||
hosts = set()
|
||||
|
||||
for f in files:
|
||||
for line in open(f):
|
||||
host = json.loads(line)
|
||||
hosts.add(host["destination"])
|
||||
|
||||
print(len(hosts))
|
@@ -40,12 +40,13 @@ class Crawler {
|
||||
try {
|
||||
uuid = UUID.fromString(pong.uuid)
|
||||
} catch (IllegalArgumentException bad) {
|
||||
log.log(Level.WARNING,"couldn't parse uuid",bad)
|
||||
hostPool.fail(host)
|
||||
return
|
||||
}
|
||||
|
||||
if (!uuid.equals(currentUUID)) {
|
||||
log.info("uuid mismatch")
|
||||
log.warning("uuid mismatch $uuid expected $currentUUID")
|
||||
hostPool.fail(host)
|
||||
return
|
||||
}
|
||||
@@ -75,11 +76,12 @@ class Crawler {
|
||||
}
|
||||
|
||||
synchronized def startCrawl() {
|
||||
currentUUID = UUID.randomUUID()
|
||||
log.info("starting new crawl with uuid $currentUUID inFlight ${inFlight.size()}")
|
||||
if (!inFlight.isEmpty()) {
|
||||
inFlight.values().each { hostPool.fail(it) }
|
||||
inFlight.clear()
|
||||
}
|
||||
currentUUID = UUID.randomUUID()
|
||||
hostPool.getUnverified(parallel).each {
|
||||
inFlight.put(it.destination, it)
|
||||
pinger.ping(it, currentUUID)
|
||||
|
@@ -15,4 +15,9 @@ class Host {
|
||||
public boolean equals(other) {
|
||||
return destination.equals(other.destination)
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
"Host[b32:${destination.toBase32()} verifyTime:$verifyTime verificationFailures:$verificationFailures]"
|
||||
}
|
||||
}
|
||||
|
@@ -64,8 +64,10 @@ public class HostCache {
|
||||
Timer timer = new Timer("timer", true)
|
||||
timer.schedule({hostPool.age()} as TimerTask, 1000,1000)
|
||||
timer.schedule({crawler.startCrawl()} as TimerTask, 10000, 10000)
|
||||
File verified = new File("verified.json")
|
||||
File unverified = new File("unverified.json")
|
||||
File verified = new File("verified")
|
||||
File unverified = new File("unverified")
|
||||
verified.mkdir()
|
||||
unverified.mkdir()
|
||||
timer.schedule({hostPool.serialize(verified, unverified)} as TimerTask, 10000, 60 * 60 * 1000)
|
||||
|
||||
session.addMuxedSessionListener(new Listener(hostPool: hostPool, toReturn: 2, crawler: crawler),
|
||||
|
@@ -1,11 +1,14 @@
|
||||
package com.muwire.hostcache
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.stream.Collectors
|
||||
|
||||
import groovy.json.JsonOutput
|
||||
|
||||
class HostPool {
|
||||
|
||||
private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyyMMdd-HH")
|
||||
|
||||
final def maxFailures
|
||||
final def maxAge
|
||||
|
||||
@@ -77,9 +80,11 @@ class HostPool {
|
||||
}
|
||||
}
|
||||
|
||||
synchronized void serialize(File verifiedFile, File unverifiedFile) {
|
||||
write(verifiedFile, verified.values())
|
||||
write(unverifiedFile, unverified.values())
|
||||
synchronized void serialize(File verifiedPath, File unverifiedPath) {
|
||||
def now = new Date()
|
||||
now = SDF.format(now)
|
||||
write(new File(verifiedPath, now), verified.values())
|
||||
write(new File(unverifiedPath, now), unverified.values())
|
||||
}
|
||||
|
||||
private void write(File target, Collection hosts) {
|
||||
|
@@ -1,17 +1,21 @@
|
||||
package com.muwire.hostcache
|
||||
|
||||
import groovy.json.JsonOutput
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.client.I2PSession
|
||||
import net.i2p.client.SendMessageOptions
|
||||
import net.i2p.client.datagram.I2PDatagramMaker
|
||||
|
||||
@Log
|
||||
class Pinger {
|
||||
|
||||
final def session
|
||||
Pinger(session) {
|
||||
final I2PSession session
|
||||
Pinger(I2PSession session) {
|
||||
this.session = session
|
||||
}
|
||||
|
||||
def ping(host, uuid) {
|
||||
log.info("pinging $host with uuid:$uuid")
|
||||
def maker = new I2PDatagramMaker(session)
|
||||
def payload = new HashMap()
|
||||
payload.type = "CrawlerPing"
|
||||
@@ -19,6 +23,8 @@ class Pinger {
|
||||
payload.uuid = uuid
|
||||
payload = JsonOutput.toJson(payload)
|
||||
payload = maker.makeI2PDatagram(payload.bytes)
|
||||
session.sendMessage(host.destination, payload, I2PSession.PROTO_DATAGRAM, 0, 0)
|
||||
def options = new SendMessageOptions()
|
||||
options.setSendLeaseSet(true)
|
||||
session.sendMessage(host.destination, payload, 0, payload.length, I2PSession.PROTO_DATAGRAM, 0, 0, options)
|
||||
}
|
||||
}
|
||||
|
@@ -5,5 +5,6 @@ include 'core'
|
||||
include 'gui'
|
||||
include 'cli'
|
||||
include 'cli-lanterna'
|
||||
include 'tracker'
|
||||
// include 'webui'
|
||||
// include 'plug'
|
||||
|
47
tracker/build.gradle
Normal file
@@ -0,0 +1,47 @@
|
||||
buildscript {
|
||||
|
||||
repositories {
|
||||
jcenter()
|
||||
mavenLocal()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.github.jengelman.gradle.plugins:shadow:5.2.0'
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id 'org.springframework.boot' version '2.2.6.RELEASE'
|
||||
}
|
||||
|
||||
apply plugin : 'application'
|
||||
apply plugin : 'io.spring.dependency-management'
|
||||
|
||||
application {
|
||||
mainClassName = 'com.muwire.tracker.Tracker'
|
||||
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties','-Xmx256M',"-Dbuild.version=${project.version}"]
|
||||
applicationName = 'mwtrackerd'
|
||||
}
|
||||
|
||||
apply plugin : 'com.github.johnrengelman.shadow'
|
||||
|
||||
springBoot {
|
||||
buildInfo {
|
||||
properties {
|
||||
version = "${project.version}"
|
||||
name = "mwtrackerd"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(":core")
|
||||
compile 'com.github.briandilley.jsonrpc4j:jsonrpc4j:1.5.3'
|
||||
|
||||
compile 'org.springframework.boot:spring-boot-starter'
|
||||
compile 'org.springframework.boot:spring-boot-starter-actuator'
|
||||
compile 'org.springframework.boot:spring-boot-starter-web'
|
||||
|
||||
runtime 'javax.jws:jsr181-api:1.0-MR1'
|
||||
}
|
||||
|
28
tracker/src/main/groovy/com/muwire/tracker/Host.groovy
Normal file
@@ -0,0 +1,28 @@
|
||||
package com.muwire.tracker
|
||||
|
||||
import com.muwire.core.Persona
|
||||
|
||||
/**
|
||||
* A participant in a swarm. The same persona can be a member of multiple
|
||||
* swarms, but in that case it would have multiple Host objects
|
||||
*/
|
||||
class Host {
|
||||
final Persona persona
|
||||
long lastPinged
|
||||
long lastResponded
|
||||
int failures
|
||||
volatile String xHave
|
||||
|
||||
Host(Persona persona) {
|
||||
this.persona = persona
|
||||
}
|
||||
|
||||
boolean isExpired(long cutoff, int maxFailures) {
|
||||
lastPinged > lastResponded && lastResponded <= cutoff && failures >= maxFailures
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
"Host:[${persona.getHumanReadableName()} lastPinged:$lastPinged lastResponded:$lastResponded failures:$failures xHave:$xHave]"
|
||||
}
|
||||
}
|
182
tracker/src/main/groovy/com/muwire/tracker/Pinger.groovy
Normal file
@@ -0,0 +1,182 @@
|
||||
package com.muwire.tracker
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.logging.Level
|
||||
|
||||
import javax.annotation.PostConstruct
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.Persona
|
||||
|
||||
import groovy.json.JsonOutput
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.client.I2PSession
|
||||
import net.i2p.client.I2PSessionMuxedListener
|
||||
import net.i2p.client.SendMessageOptions
|
||||
import net.i2p.client.datagram.I2PDatagramDissector
|
||||
import net.i2p.client.datagram.I2PDatagramMaker
|
||||
import net.i2p.data.Base64
|
||||
|
||||
@Component
|
||||
@Log
|
||||
class Pinger {
|
||||
@Autowired
|
||||
private Core core
|
||||
|
||||
@Autowired
|
||||
private SwarmManager swarmManager
|
||||
|
||||
@Autowired
|
||||
private TrackerProperties trackerProperties
|
||||
|
||||
private final Map<UUID, PingInProgress> inFlight = new ConcurrentHashMap<>()
|
||||
private final Timer expiryTimer = new Timer("pinger-timer",true)
|
||||
|
||||
@PostConstruct
|
||||
private void registerListener() {
|
||||
core.getI2pSession().addMuxedSessionListener(new Listener(), I2PSession.PROTO_DATAGRAM, Constants.TRACKER_PORT)
|
||||
expiryTimer.schedule({expirePings()} as TimerTask, 1000, 1000)
|
||||
}
|
||||
|
||||
private void expirePings() {
|
||||
final long now = System.currentTimeMillis()
|
||||
for(Iterator<UUID> iter = inFlight.keySet().iterator(); iter.hasNext();) {
|
||||
UUID uuid = iter.next()
|
||||
PingInProgress ping = inFlight.get(uuid)
|
||||
if (now - ping.pingTime > trackerProperties.getSwarmParameters().getPingTimeout() * 1000L) {
|
||||
iter.remove()
|
||||
swarmManager.fail(ping.target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ping(SwarmManager.HostAndIH target, long now) {
|
||||
UUID uuid = UUID.randomUUID()
|
||||
def ping = new PingInProgress(target, now)
|
||||
inFlight.put(uuid, ping)
|
||||
|
||||
def message = [:]
|
||||
message.type = "TrackerPing"
|
||||
message.version = 1
|
||||
message.infoHash = Base64.encode(target.getInfoHash().getRoot())
|
||||
message.uuid = uuid.toString()
|
||||
|
||||
message = JsonOutput.toJson(message)
|
||||
def maker = new I2PDatagramMaker(core.getI2pSession())
|
||||
message = maker.makeI2PDatagram(message.bytes)
|
||||
def options = new SendMessageOptions()
|
||||
options.setSendLeaseSet(true)
|
||||
core.getI2pSession().sendMessage(target.getHost().getPersona().getDestination(), message, 0, message.length, I2PSession.PROTO_DATAGRAM,
|
||||
Constants.TRACKER_PORT, Constants.TRACKER_PORT, options)
|
||||
}
|
||||
|
||||
private static class PingInProgress {
|
||||
private final SwarmManager.HostAndIH target
|
||||
private final long pingTime
|
||||
PingInProgress(SwarmManager.HostAndIH target, long pingTime) {
|
||||
this.target = target
|
||||
this.pingTime = pingTime
|
||||
}
|
||||
}
|
||||
|
||||
private class Listener implements I2PSessionMuxedListener {
|
||||
|
||||
@Override
|
||||
public void messageAvailable(I2PSession session, int msgId, long size) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void messageAvailable(I2PSession session, int msgId, long size, int proto, int fromport, int toport) {
|
||||
if (proto != I2PSession.PROTO_DATAGRAM) {
|
||||
log.warning("received unexpected protocol $proto")
|
||||
return
|
||||
}
|
||||
|
||||
byte [] payload = session.receiveMessage(msgId)
|
||||
def dissector = new I2PDatagramDissector()
|
||||
try {
|
||||
dissector.loadI2PDatagram(payload)
|
||||
def sender = dissector.getSender()
|
||||
|
||||
log.info("got a response from ${sender.toBase32()}")
|
||||
|
||||
payload = dissector.getPayload()
|
||||
def slurper = new JsonSlurper()
|
||||
def json = slurper.parse(payload)
|
||||
|
||||
if (json.type != "TrackerPong") {
|
||||
log.warning("unknown type ${json.type}")
|
||||
return
|
||||
}
|
||||
|
||||
if (json.me == null) {
|
||||
log.warning("sender persona missing")
|
||||
return
|
||||
}
|
||||
|
||||
Persona senderPersona = new Persona(new ByteArrayInputStream(Base64.decode(json.me)))
|
||||
if (sender != senderPersona.getDestination()) {
|
||||
log.warning("persona in payload does not match sender ${senderPersona.getHumanReadableName()}")
|
||||
return
|
||||
}
|
||||
|
||||
if (json.uuid == null) {
|
||||
log.warning("uuid missing")
|
||||
return
|
||||
}
|
||||
|
||||
UUID uuid = UUID.fromString(json.uuid)
|
||||
def ping = inFlight.remove(uuid)
|
||||
|
||||
if (ping == null) {
|
||||
log.warning("no ping in progress for $uuid")
|
||||
return
|
||||
}
|
||||
|
||||
if (json.code == null) {
|
||||
log.warning("no code")
|
||||
return
|
||||
}
|
||||
|
||||
int code = json.code
|
||||
|
||||
if (json.xHave != null)
|
||||
ping.target.host.xHave = json.xHave
|
||||
|
||||
|
||||
Set<Persona> altlocs = new HashSet<>()
|
||||
json.altlocs?.collect(altlocs,{ new Persona(new ByteArrayInputStream(Base64.decode(it))) })
|
||||
|
||||
log.info("For ${ping.target.infoHash} received code $code and altlocs ${altlocs.size()}")
|
||||
|
||||
swarmManager.handleResponse(ping.target, code, altlocs)
|
||||
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING,"invalid datagram",e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportAbuse(I2PSession session, int severity) {
|
||||
log.warning("reportabuse $session $severity")
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnected(I2PSession session) {
|
||||
log.severe("disconnected")
|
||||
}
|
||||
|
||||
@Override
|
||||
public void errorOccurred(I2PSession session, String message, Throwable error) {
|
||||
log.log(Level.SEVERE,message,error)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,71 @@
|
||||
package com.muwire.tracker
|
||||
|
||||
class SetupWizard {
|
||||
|
||||
private final File home
|
||||
|
||||
SetupWizard(File home) {
|
||||
this.home = home
|
||||
}
|
||||
|
||||
Properties performSetup() {
|
||||
println "**** Welcome to mwtrackerd setup wizard *****"
|
||||
println "This wizard ask you some questions and configure the settings for the MuWire tracker daemon."
|
||||
println "The settings will be saved in ${home.getAbsolutePath()} where you can edit them manually if you wish."
|
||||
println "You can re-run this wizard by launching mwtrackerd with the \"setup\" argument."
|
||||
println "*****************"
|
||||
|
||||
Scanner scanner = new Scanner(System.in)
|
||||
|
||||
Properties rv = new Properties()
|
||||
|
||||
// nickname
|
||||
while(true) {
|
||||
println "Please select a nickname for your tracker"
|
||||
String nick = scanner.nextLine()
|
||||
if (nick.trim().length() == 0) {
|
||||
println "nickname cannot be empty"
|
||||
continue
|
||||
}
|
||||
rv['nickname'] = nick
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
// i2cp host and port
|
||||
println "Enter the address of an I2P or I2Pd router to connect to. (default is 127.0.0.1)"
|
||||
String i2cpHost = scanner.nextLine()
|
||||
if (i2cpHost.trim().length() == 0)
|
||||
i2cpHost = "127.0.0.1"
|
||||
rv['i2cp.tcp.host'] = i2cpHost
|
||||
|
||||
println "Enter the port of the I2CP interface of the I2P[d] router (default is 7654)"
|
||||
String i2cpPort = scanner.nextLine()
|
||||
if (i2cpPort.trim().length() == 0)
|
||||
i2cpPort = "7654"
|
||||
rv['i2cp.tcp.port'] = i2cpPort
|
||||
|
||||
// json-rpc interface
|
||||
println "Enter the address to which to bind the JSON-RPC interface of the tracker."
|
||||
println "Default is 127.0.0.1. If you want to allow JSON-RPC connections from other hosts you can enter 0.0.0.0"
|
||||
String jsonRpcIface = scanner.nextLine()
|
||||
if (jsonRpcIface.trim().length() == 0)
|
||||
jsonRpcIface = "127.0.0.1"
|
||||
rv['jsonrpc.iface'] = jsonRpcIface
|
||||
|
||||
println "Enter the port on which the JSON-RPC interface should listen. (default is 12345)"
|
||||
String jsonRpcPort = scanner.nextLine()
|
||||
if (jsonRpcPort.trim().length() == 0)
|
||||
jsonRpcPort = "12345"
|
||||
rv['jsonrpc.port'] = jsonRpcPort
|
||||
|
||||
// that's all
|
||||
println "*****************"
|
||||
println "That's all the setup that's required to get the tracker up and running."
|
||||
println "The tracker has many other settings which can be changed in the config files."
|
||||
println "Refer to the documentation for their description."
|
||||
println "*****************"
|
||||
|
||||
rv
|
||||
}
|
||||
}
|
182
tracker/src/main/groovy/com/muwire/tracker/Swarm.groovy
Normal file
@@ -0,0 +1,182 @@
|
||||
package com.muwire.tracker
|
||||
|
||||
import java.util.function.Function
|
||||
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Persona
|
||||
|
||||
import groovy.util.logging.Log
|
||||
|
||||
/**
|
||||
* A swarm for a given file
|
||||
*/
|
||||
@Log
|
||||
class Swarm {
|
||||
final InfoHash infoHash
|
||||
|
||||
/**
|
||||
* Invariant: these four collections are mutually exclusive.
|
||||
* A given host can be only in one of them at the same time.
|
||||
*/
|
||||
private final Map<Persona,Host> seeds = new HashMap<>()
|
||||
private final Map<Persona,Host> leeches = new HashMap<>()
|
||||
private final Map<Persona,Host> unknown = new HashMap<>()
|
||||
private final Set<Persona> negative = new HashSet<>()
|
||||
|
||||
/**
|
||||
* hosts which are currently being pinged. Hosts can be in here
|
||||
* and in the collections above, except for negative.
|
||||
*/
|
||||
private final Map<Persona, Host> inFlight = new HashMap<>()
|
||||
|
||||
/**
|
||||
* Last time a query was made to the MW network for this hash
|
||||
*/
|
||||
private long lastQueryTime
|
||||
|
||||
/**
|
||||
* Last time a batch of hosts was pinged
|
||||
*/
|
||||
private long lastPingTime
|
||||
|
||||
Swarm(InfoHash infoHash) {
|
||||
this.infoHash = infoHash
|
||||
}
|
||||
|
||||
/**
|
||||
* @param cutoff expire hosts older than this
|
||||
*/
|
||||
synchronized void expire(long cutoff, int maxFailures) {
|
||||
doExpire(cutoff, maxFailures, seeds)
|
||||
doExpire(cutoff, maxFailures, leeches)
|
||||
doExpire(cutoff, maxFailures, unknown)
|
||||
}
|
||||
|
||||
private static void doExpire(long cutoff, int maxFailures, Map<Persona,Host> map) {
|
||||
for (Iterator<Persona> iter = map.keySet().iterator(); iter.hasNext();) {
|
||||
Persona p = iter.next()
|
||||
Host h = map.get(p)
|
||||
if (h.isExpired(cutoff, maxFailures))
|
||||
iter.remove()
|
||||
}
|
||||
}
|
||||
|
||||
synchronized boolean shouldQuery(long queryCutoff, long now) {
|
||||
if (!(seeds.isEmpty() &&
|
||||
leeches.isEmpty() &&
|
||||
inFlight.isEmpty() &&
|
||||
unknown.isEmpty()))
|
||||
return false
|
||||
if (lastQueryTime <= queryCutoff) {
|
||||
lastQueryTime = now
|
||||
return true
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
synchronized boolean isHealthy() {
|
||||
!seeds.isEmpty()
|
||||
// TODO add xHave accumulation of leeches
|
||||
}
|
||||
|
||||
synchronized void add(Persona p) {
|
||||
if (!(seeds.containsKey(p) || leeches.containsKey(p) ||
|
||||
negative.contains(p) || inFlight.containsKey(p)))
|
||||
unknown.computeIfAbsent(p, {new Host(it)} as Function)
|
||||
}
|
||||
|
||||
synchronized void handleResponse(Host responder, int code) {
|
||||
Host h = inFlight.remove(responder.persona)
|
||||
if (responder != h)
|
||||
log.warning("received a response mismatch from host $responder vs $h")
|
||||
|
||||
responder.lastResponded = System.currentTimeMillis()
|
||||
responder.failures = 0
|
||||
switch(code) {
|
||||
case 200: addSeed(responder); break
|
||||
case 206 : addLeech(responder); break;
|
||||
default :
|
||||
addNegative(responder)
|
||||
}
|
||||
}
|
||||
|
||||
synchronized void fail(Host failed) {
|
||||
Host h = inFlight.remove(failed.persona)
|
||||
if (h != failed)
|
||||
log.warning("failed a host that wasn't in flight $failed vs $h")
|
||||
h.failures++
|
||||
}
|
||||
|
||||
private void addSeed(Host h) {
|
||||
leeches.remove(h.persona)
|
||||
unknown.remove(h.persona)
|
||||
seeds.put(h.persona, h)
|
||||
}
|
||||
|
||||
private void addLeech(Host h) {
|
||||
unknown.remove(h.persona)
|
||||
seeds.remove(h.persona)
|
||||
leeches.put(h.persona, h)
|
||||
}
|
||||
|
||||
private void addNegative(Host h) {
|
||||
unknown.remove(h.persona)
|
||||
seeds.remove(h.persona)
|
||||
leeches.remove(h.persona)
|
||||
negative.add(h.persona)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param max number of hosts to give back
|
||||
* @param now what time is it now
|
||||
* @param cutoff only consider hosts which have been pinged before this time
|
||||
* @return hosts to be pinged
|
||||
*/
|
||||
synchronized List<Host> getBatchToPing(int max, long now, long cutOff) {
|
||||
List<Host> rv = new ArrayList<>()
|
||||
rv.addAll(unknown.values())
|
||||
rv.addAll(seeds.values())
|
||||
rv.addAll(leeches.values())
|
||||
rv.removeAll(inFlight.values())
|
||||
|
||||
rv.removeAll { it.lastPinged >= cutOff }
|
||||
|
||||
Collections.sort(rv, {l, r ->
|
||||
Long.compare(l.lastPinged, r.lastPinged)
|
||||
} as Comparator<Host>)
|
||||
|
||||
if (rv.size() > max)
|
||||
rv = rv[0..(max-1)]
|
||||
|
||||
rv.each {
|
||||
it.lastPinged = now
|
||||
inFlight.put(it.persona, it)
|
||||
}
|
||||
|
||||
if (!rv.isEmpty())
|
||||
lastPingTime = now
|
||||
rv
|
||||
}
|
||||
|
||||
synchronized long getLastPingTime() {
|
||||
lastPingTime
|
||||
}
|
||||
|
||||
public Info info() {
|
||||
List<String> seeders = seeds.keySet().collect { it.getHumanReadableName() }
|
||||
List<String> leechers = leeches.keySet().collect { it.getHumanReadableName() }
|
||||
return new Info(seeders, leechers, unknown.size(), negative.size())
|
||||
}
|
||||
|
||||
public static class Info {
|
||||
final List<String> seeders, leechers
|
||||
final int unknown, negative
|
||||
|
||||
Info(List<String> seeders, List<String> leechers, int unknown, int negative) {
|
||||
this.seeders = seeders
|
||||
this.leechers = leechers
|
||||
this.unknown = unknown
|
||||
this.negative = negative
|
||||
}
|
||||
}
|
||||
}
|
165
tracker/src/main/groovy/com/muwire/tracker/SwarmManager.groovy
Normal file
@@ -0,0 +1,165 @@
|
||||
package com.muwire.tracker
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.function.Function
|
||||
|
||||
import javax.annotation.PostConstruct
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.search.QueryEvent
|
||||
import com.muwire.core.search.SearchEvent
|
||||
import com.muwire.core.search.UIResultBatchEvent
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.crypto.DSAEngine
|
||||
import net.i2p.data.Signature
|
||||
|
||||
@Component
|
||||
@Log
|
||||
class SwarmManager {
|
||||
@Autowired
|
||||
private Core core
|
||||
|
||||
@Autowired
|
||||
private Pinger pinger
|
||||
|
||||
@Autowired
|
||||
private TrackerProperties trackerProperties
|
||||
|
||||
private final Map<InfoHash, Swarm> swarms = new ConcurrentHashMap<>()
|
||||
private final Map<UUID, InfoHash> queries = new ConcurrentHashMap<>()
|
||||
private final Timer swarmTimer = new Timer("swarm-timer",true)
|
||||
|
||||
@PostConstruct
|
||||
public void postConstruct() {
|
||||
core.eventBus.register(UIResultBatchEvent.class, this)
|
||||
swarmTimer.schedule({trackSwarms()} as TimerTask, 10 * 1000, 10 * 1000)
|
||||
}
|
||||
|
||||
void onUIResultBatchEvent(UIResultBatchEvent e) {
|
||||
InfoHash stored = queries.get(e.uuid)
|
||||
InfoHash ih = e.results[0].infohash
|
||||
|
||||
if (ih != stored) {
|
||||
log.warning("infohash mismatch in result $ih vs $stored")
|
||||
return
|
||||
}
|
||||
|
||||
Swarm swarm = swarms.get(ih)
|
||||
if (swarm == null) {
|
||||
log.warning("no swarm found for result with infoHash $ih")
|
||||
return
|
||||
}
|
||||
|
||||
log.info("got a result with uuid ${e.uuid} for infoHash $ih")
|
||||
swarm.add(e.results[0].sender)
|
||||
}
|
||||
|
||||
int countSwarms() {
|
||||
swarms.size()
|
||||
}
|
||||
|
||||
private void trackSwarms() {
|
||||
final long now = System.currentTimeMillis()
|
||||
final long expiryCutoff = now - trackerProperties.getSwarmParameters().getExpiry() * 60 * 1000L
|
||||
final int maxFailures = trackerProperties.getSwarmParameters().getMaxFailures()
|
||||
swarms.values().each { it.expire(expiryCutoff, maxFailures) }
|
||||
final long queryCutoff = now - trackerProperties.getSwarmParameters().getQueryInterval() * 60 * 60 * 1000L
|
||||
swarms.values().each {
|
||||
if (it.shouldQuery(queryCutoff, now))
|
||||
query(it)
|
||||
}
|
||||
|
||||
List<Swarm> swarmList = new ArrayList<>(swarms.values())
|
||||
Collections.sort(swarmList,{Swarm x, Swarm y ->
|
||||
Long.compare(x.getLastPingTime(), y.getLastPingTime())
|
||||
} as Comparator<Swarm>)
|
||||
|
||||
List<HostAndIH> toPing = new ArrayList<>()
|
||||
final int amount = trackerProperties.getSwarmParameters().getPingParallel()
|
||||
final long pingCutoff = now - trackerProperties.getSwarmParameters().getPingInterval() * 60 * 1000L
|
||||
|
||||
for(int i = 0; i < swarmList.size() && toPing.size() < amount; i++) {
|
||||
Swarm s = swarmList.get(i)
|
||||
List<Host> hostsFromSwarm = s.getBatchToPing(amount - toPing.size(), now, pingCutoff)
|
||||
hostsFromSwarm.collect(toPing, { host -> new HostAndIH(host, s.getInfoHash())})
|
||||
}
|
||||
|
||||
log.info("will ping $toPing")
|
||||
|
||||
toPing.each { pinger.ping(it, now) }
|
||||
}
|
||||
|
||||
private void query(Swarm swarm) {
|
||||
InfoHash infoHash = swarm.getInfoHash()
|
||||
cleanQueryMap(infoHash)
|
||||
UUID uuid = UUID.randomUUID()
|
||||
queries.put(uuid, infoHash)
|
||||
|
||||
log.info("will query MW network for $infoHash with uuid $uuid")
|
||||
|
||||
def searchEvent = new SearchEvent(searchHash : infoHash.getRoot(), uuid: uuid, oobInfohash: true, compressedResults : true, persona : core.me)
|
||||
byte [] payload = infoHash.getRoot()
|
||||
boolean firstHop = core.muOptions.allowUntrusted || core.muOptions.searchExtraHop
|
||||
|
||||
Signature sig = DSAEngine.getInstance().sign(payload, core.spk)
|
||||
long timestamp = System.currentTimeMillis()
|
||||
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : firstHop,
|
||||
replyTo: core.me.destination, receivedOn: core.me.destination,
|
||||
originator : core.me, sig : sig.data, queryTime : timestamp, sig2 : DataUtil.signUUID(uuid, timestamp, core.spk)))
|
||||
}
|
||||
|
||||
void track(InfoHash infoHash) {
|
||||
swarms.computeIfAbsent(infoHash, {new Swarm(it)} as Function)
|
||||
}
|
||||
|
||||
boolean forget(InfoHash infoHash) {
|
||||
Swarm swarm = swarms.remove(infoHash)
|
||||
if (swarm != null) {
|
||||
cleanQueryMap(infoHash)
|
||||
return true
|
||||
} else
|
||||
return false
|
||||
}
|
||||
|
||||
private void cleanQueryMap(InfoHash infoHash) {
|
||||
queries.values().removeAll {it == infoHash}
|
||||
}
|
||||
|
||||
Swarm.Info info(InfoHash infoHash) {
|
||||
swarms.get(infoHash)?.info()
|
||||
}
|
||||
|
||||
void fail(HostAndIH target) {
|
||||
log.info("failing $target")
|
||||
swarms.get(target.infoHash)?.fail(target.host)
|
||||
}
|
||||
|
||||
void handleResponse(HostAndIH target, int code, Set<Persona> altlocs) {
|
||||
Swarm swarm = swarms.get(target.infoHash)
|
||||
swarm?.handleResponse(target.host, code)
|
||||
altlocs.each {
|
||||
swarm?.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
public static class HostAndIH {
|
||||
final Host host
|
||||
final InfoHash infoHash
|
||||
HostAndIH(Host host, InfoHash infoHash) {
|
||||
this.host = host
|
||||
this.infoHash = infoHash
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
"$host:$infoHash"
|
||||
}
|
||||
}
|
||||
}
|
10
tracker/src/main/groovy/com/muwire/tracker/TrackRequest.java
Normal file
@@ -0,0 +1,10 @@
|
||||
package com.muwire.tracker;
|
||||
|
||||
public class TrackRequest {
|
||||
String infoHash;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "infoHash: " +infoHash;
|
||||
}
|
||||
}
|
119
tracker/src/main/groovy/com/muwire/tracker/Tracker.groovy
Normal file
@@ -0,0 +1,119 @@
|
||||
package com.muwire.tracker
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
import org.springframework.boot.SpringApplication
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.web.server.ConfigurableWebServerFactory
|
||||
import org.springframework.boot.web.server.WebServerFactoryCustomizer
|
||||
import org.springframework.context.annotation.Bean
|
||||
|
||||
import com.googlecode.jsonrpc4j.spring.JsonServiceExporter
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.UILoadedEvent
|
||||
import com.muwire.core.files.AllFilesLoadedEvent
|
||||
|
||||
@SpringBootApplication
|
||||
class Tracker {
|
||||
|
||||
private static final String VERSION = System.getProperty("build.version")
|
||||
|
||||
private static Core core
|
||||
private static TrackerService trackerService
|
||||
|
||||
public static void main(String [] args) {
|
||||
println "Launching MuWire Tracker version $VERSION"
|
||||
|
||||
File home = new File(System.getProperty("user.home"))
|
||||
home = new File(home, ".mwtrackerd")
|
||||
home.mkdir()
|
||||
|
||||
File mwProps = new File(home, "MuWire.properties")
|
||||
File i2pProps = new File(home, "i2p.properties")
|
||||
File trackerProps = new File(home, "tracker.properties")
|
||||
|
||||
boolean launchSetup = false
|
||||
|
||||
if (args.length > 0 && args[0] == "setup") {
|
||||
println "Setup requested, entering setup wizard"
|
||||
launchSetup = true
|
||||
} else if (!(mwProps.exists() && i2pProps.exists() && trackerProps.exists())) {
|
||||
println "Config files not found, entering setup wizard"
|
||||
launchSetup = true
|
||||
}
|
||||
|
||||
if (launchSetup) {
|
||||
SetupWizard wizard = new SetupWizard(home)
|
||||
Properties props = wizard.performSetup()
|
||||
|
||||
// nickname goes to mw.props
|
||||
MuWireSettings mwSettings = new MuWireSettings()
|
||||
mwSettings.nickname = props['nickname']
|
||||
|
||||
mwProps.withPrintWriter("UTF-8", {
|
||||
mwSettings.write(it)
|
||||
})
|
||||
|
||||
// i2cp host & port go in i2p.properties
|
||||
def i2cpProps = new Properties()
|
||||
i2cpProps['i2cp.tcp.port'] = props['i2cp.tcp.port']
|
||||
i2cpProps['i2cp.tcp.host'] = props['i2cp.tcp.host']
|
||||
i2cpProps['inbound.nickname'] = "MuWire Tracker"
|
||||
i2cpProps['outbound.nickname'] = "MuWire Tracker"
|
||||
|
||||
i2pProps.withPrintWriter { i2cpProps.store(it, "") }
|
||||
|
||||
// json rcp props go in tracker.properties
|
||||
def jsonProps = new Properties()
|
||||
jsonProps['tracker.jsonRpc.iface'] = props['jsonrpc.iface']
|
||||
jsonProps['tracker.jsonRpc.port'] = props['jsonrpc.port']
|
||||
|
||||
trackerProps.withPrintWriter { jsonProps.store(it, "") }
|
||||
}
|
||||
|
||||
Properties p = new Properties()
|
||||
mwProps.withReader("UTF-8", { p.load(it) } )
|
||||
MuWireSettings muSettings = new MuWireSettings(p)
|
||||
p = new Properties()
|
||||
trackerProps.withInputStream { p.load(it) }
|
||||
|
||||
core = new Core(muSettings, home, VERSION)
|
||||
|
||||
|
||||
// init json service object
|
||||
trackerService = new TrackerServiceImpl(core)
|
||||
core.eventBus.with {
|
||||
register(UILoadedEvent.class, trackerService)
|
||||
}
|
||||
|
||||
Thread coreStarter = new Thread({
|
||||
core.startServices()
|
||||
core.eventBus.publish(new UILoadedEvent())
|
||||
} as Runnable)
|
||||
coreStarter.start()
|
||||
|
||||
System.setProperty("spring.config.location", trackerProps.getAbsolutePath())
|
||||
SpringApplication.run(Tracker.class, args)
|
||||
}
|
||||
|
||||
@Bean
|
||||
Core core() {
|
||||
core
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TrackerService trackerService() {
|
||||
trackerService
|
||||
}
|
||||
|
||||
@Bean(name = '/tracker')
|
||||
public JsonServiceExporter jsonServiceExporter() {
|
||||
def exporter = new JsonServiceExporter()
|
||||
exporter.setService(trackerService())
|
||||
exporter.setServiceInterface(TrackerService.class)
|
||||
exporter
|
||||
}
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
package com.muwire.tracker
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties("tracker")
|
||||
class TrackerProperties {
|
||||
|
||||
final JsonRpc jsonRpc = new JsonRpc()
|
||||
|
||||
public static class JsonRpc {
|
||||
InetAddress iface
|
||||
int port
|
||||
}
|
||||
|
||||
final SwarmParameters swarmParameters = new SwarmParameters()
|
||||
|
||||
public static class SwarmParameters {
|
||||
/** how often to kick of queries on the MW net, in hours */
|
||||
int queryInterval = 1
|
||||
/** how many hosts to ping in parallel */
|
||||
int pingParallel = 5
|
||||
/** interval of time between pinging the same host, in minutes */
|
||||
int pingInterval = 15
|
||||
/** how long to wait before declaring a host is dead, in minutes */
|
||||
int expiry = 60
|
||||
/** how long to wait for a host to respond to a ping, in seconds */
|
||||
int pingTimeout = 20
|
||||
/** Do not expire a host until it has failed this many times */
|
||||
int maxFailures = 3
|
||||
}
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
package com.muwire.tracker;
|
||||
|
||||
public interface TrackerService {
|
||||
public TrackerStatus status();
|
||||
public void track(String infoHash);
|
||||
public boolean forget(String infoHash);
|
||||
public Swarm.Info info(String infoHash);
|
||||
}
|
@@ -0,0 +1,54 @@
|
||||
package com.muwire.tracker
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.UILoadedEvent
|
||||
|
||||
import net.i2p.data.Base64
|
||||
|
||||
@Component
|
||||
class TrackerServiceImpl implements TrackerService {
|
||||
|
||||
private final TrackerStatus status = new TrackerStatus()
|
||||
private final Core core
|
||||
|
||||
@Autowired
|
||||
private SwarmManager swarmManager
|
||||
|
||||
TrackerServiceImpl(Core core) {
|
||||
this.core = core
|
||||
status.status = "Starting"
|
||||
}
|
||||
|
||||
public TrackerStatus status() {
|
||||
status.connections = core.getConnectionManager().getConnections().size()
|
||||
status.swarms = swarmManager.countSwarms()
|
||||
status
|
||||
}
|
||||
|
||||
|
||||
void onUILoadedEvent(UILoadedEvent e) {
|
||||
status.status = "Running"
|
||||
}
|
||||
|
||||
@Override
|
||||
public void track(String infoHash) {
|
||||
InfoHash ih = new InfoHash(Base64.decode(infoHash))
|
||||
swarmManager.track(ih)
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean forget(String infoHash) {
|
||||
InfoHash ih = new InfoHash(Base64.decode(infoHash))
|
||||
swarmManager.forget(ih)
|
||||
}
|
||||
|
||||
@Override
|
||||
public Swarm.Info info(String infoHash) {
|
||||
InfoHash ih = new InfoHash(Base64.decode(infoHash))
|
||||
swarmManager.info(ih)
|
||||
}
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
package com.muwire.tracker
|
||||
|
||||
class TrackerStatus {
|
||||
volatile String status
|
||||
int connections
|
||||
int swarms
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
package com.muwire.tracker
|
||||
|
||||
import org.springframework.boot.web.server.ConfigurableWebServerFactory
|
||||
import org.springframework.boot.web.server.WebServerFactoryCustomizer
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class WebServerConfiguration implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
|
||||
|
||||
private final TrackerProperties trackerProperties
|
||||
|
||||
WebServerConfiguration(TrackerProperties trackerProperties) {
|
||||
this.trackerProperties = trackerProperties;
|
||||
}
|
||||
@Override
|
||||
public void customize(ConfigurableWebServerFactory factory) {
|
||||
factory.setAddress(trackerProperties.jsonRpc.getIface())
|
||||
factory.setPort(trackerProperties.jsonRpc.port)
|
||||
}
|
||||
}
|
@@ -66,6 +66,7 @@ task generateWebXML {
|
||||
def jasper = new File("$buildDir/tmp_jsp/web.xml.jasper")
|
||||
templateText = templateText.replaceAll("__JASPER__", jasper.text)
|
||||
templateText = templateText.replaceAll("__VERSION__", project.version)
|
||||
templateText = templateText.replaceAll("__BUILD_NUMBER__", project.buildNumber)
|
||||
def webXml = new File("$buildDir/tmp_jsp/web.xml")
|
||||
webXml.text = templateText
|
||||
}
|
||||
|
@@ -191,11 +191,21 @@ iframe {
|
||||
padding: 0 0 22px 28px;
|
||||
width: 300px;
|
||||
}
|
||||
.title-and-help {
|
||||
display: inline-block;
|
||||
}
|
||||
.pagetitle {
|
||||
display: inline-block;
|
||||
font-size: 2em;
|
||||
padding-left: 28px;
|
||||
}
|
||||
.pagehelp {
|
||||
font-size: 2em;
|
||||
padding-right: 28px;
|
||||
float: right;
|
||||
position: absolute;
|
||||
right:0;
|
||||
}
|
||||
.password {
|
||||
height: 24px;
|
||||
position: absolute;
|
||||
@@ -323,28 +333,43 @@ See also .menu-icon
|
||||
font-weight: 300;
|
||||
margin: 0 24px 0 48px;
|
||||
}
|
||||
.menuitem.identities .menu-icon:before {
|
||||
content: url("images/identities.png");
|
||||
}
|
||||
.menuitem.address-book .menu-icon:before {
|
||||
content: url("images/addressbook.png");
|
||||
}
|
||||
|
||||
.menuitem.settings .menu-icon:before {
|
||||
content: url("images/settings.png");
|
||||
content: url("images/ConfigurationPage.png");
|
||||
}
|
||||
.menuitem.advancedSharing .menu-icon:before {
|
||||
content: url("images/AdvancedSharing.png");
|
||||
}
|
||||
.menuitem.downloads .menu-icon:before {
|
||||
content: url("images/inbox.png");
|
||||
content: url("images/Downloads.png");
|
||||
}
|
||||
.menuitem.uploads .menu-icon:before {
|
||||
content: url("images/uploads.png");
|
||||
}
|
||||
.menuitem.search .menu-icon:before {
|
||||
content: url("images/delay.png");
|
||||
content: url("images/Search.png");
|
||||
}
|
||||
.menuitem.shared .menu-icon:before {
|
||||
content: url("images/folder.png");
|
||||
content: url("images/SharedFiles.png");
|
||||
}
|
||||
.menuitem.browse .menu-icon:before {
|
||||
content : url("images/BrowseHost.png")
|
||||
}
|
||||
.menuitem.feeds .menu-icon:before {
|
||||
content : url("images/Feeds.png")
|
||||
}
|
||||
.menuitem.trustUsers .menu-icon:before {
|
||||
content : url("images/TrustUsers.png")
|
||||
}
|
||||
.menuitem.trustList .menu-icon:before {
|
||||
content : url("images/TrustList.png")
|
||||
}
|
||||
.menuitem.aboutMe .menu-icon:before {
|
||||
content : url("images/AboutMe.png")
|
||||
}
|
||||
.menuitem.muStatus .menu-icon:before {
|
||||
content : url("images/MuStatus.png")
|
||||
}
|
||||
|
||||
|
||||
/* Main content */
|
||||
|
||||
@@ -463,7 +488,7 @@ table td, th {
|
||||
padding-top: 3px;
|
||||
background: white;
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
overflow: hidden;
|
||||
font-size: 1em;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
@@ -1,3 +1,10 @@
|
||||
:root {
|
||||
--hover-menu-bg : #c8e0ff;
|
||||
--hover-menu-link-bg : #d8f0ff;
|
||||
|
||||
--table-bg : #ceeee8;
|
||||
}
|
||||
|
||||
#table-wrapper {
|
||||
position:relative;
|
||||
}
|
||||
@@ -6,14 +13,31 @@
|
||||
overflow:auto;
|
||||
margin-top:20px;
|
||||
}
|
||||
.paddedTable {
|
||||
padding-bottom: 6%;
|
||||
}
|
||||
.paddedTable table tbody tr td .dropdown .dropdown-content {
|
||||
background: var(--hover-menu-bg);
|
||||
}
|
||||
|
||||
.paddedTable table tbody tr td .dropdown .dropdown-content a:hover {
|
||||
background: var(--hover-menu-link-bg);
|
||||
}
|
||||
|
||||
#table-wrapper table {
|
||||
width:100%;
|
||||
|
||||
}
|
||||
#table-wrapper table * {
|
||||
background: #ceeee8;
|
||||
#table-wrapper table tbody tr td {
|
||||
background: var(--table-bg);
|
||||
color:black;
|
||||
}
|
||||
|
||||
#table-wrapper table thead tr th {
|
||||
background: var(--table-bg);
|
||||
color:black;
|
||||
}
|
||||
|
||||
#table-wrapper table td, th {
|
||||
padding-right: 10px;
|
||||
padding-bottom: 1px;
|
||||
@@ -32,6 +56,9 @@ div#activeSearches table td:nth-child(2) {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
div#topTableSender table thead th:nth-child(1) {
|
||||
width: 45%;
|
||||
}
|
||||
div#topTableSender table thead th:nth-child(2) {
|
||||
width: 100px;
|
||||
}
|
||||
@@ -39,7 +66,10 @@ div#topTableSender table thead th:nth-child(3) {
|
||||
width: 100px;
|
||||
}
|
||||
div#topTableSender table thead th:nth-child(4) {
|
||||
width: 340px;
|
||||
width: 100px;
|
||||
}
|
||||
div#topTableSender table thead th:nth-child(5) {
|
||||
width: 20%;
|
||||
}
|
||||
div#topTableSender table tbody td:nth-child(1) {
|
||||
text-overflow: ellipsis;
|
||||
@@ -51,6 +81,11 @@ div#topTableSender table tbody td:nth-child(3) {
|
||||
padding-right: 40px;
|
||||
text-align: right;
|
||||
}
|
||||
div#topTableSender table tbody td:nth-child(5) {
|
||||
text-overflow: ellipsis;
|
||||
overflow: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div#bottomTableSender table thead th:nth-child(2) {
|
||||
width: 100px;
|
||||
@@ -97,7 +132,10 @@ div#bottomTableFile table thead th:nth-child(2) {
|
||||
width: 100px;
|
||||
}
|
||||
div#bottomTableFile table thead th:nth-child(3) {
|
||||
width: 340px;
|
||||
width: 100px;
|
||||
}
|
||||
div#bottomTableFile table thead th:nth-child(4) {
|
||||
width: 20%;
|
||||
}
|
||||
div#bottomTableFile table tbody td:nth-child(1) {
|
||||
text-overflow: ellipsis;
|
||||
@@ -105,6 +143,10 @@ div#bottomTableFile table tbody td:nth-child(1) {
|
||||
div#bottomTableFile table tbody td:nth-child(2) {
|
||||
text-align: center;
|
||||
}
|
||||
div#bottomTableFile table tbody td:nth-child(4) {
|
||||
text-overflow:ellipsis;
|
||||
overflow:auto;
|
||||
}
|
||||
|
||||
div#activeBrowses table thead th:nth-child(2) {
|
||||
width: 100px;
|
||||
@@ -128,6 +170,7 @@ div#filesTable table thead th:nth-child(2) {
|
||||
}
|
||||
div#filesTable table tbody td:nth-child(1) {
|
||||
text-overflow: ellipsis;
|
||||
overflow: auto;
|
||||
}
|
||||
div#filesTable table tbody td:nth-child(2) {
|
||||
padding-right: 25px;
|
||||
@@ -196,6 +239,109 @@ div#downloadDetails table td {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
div#feedConfig table * {
|
||||
background: #bcd1e5 !important;
|
||||
}
|
||||
div#feedConfig table {
|
||||
border-collapse: collapse;
|
||||
width: auto;
|
||||
}
|
||||
div#feedConfig table td {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
div#feedsTable table thead th:nth-child(2) {
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
div#feedsTable table thead th:nth-child(3) {
|
||||
width: 170px;
|
||||
}
|
||||
|
||||
div#feedsTable table thead th:nth-child(4) {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
div#feedsTable table tbody td:nth-child(1) {
|
||||
text-overflow: ellipsis;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
div#itemsTable table thead th:nth-child(2) {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
div#itemsTable table thead th:nth-child(3) {
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
div#itemsTable table thead th:nth-child(4) {
|
||||
width: 170px;
|
||||
}
|
||||
|
||||
div#itemsTable table tbody td:nth-child(1) {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
div#dirConfig table * {
|
||||
background: #bcd1e5 !important;
|
||||
}
|
||||
div#dirConfig table {
|
||||
border-collapse: collapse;
|
||||
width: auto;
|
||||
}
|
||||
div#dirConfig table td {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
div#dirsTable table thead th:nth-child(2) {
|
||||
width: 80px;
|
||||
}
|
||||
div#dirsTable table thead th:nth-child(3) {
|
||||
width: 90px;
|
||||
}
|
||||
div#dirsTable table thead th:nth-child(4) {
|
||||
width: 170px;
|
||||
}
|
||||
div#dirsTable table tbody td:nth-child(1) {
|
||||
text-overflow: ellipsis;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
div#hitsTable table thead th:nth-child(1) {
|
||||
width: 270px;
|
||||
}
|
||||
|
||||
div#hitsTable table thead th:nth-child(2) {
|
||||
width: 170px;
|
||||
}
|
||||
|
||||
div#hitsTable table tbody td:nth-child(1) {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
div#hitsTable table tbody td:nth-child(3) {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
div#certificatesTable table thead th:nth-child(2) {
|
||||
width: 170px;
|
||||
}
|
||||
|
||||
div#certificatesTable table tbody td:nth-child(1) {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
div#certificatesTable table tbody td:nth-child(3) {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
div#uploads table thead th:nth-child(2) {
|
||||
width: 120px;
|
||||
}
|
||||
@@ -237,6 +383,7 @@ div#trustedUsers table tbody td:nth-child(3) {
|
||||
}
|
||||
div#trustedUsers table tbody td:nth-child(1) {
|
||||
text-overflow: ellipsis;
|
||||
overflow: auto;
|
||||
}
|
||||
div#trustedUsers table tbody td:nth-child(3) {
|
||||
text-align: center;
|
||||
@@ -244,6 +391,7 @@ div#trustedUsers table tbody td:nth-child(3) {
|
||||
|
||||
div#distrustedUsers table tbody td:nth-child(1) {
|
||||
text-overflow: ellipsis;
|
||||
overflow: auto;
|
||||
}
|
||||
div#distrustedUsers table thead th:nth-child(2) {
|
||||
width: 300px;
|
||||
@@ -263,6 +411,7 @@ div#trustLists table thead th:nth-child(5) {
|
||||
}
|
||||
div#trustLists table tbody td:nth-child(1) {
|
||||
text-overflow: ellipsis;
|
||||
overflow: auto;
|
||||
}
|
||||
div#trustLists table tbody td:nth-child(2) {
|
||||
padding-right: 40px;
|
||||
@@ -284,6 +433,7 @@ div#trusted table thead th:nth-child(3) {
|
||||
}
|
||||
div#trusted table tbody td:nth-child(1) {
|
||||
text-overflow: ellipsis;
|
||||
overflow: auto;
|
||||
}
|
||||
div#trusted table tbody td:nth-child(3) {
|
||||
text-align: center;
|
||||
@@ -296,6 +446,7 @@ div#distrusted table thead th:nth-child(3) {
|
||||
}
|
||||
div#distrusted table tbody td:nth-child(1) {
|
||||
text-overflow: ellipsis;
|
||||
overflow: auto;
|
||||
}
|
||||
div#distrusted table tbody td:nth-child(3) {
|
||||
text-align: center;
|
||||
@@ -355,6 +506,11 @@ span.right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
span.center {
|
||||
display : inline-block;
|
||||
text-align : center;
|
||||
}
|
||||
|
||||
input.right {
|
||||
text-align: right;
|
||||
}
|
||||
@@ -370,6 +526,10 @@ pre.comment {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
pre.fullId {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* File tree CSS */
|
||||
|
||||
/* Remove default bullets */
|
||||
@@ -413,3 +573,129 @@ ul, #sharedTree {
|
||||
.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
ul.fileTree {
|
||||
margin-left: -1.5%;
|
||||
}
|
||||
|
||||
li.fileTree {
|
||||
|
||||
}
|
||||
|
||||
.accordion {
|
||||
cursor: pointer;
|
||||
transition: 0.4s;
|
||||
}
|
||||
|
||||
.panel {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height: 0.2s ease-out;
|
||||
}
|
||||
|
||||
.droplink {
|
||||
cursor : pointer;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
display: none;
|
||||
position: absolute;
|
||||
z-index:1;
|
||||
background: var(--hover-menu-bg);
|
||||
background-color: var(--hover-menu-bg);
|
||||
padding: 3px 14px 3px 14px;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.dropdown-content-right {
|
||||
display: none;
|
||||
position: absolute;
|
||||
z-index:1;
|
||||
background: var(--hover-menu-bg);
|
||||
background-color: var(--hover-menu-bg);
|
||||
padding: 3px 14px 3px 14px;
|
||||
width: max-content;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.dropdown-content a {
|
||||
color: black;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown-content-right a {
|
||||
color: black;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Change color of dropdown links on hover */
|
||||
.dropdown-content a:hover {
|
||||
background: var(--hover-menu-link-bg);
|
||||
background-color: var(--hover-menu-link-bg);
|
||||
}
|
||||
|
||||
.dropdown-content-right a:hover {
|
||||
background: var(--hover-menu-link-bg);
|
||||
background-color: var(--hover-menu-link-bg);
|
||||
}
|
||||
|
||||
/* Show the dropdown menu on hover */
|
||||
.dropdown:hover .dropdown-content {display: block;}
|
||||
.dropdown:hover .dropdown-content-right {display: block;}
|
||||
|
||||
textarea.copypaste {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
z-index: -9999;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
border-bottom: 1px dotted black;
|
||||
}
|
||||
|
||||
.tooltip .tooltiptext {
|
||||
visibility: hidden;
|
||||
width: 240px;
|
||||
text-align: center;
|
||||
border-radius: 6px;
|
||||
padding: 5px 0;
|
||||
background : #d8f0ff;
|
||||
|
||||
/* Position the tooltip */
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.tooltip:hover .tooltiptext {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.configuration-section table tr td {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.configuration-section .tooltip {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.configuration-section .tooltip .tooltiptext {
|
||||
white-space: pre-wrap;
|
||||
padding : 5px 0 5px 5px;
|
||||
}
|
||||
|
||||
.title-and-help .pagehelp .tooltip .tooltiptext {
|
||||
white-space: pre-wrap;
|
||||
right : 0;
|
||||
top:20px;
|
||||
width: 400px;
|
||||
font-size: initial;
|
||||
color: initial;
|
||||
}
|
BIN
webui/src/main/images/AboutMe.png
Normal file
After Width: | Height: | Size: 849 B |