Compare commits
95 Commits
muwire-0.6
...
muwire-0.6
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -2,7 +2,7 @@
|
||||
**/.settings
|
||||
**/build
|
||||
.gradle
|
||||
.project
|
||||
.classpath
|
||||
**/.project
|
||||
**/.classpath
|
||||
**/*.rej
|
||||
**/*.orig
|
||||
|
@@ -51,6 +51,9 @@ 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
|
||||
|
||||
## 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.
|
||||
|
||||
## GPG Fingerprint
|
||||
|
||||
```
|
||||
@@ -69,3 +72,4 @@ 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
|
||||
|
1
TODO.md
1
TODO.md
@@ -20,6 +20,7 @@ This helps with scalability
|
||||
* Ability to share trust list only with trusted users
|
||||
* Confidential files visible only to certain users
|
||||
* 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
|
||||
|
13
build.gradle
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.12"
|
||||
private static final String MW_VERSION = "0.6.14"
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
@@ -55,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
|
||||
@@ -81,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
|
||||
@@ -108,7 +113,7 @@ 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
|
||||
@@ -130,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
|
||||
|
||||
@@ -157,12 +165,12 @@ public class Core {
|
||||
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
|
||||
@@ -365,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)
|
||||
@@ -385,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)
|
||||
@@ -411,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() {
|
||||
@@ -427,6 +455,7 @@ public class Core {
|
||||
updateClient?.start()
|
||||
feedManager.start()
|
||||
feedClient.start()
|
||||
trackerResponder.start()
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
@@ -454,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")
|
||||
@@ -464,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")
|
||||
@@ -511,7 +544,7 @@ public class Core {
|
||||
}
|
||||
}
|
||||
|
||||
Core core = new Core(props, home, "0.6.12")
|
||||
Core core = new Core(props, home, "0.6.14")
|
||||
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"))
|
||||
@@ -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))
|
||||
|
@@ -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())
|
||||
}
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@ 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<>()
|
||||
|
@@ -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,7 +136,7 @@ class DirectoryWatcher {
|
||||
SharedFile sf = fileManager.fileToSharedFile.get(f)
|
||||
if (sf != null)
|
||||
eventBus.publish(new FileUnsharedEvent(unsharedFile : sf, deleted : true))
|
||||
else if (muOptions.watchedDirectories.contains(f.toString()))
|
||||
else if (watchedDirectoryManager.isWatched(f))
|
||||
eventBus.publish(new DirectoryUnsharedEvent(directory : f, deleted : true))
|
||||
else
|
||||
log.fine("Entry was not relevant");
|
||||
@@ -153,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
|
||||
}
|
||||
}
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ 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 byte FILE_CERT_VERSION = (byte)2;
|
||||
public static final int CHAT_VERSION = 1;
|
||||
|
||||
@@ -17,5 +18,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();
|
||||
|
@@ -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,11 @@ public class DataUtil {
|
||||
Signature sig = DSAEngine.getInstance().sign(payload, spk);
|
||||
return sig.getData();
|
||||
}
|
||||
|
||||
public static boolean isValidName(String name) {
|
||||
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,5 +1,5 @@
|
||||
group = com.muwire
|
||||
version = 0.6.12
|
||||
version = 0.6.14
|
||||
i2pVersion = 0.9.45
|
||||
groovyVersion = 2.4.15
|
||||
slf4jVersion = 1.7.25
|
||||
@@ -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.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,8 @@ 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} 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
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
|
||||
}
|
||||
}
|
@@ -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()
|
||||
|
@@ -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
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,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
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
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
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
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
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
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
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
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
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)
|
||||
}
|
||||
}
|
@@ -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;
|
||||
@@ -327,6 +337,9 @@ See also .menu-icon
|
||||
.menuitem.settings .menu-icon:before {
|
||||
content: url("images/ConfigurationPage.png");
|
||||
}
|
||||
.menuitem.advancedSharing .menu-icon:before {
|
||||
content: url("images/AdvancedSharing.png");
|
||||
}
|
||||
.menuitem.downloads .menu-icon:before {
|
||||
content: url("images/Downloads.png");
|
||||
}
|
||||
|
@@ -1,3 +1,10 @@
|
||||
:root {
|
||||
--hover-menu-bg : #c8e0ff;
|
||||
--hover-menu-link-bg : #d8f0ff;
|
||||
|
||||
--table-bg : #ceeee8;
|
||||
}
|
||||
|
||||
#table-wrapper {
|
||||
position:relative;
|
||||
}
|
||||
@@ -9,14 +16,28 @@
|
||||
.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;
|
||||
@@ -36,7 +57,7 @@ div#activeSearches table td:nth-child(2) {
|
||||
}
|
||||
|
||||
div#topTableSender table thead th:nth-child(1) {
|
||||
width: 35%;
|
||||
width: 45%;
|
||||
}
|
||||
div#topTableSender table thead th:nth-child(2) {
|
||||
width: 100px;
|
||||
@@ -48,7 +69,7 @@ div#topTableSender table thead th:nth-child(4) {
|
||||
width: 100px;
|
||||
}
|
||||
div#topTableSender table thead th:nth-child(5) {
|
||||
width: 35%;
|
||||
width: 20%;
|
||||
}
|
||||
div#topTableSender table tbody td:nth-child(1) {
|
||||
text-overflow: ellipsis;
|
||||
@@ -60,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;
|
||||
@@ -109,7 +135,7 @@ div#bottomTableFile table thead th:nth-child(3) {
|
||||
width: 100px;
|
||||
}
|
||||
div#bottomTableFile table thead th:nth-child(4) {
|
||||
width: 340px;
|
||||
width: 20%;
|
||||
}
|
||||
div#bottomTableFile table tbody td:nth-child(1) {
|
||||
text-overflow: ellipsis;
|
||||
@@ -117,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;
|
||||
@@ -256,6 +286,34 @@ 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;
|
||||
}
|
||||
@@ -325,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;
|
||||
@@ -332,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;
|
||||
@@ -351,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;
|
||||
@@ -372,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;
|
||||
@@ -384,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;
|
||||
@@ -443,6 +506,11 @@ span.right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
span.center {
|
||||
display : inline-block;
|
||||
text-align : center;
|
||||
}
|
||||
|
||||
input.right {
|
||||
text-align: right;
|
||||
}
|
||||
@@ -538,21 +606,47 @@ li.fileTree {
|
||||
display: none;
|
||||
position: absolute;
|
||||
z-index:1;
|
||||
background: #f5f5f5;
|
||||
padding-left: 20px;
|
||||
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-color: #ddd;}
|
||||
.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;
|
||||
@@ -560,3 +654,48 @@ textarea.copypaste {
|
||||
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/AdvancedSharing.png
Normal file
BIN
webui/src/main/images/AdvancedSharing.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 772 B |
@@ -0,0 +1,53 @@
|
||||
package com.muwire.webui;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import com.muwire.core.Core;
|
||||
import com.muwire.core.files.DirectoryUnsharedEvent;
|
||||
import com.muwire.core.files.DirectoryWatchedEvent;
|
||||
import com.muwire.core.files.directories.UISyncDirectoryEvent;
|
||||
import com.muwire.core.files.directories.WatchedDirectoryConfigurationEvent;
|
||||
import com.muwire.core.files.directories.WatchedDirectorySyncEvent;
|
||||
|
||||
public class AdvancedSharingManager {
|
||||
|
||||
private final Core core;
|
||||
private volatile long revision;
|
||||
|
||||
public AdvancedSharingManager(Core core) {
|
||||
this.core = core;
|
||||
}
|
||||
|
||||
public long getRevision() {
|
||||
return revision;
|
||||
}
|
||||
|
||||
public void onDirectoryWatchedEvent(DirectoryWatchedEvent e) {
|
||||
revision++;
|
||||
}
|
||||
|
||||
public void onDirectoryUnsharedEvent(DirectoryUnsharedEvent e) {
|
||||
revision++;
|
||||
}
|
||||
|
||||
public void onWatchedDirectorySyncEvent(WatchedDirectorySyncEvent e) {
|
||||
revision++;
|
||||
}
|
||||
|
||||
|
||||
void sync(File dir) {
|
||||
revision++;
|
||||
UISyncDirectoryEvent event = new UISyncDirectoryEvent();
|
||||
event.setDirectory(dir);
|
||||
core.getEventBus().publish(event);
|
||||
}
|
||||
|
||||
void configure(File dir, boolean autoWatch, int syncInterval) {
|
||||
revision++;
|
||||
WatchedDirectoryConfigurationEvent event = new WatchedDirectoryConfigurationEvent();
|
||||
event.setAutoWatch(autoWatch);
|
||||
event.setDirectory(dir);
|
||||
event.setSyncInterval(syncInterval);
|
||||
core.getEventBus().publish(event);
|
||||
}
|
||||
}
|
143
webui/src/main/java/com/muwire/webui/AdvancedSharingServlet.java
Normal file
143
webui/src/main/java/com/muwire/webui/AdvancedSharingServlet.java
Normal file
@@ -0,0 +1,143 @@
|
||||
package com.muwire.webui;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.text.Collator;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.servlet.ServletConfig;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import com.muwire.core.Core;
|
||||
import com.muwire.core.files.directories.WatchedDirectory;
|
||||
|
||||
import net.i2p.data.DataHelper;
|
||||
|
||||
public class AdvancedSharingServlet extends HttpServlet {
|
||||
|
||||
private Core core;
|
||||
private AdvancedSharingManager advancedSharingManager;
|
||||
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
|
||||
String section = req.getParameter("section");
|
||||
if (section == null) {
|
||||
resp.sendError(403, "Bad section param");
|
||||
return;
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("<?xml version='1.0' encoding='UTF-8'?>");
|
||||
|
||||
if (section.equals("revision")) {
|
||||
sb.append("<Revision>").append(advancedSharingManager.getRevision()).append("</Revision>");
|
||||
} else if (section.equals("dirs")) {
|
||||
List<WrappedDir> dirs = core.getWatchedDirectoryManager().getWatchedDirsStream().
|
||||
map(WrappedDir::new).
|
||||
collect(Collectors.toList());
|
||||
DIR_COMPARATORS.sort(dirs, req);
|
||||
|
||||
sb.append("<WatchedDirs>");
|
||||
dirs.forEach(d -> d.toXML(sb));
|
||||
sb.append("</WatchedDirs>");
|
||||
} else {
|
||||
resp.sendError(403, "Bad section param");
|
||||
return;
|
||||
}
|
||||
|
||||
resp.setContentType("text/xml");
|
||||
resp.setCharacterEncoding("UTF-8");
|
||||
resp.setDateHeader("Expires", 0);
|
||||
resp.setHeader("Pragma", "no-cache");
|
||||
resp.setHeader("Cache-Control", "no-store, max-age=0, no-cache, must-revalidate");
|
||||
byte[] out = sb.toString().getBytes("UTF-8");
|
||||
resp.setContentLength(out.length);
|
||||
resp.getOutputStream().write(out);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
|
||||
String action = req.getParameter("action");
|
||||
if (action == null) {
|
||||
resp.sendError(403,"Bad param");
|
||||
return;
|
||||
}
|
||||
String path = req.getParameter("path");
|
||||
if (path == null) {
|
||||
resp.sendError(403, "Bad param");
|
||||
return;
|
||||
}
|
||||
|
||||
File dir = Util.getFromPathElements(path);
|
||||
|
||||
if (action.equals("sync")) {
|
||||
advancedSharingManager.sync(dir);
|
||||
Util.pause();
|
||||
} else if (action.equals("configure")) {
|
||||
boolean autoWatch = Boolean.parseBoolean(req.getParameter("autoWatch"));
|
||||
int syncInterval = Integer.parseInt(req.getParameter("syncInterval"));
|
||||
advancedSharingManager.configure(dir, autoWatch, syncInterval);
|
||||
Util.pause();
|
||||
resp.sendRedirect("/MuWire/AdvancedSharing");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(ServletConfig config) throws ServletException {
|
||||
core = (Core) config.getServletContext().getAttribute("core");
|
||||
advancedSharingManager = (AdvancedSharingManager) config.getServletContext().getAttribute("advancedSharingManager");
|
||||
}
|
||||
|
||||
private static class WrappedDir {
|
||||
private final String directory;
|
||||
private final boolean autoWatch;
|
||||
private final long lastSync;
|
||||
private final int syncInterval;
|
||||
|
||||
WrappedDir(WatchedDirectory wd) {
|
||||
this.directory = wd.getDirectory().getAbsolutePath();
|
||||
this.autoWatch = wd.getAutoWatch();
|
||||
this.lastSync = wd.getLastSync();
|
||||
this.syncInterval = wd.getSyncInterval();
|
||||
}
|
||||
|
||||
void toXML(StringBuilder sb) {
|
||||
sb.append("<WatchedDir>");
|
||||
sb.append("<Directory>").append(Util.escapeHTMLinXML(directory)).append("</Directory>");
|
||||
sb.append("<AutoWatch>").append(autoWatch).append("</AutoWatch>");
|
||||
sb.append("<LastSync>").append(DataHelper.formatTime(lastSync)).append("</LastSync>");
|
||||
sb.append("<LastSyncTS>").append(lastSync).append("</LastSyncTS>");
|
||||
sb.append("<SyncInterval>").append(syncInterval).append("</SyncInterval>");
|
||||
sb.append("</WatchedDir>");
|
||||
}
|
||||
}
|
||||
|
||||
private static final Comparator<WrappedDir> BY_DIRECTORY = (l, r) -> {
|
||||
return Collator.getInstance().compare(l.directory, r.directory);
|
||||
};
|
||||
|
||||
private static final Comparator<WrappedDir> BY_AUTOWATCH = (l, r) -> {
|
||||
return Boolean.compare(l.autoWatch, r.autoWatch);
|
||||
};
|
||||
|
||||
private static final Comparator<WrappedDir> BY_LAST_SYNC = (l, r) -> {
|
||||
return Long.compare(l.lastSync, r.lastSync);
|
||||
};
|
||||
|
||||
private static final Comparator<WrappedDir> BY_SYNC_INTERVAL = (l, r) -> {
|
||||
return Integer.compare(l.syncInterval, r.syncInterval);
|
||||
};
|
||||
|
||||
private static final ColumnComparators<WrappedDir> DIR_COMPARATORS = new ColumnComparators<>();
|
||||
static {
|
||||
DIR_COMPARATORS.add("Directory", BY_DIRECTORY);
|
||||
DIR_COMPARATORS.add("Auto Watch", BY_AUTOWATCH);
|
||||
DIR_COMPARATORS.add("Last Sync", BY_LAST_SYNC);
|
||||
DIR_COMPARATORS.add("Sync Interval", BY_SYNC_INTERVAL);
|
||||
}
|
||||
}
|
@@ -82,6 +82,7 @@ public class ConfigurationServlet extends HttpServlet {
|
||||
core.getMuOptions().setAutoPublishSharedFiles(false);
|
||||
core.getMuOptions().setDefaultFeedAutoDownload(false);
|
||||
core.getMuOptions().setDefaultFeedSequential(false);
|
||||
core.getMuOptions().setAllowTracking(false);
|
||||
}
|
||||
|
||||
private void update(String name, String value) throws Exception {
|
||||
@@ -99,6 +100,7 @@ public class ConfigurationServlet extends HttpServlet {
|
||||
case "shareHiddenFiles" : core.getMuOptions().setShareHiddenFiles(true); break;
|
||||
case "searchComments" : core.getMuOptions().setSearchComments(true); break;
|
||||
case "browseFiles" : core.getMuOptions().setBrowseFiles(true); break;
|
||||
case "allowTracking" : core.getMuOptions().setAllowTracking(true); break;
|
||||
case "speedSmoothSeconds" : core.getMuOptions().setSpeedSmoothSeconds(Integer.parseInt(value)); break;
|
||||
case "inbound.length" : core.getI2pOptions().setProperty(name, value); break;
|
||||
case "inbound.quantity" : core.getI2pOptions().setProperty(name, value); break;
|
||||
|
@@ -134,14 +134,14 @@ public class FileManager {
|
||||
}
|
||||
|
||||
for (File directory : cb.directories) {
|
||||
if (core.getMuOptions().getWatchedDirectories().contains(directory.getAbsolutePath())) {
|
||||
if (core.getWatchedDirectoryManager().isWatched(directory)) {
|
||||
DirectoryUnsharedEvent e = new DirectoryUnsharedEvent();
|
||||
e.setDirectory(directory);
|
||||
core.getEventBus().publish(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (core.getMuOptions().getWatchedDirectories().contains(file.getAbsolutePath())) {
|
||||
if (core.getWatchedDirectoryManager().isWatched(file)) {
|
||||
DirectoryUnsharedEvent event = new DirectoryUnsharedEvent();
|
||||
event.setDirectory(file);
|
||||
core.getEventBus().publish(event);
|
||||
|
@@ -245,7 +245,7 @@ public class FilesServlet extends HttpServlet {
|
||||
|
||||
void toXML(StringBuilder sb) {
|
||||
String name = file.getName().isEmpty() ? file.toString() : file.getName();
|
||||
boolean shared = core.getMuOptions().getWatchedDirectories().contains(file.getAbsolutePath());
|
||||
boolean shared = core.getWatchedDirectoryManager().isWatched(file);
|
||||
sb.append("<Directory>");
|
||||
sb.append("<Name>").append(Util.escapeHTMLinXML(name)).append("</Name>");
|
||||
sb.append("<Shared>").append(shared).append("</Shared>");
|
||||
|
@@ -8,6 +8,9 @@ import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import com.muwire.core.Constants;
|
||||
import com.muwire.core.util.DataUtil;
|
||||
|
||||
public class InitServlet extends HttpServlet {
|
||||
|
||||
@Override
|
||||
@@ -17,6 +20,9 @@ public class InitServlet extends HttpServlet {
|
||||
if (nickname == null || nickname.trim().length() == 0)
|
||||
throw new Exception("Nickname cannot be blank");
|
||||
|
||||
if (!DataUtil.isValidName(nickname))
|
||||
throw new Exception("Nickname cannot contain any of " + Constants.INVALID_NICKNAME_CHARS);
|
||||
|
||||
String downloadLocation = req.getParameter("download_location");
|
||||
if (downloadLocation == null)
|
||||
throw new Exception("Download location cannot be blank");
|
||||
|
@@ -1,30 +1,35 @@
|
||||
package com.muwire.webui;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import com.muwire.core.Core;
|
||||
import com.muwire.core.MuWireSettings;
|
||||
import com.muwire.core.UILoadedEvent;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.router.RouterContext;
|
||||
|
||||
class MWStarter extends Thread {
|
||||
private final MuWireSettings settings;
|
||||
private final File home;
|
||||
private final String version;
|
||||
private final Core core;
|
||||
private final MuWireClient client;
|
||||
|
||||
MWStarter(MuWireSettings settings, File home, String version, MuWireClient client) {
|
||||
this.settings = settings;
|
||||
this.home = home;
|
||||
this.version = version;
|
||||
MWStarter(Core core, MuWireClient client) {
|
||||
this.core = core;
|
||||
this.client = client;
|
||||
setName("MW starter");
|
||||
setDaemon(true);
|
||||
}
|
||||
|
||||
public void run() {
|
||||
Core core = new Core(settings, home, version);
|
||||
client.setCore(core);
|
||||
RouterContext ctx = (RouterContext) I2PAppContext.getGlobalContext();
|
||||
|
||||
while(!ctx.clientManager().isAlive()) {
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
core.startServices();
|
||||
client.setCoreLoaded();
|
||||
core.getEventBus().publish(new UILoadedEvent());
|
||||
}
|
||||
}
|
||||
|
@@ -30,12 +30,14 @@ import com.muwire.core.filefeeds.FeedLoadedEvent;
|
||||
import com.muwire.core.filefeeds.UIFeedConfigurationEvent;
|
||||
import com.muwire.core.files.AllFilesLoadedEvent;
|
||||
import com.muwire.core.files.DirectoryUnsharedEvent;
|
||||
import com.muwire.core.files.DirectoryWatchedEvent;
|
||||
import com.muwire.core.files.FileDownloadedEvent;
|
||||
import com.muwire.core.files.FileHashedEvent;
|
||||
import com.muwire.core.files.FileHashingEvent;
|
||||
import com.muwire.core.files.FileLoadedEvent;
|
||||
import com.muwire.core.files.FileSharedEvent;
|
||||
import com.muwire.core.files.FileUnsharedEvent;
|
||||
import com.muwire.core.files.directories.WatchedDirectorySyncEvent;
|
||||
import com.muwire.core.search.BrowseStatusEvent;
|
||||
import com.muwire.core.search.UIResultBatchEvent;
|
||||
import com.muwire.core.search.UIResultEvent;
|
||||
@@ -60,6 +62,7 @@ public class MuWireClient {
|
||||
private final File mwProps;
|
||||
|
||||
private volatile Core core;
|
||||
private volatile boolean coreLoaded;
|
||||
|
||||
public MuWireClient(RouterContext ctx, String home, String version, ServletContext servletContext) {
|
||||
this.ctx = ctx;
|
||||
@@ -90,7 +93,9 @@ public class MuWireClient {
|
||||
reader.close();
|
||||
|
||||
MuWireSettings settings = new MuWireSettings(props);
|
||||
MWStarter starter = new MWStarter(settings, new File(home), version, this);
|
||||
Core core = new Core(settings, new File(home), version);
|
||||
setCore(core);
|
||||
MWStarter starter = new MWStarter(core, this);
|
||||
starter.start();
|
||||
}
|
||||
|
||||
@@ -102,6 +107,8 @@ public class MuWireClient {
|
||||
this.core = null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public boolean needsMWInit() {
|
||||
return !mwProps.exists();
|
||||
}
|
||||
@@ -177,6 +184,11 @@ public class MuWireClient {
|
||||
core.getEventBus().register(FeedFetchEvent.class, feedManager);
|
||||
core.getEventBus().register(FeedItemFetchedEvent.class, feedManager);
|
||||
|
||||
AdvancedSharingManager advancedSharingManager = new AdvancedSharingManager(core);
|
||||
core.getEventBus().register(DirectoryWatchedEvent.class, advancedSharingManager);
|
||||
core.getEventBus().register(DirectoryUnsharedEvent.class, advancedSharingManager);
|
||||
core.getEventBus().register(WatchedDirectorySyncEvent.class, advancedSharingManager);
|
||||
|
||||
servletContext.setAttribute("searchManager", searchManager);
|
||||
servletContext.setAttribute("downloadManager", downloadManager);
|
||||
servletContext.setAttribute("connectionCounter", connectionCounter);
|
||||
@@ -186,19 +198,22 @@ public class MuWireClient {
|
||||
servletContext.setAttribute("certificateManager", certificateManager);
|
||||
servletContext.setAttribute("uploadManager", uploadManager);
|
||||
servletContext.setAttribute("feedManager", feedManager);
|
||||
servletContext.setAttribute("advancedSharingManager", advancedSharingManager);
|
||||
}
|
||||
|
||||
public String getHome() {
|
||||
return home;
|
||||
}
|
||||
|
||||
void setCoreLoaded() {
|
||||
coreLoaded = true;
|
||||
}
|
||||
|
||||
public boolean isCoreLoaded() {
|
||||
return coreLoaded;
|
||||
}
|
||||
|
||||
public void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
|
||||
core.getMuOptions().getWatchedDirectories().stream().map(File::new).
|
||||
forEach(f -> {
|
||||
FileSharedEvent event = new FileSharedEvent();
|
||||
event.setFile(f);
|
||||
core.getEventBus().publish(event);
|
||||
});
|
||||
|
||||
core.getMuOptions().getTrustSubscriptions().forEach( p -> {
|
||||
TrustSubscriptionEvent event = new TrustSubscriptionEvent();
|
||||
|
@@ -21,17 +21,8 @@ public class MuWireServlet extends HttpServlet {
|
||||
@Override
|
||||
public void init(ServletConfig config) throws ServletException {
|
||||
super.init(config);
|
||||
RouterContext ctx = (RouterContext) I2PAppContext.getGlobalContext();
|
||||
|
||||
|
||||
while(!ctx.clientManager().isAlive()) {
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException e) {
|
||||
throw new ServletException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
RouterContext ctx = (RouterContext) I2PAppContext.getGlobalContext();
|
||||
|
||||
String home = ctx.getConfigDir()+File.separator+"plugins"+File.separator+"MuWire";
|
||||
version = config.getInitParameter("version");
|
||||
@@ -51,7 +42,7 @@ public class MuWireServlet extends HttpServlet {
|
||||
if (client.needsMWInit()) {
|
||||
resp.sendRedirect("/MuWire/MuWire");
|
||||
} else {
|
||||
if (client.getCore() == null) {
|
||||
if (!client.isCoreLoaded()) {
|
||||
resp.setContentType("text/html");
|
||||
resp.setCharacterEncoding("UTF-8");
|
||||
resp.getWriter().println("<html><head>\n" +
|
||||
|
53
webui/src/main/java/com/muwire/webui/SignServlet.java
Normal file
53
webui/src/main/java/com/muwire/webui/SignServlet.java
Normal file
@@ -0,0 +1,53 @@
|
||||
package com.muwire.webui;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import javax.servlet.ServletConfig;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import com.muwire.core.Core;
|
||||
|
||||
import net.i2p.crypto.DSAEngine;
|
||||
import net.i2p.data.Base64;
|
||||
import net.i2p.data.Signature;
|
||||
|
||||
public class SignServlet extends HttpServlet {
|
||||
|
||||
private Core core;
|
||||
|
||||
@Override
|
||||
public void init(ServletConfig config) throws ServletException {
|
||||
core = (Core) config.getServletContext().getAttribute("core");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
|
||||
String text = req.getParameter("text");
|
||||
if (text == null) {
|
||||
resp.sendError(503, "Nothing to sign?");
|
||||
return;
|
||||
}
|
||||
|
||||
byte [] payload = text.getBytes(StandardCharsets.UTF_8);
|
||||
Signature sig = DSAEngine.getInstance().sign(payload, core.getSpk());
|
||||
|
||||
String response = Base64.encode(sig.getData());
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("<?xml version='1.0' encoding='UTF-8'?>");
|
||||
sb.append("<Signed>").append(response).append("</Signed>");
|
||||
|
||||
resp.setContentType("text/xml");
|
||||
resp.setCharacterEncoding("UTF-8");
|
||||
resp.setDateHeader("Expires", 0);
|
||||
resp.setHeader("Pragma", "no-cache");
|
||||
resp.setHeader("Cache-Control", "no-store, max-age=0, no-cache, must-revalidate");
|
||||
byte[] out = sb.toString().getBytes("UTF-8");
|
||||
resp.setContentLength(out.length);
|
||||
resp.getOutputStream().write(out);
|
||||
}
|
||||
}
|
@@ -33,6 +33,7 @@ public class Util {
|
||||
_x("About Me"),
|
||||
_x("Actions"),
|
||||
_x("Active Sources"),
|
||||
_x("Auto-watch directory for changes using operating system"),
|
||||
_x("Browse"),
|
||||
_x("Browsing"),
|
||||
_x("Cancel"),
|
||||
@@ -45,6 +46,8 @@ public class Util {
|
||||
_x("Copy To Clipbaord"),
|
||||
_x("Default settings for new feeds"),
|
||||
_x("Details for {0}"),
|
||||
_x("Directory configuration for {0}"),
|
||||
_x("Directory sync frequency (seconds, 0 means never)"),
|
||||
_x("Distrusted"),
|
||||
_x("Distrusted User"),
|
||||
_x("Down"),
|
||||
@@ -85,6 +88,7 @@ public class Util {
|
||||
_x("MuWire Status"),
|
||||
_x("must be greater than zero"),
|
||||
_x("Name"),
|
||||
_x("Never"),
|
||||
_x("Number of items to keep on disk (-1 means unlimited)"),
|
||||
_x("Outgoing Connections"),
|
||||
// verb
|
||||
@@ -123,6 +127,7 @@ public class Util {
|
||||
_x("Submit"),
|
||||
_x("Subscribe"),
|
||||
_x("Subscribed"),
|
||||
_x("Sync"),
|
||||
_x("Times Browsed"),
|
||||
_x("Timestamp"),
|
||||
_x("Total Pieces"),
|
||||
|
29
webui/src/main/js/accordion.js
Normal file
29
webui/src/main/js/accordion.js
Normal file
@@ -0,0 +1,29 @@
|
||||
var openAccordion = 0;
|
||||
|
||||
function initAccordion() {
|
||||
var acc = document.getElementsByClassName("accordion");
|
||||
var i;
|
||||
|
||||
for (i = 0; i < acc.length; i++) {
|
||||
acc[i].addEventListener("click", function() {
|
||||
this.classList.toggle("active");
|
||||
var panel = this.nextElementSibling;
|
||||
if (panel.style.maxHeight) {
|
||||
panel.style.maxHeight = null;
|
||||
} else {
|
||||
panel.style.maxHeight = panel.scrollHeight + "px";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (openAccordion > 0) {
|
||||
acc[openAccordion - 1].classList.add("active");
|
||||
var panel = acc[openAccordion - 1].nextElementSibling;
|
||||
panel.style.maxHeight = panel.scrollHeight + "px";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
initAccordion();
|
||||
}, true);
|
153
webui/src/main/js/advancedSharing.js
Normal file
153
webui/src/main/js/advancedSharing.js
Normal file
@@ -0,0 +1,153 @@
|
||||
class Directory {
|
||||
constructor(xmlNode) {
|
||||
this.directory = xmlNode.getElementsByTagName("Directory")[0].childNodes[0].nodeValue
|
||||
this.path = Base64.encode(this.directory)
|
||||
this.autoWatch = xmlNode.getElementsByTagName("AutoWatch")[0].childNodes[0].nodeValue
|
||||
this.syncInterval = xmlNode.getElementsByTagName("SyncInterval")[0].childNodes[0].nodeValue
|
||||
this.lastSync = xmlNode.getElementsByTagName("LastSync")[0].childNodes[0].nodeValue
|
||||
this.lastSyncTS = parseInt(xmlNode.getElementsByTagName("LastSyncTS")[0].childNodes[0].nodeValue)
|
||||
}
|
||||
|
||||
getMapping() {
|
||||
var mapping = new Map()
|
||||
|
||||
var configLink = new Link(_t("Configure"), "configure", [this.path])
|
||||
var syncLink = new Link(_t("Sync"), "sync", [this.path])
|
||||
var syncHtml = syncLink.render()
|
||||
if (this.autoWatch == "true")
|
||||
syncHtml = ""
|
||||
var divRight = "<span class='right'><div class='dropdown'><a class='droplink'>" + _t("Actions") + "</a><div class='dropdown-content'>" +
|
||||
syncHtml + configLink.render() + "</div></div></span>"
|
||||
|
||||
mapping.set("Directory", this.directory + divRight)
|
||||
mapping.set("Auto Watch", this.autoWatch)
|
||||
|
||||
if (this.lastSyncTS == 0)
|
||||
mapping.set("Last Sync", _t("Never"))
|
||||
else
|
||||
mapping.set("Last Sync", this.lastSync)
|
||||
mapping.set("Sync Interval", this.syncInterval)
|
||||
|
||||
return mapping
|
||||
}
|
||||
}
|
||||
|
||||
function initAdvancedSharing() {
|
||||
setInterval(fetchRevision, 3000)
|
||||
setTimeout(fetchRevision, 1)
|
||||
}
|
||||
|
||||
function fetchRevision() {
|
||||
var xmlhttp = new XMLHttpRequest()
|
||||
xmlhttp.onreadystatechange = function() {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
var newRevision = parseInt(this.responseXML.getElementsByTagName("Revision")[0].childNodes[0].nodeValue)
|
||||
if (newRevision > revision) {
|
||||
revision = newRevision
|
||||
refreshDirs()
|
||||
}
|
||||
}
|
||||
}
|
||||
xmlhttp.open("GET", "/MuWire/AdvancedShare?section=revision", true)
|
||||
xmlhttp.send()
|
||||
}
|
||||
|
||||
function refreshDirs() {
|
||||
var xmlhttp = new XMLHttpRequest()
|
||||
xmlhttp.onreadystatechange = function() {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
pathToDir.clear()
|
||||
var listOfDirs = []
|
||||
var dirNodes = this.responseXML.getElementsByTagName("WatchedDir")
|
||||
var i
|
||||
for (i = 0; i < dirNodes.length; i ++) {
|
||||
var dir = new Directory(dirNodes[i])
|
||||
listOfDirs.push(dir)
|
||||
pathToDir.set(dir.path, dir)
|
||||
}
|
||||
|
||||
var newOrder
|
||||
if (sortOrder == "descending")
|
||||
newOrder = "ascending"
|
||||
else if (sortOrder == "ascending")
|
||||
newOrder = "descending"
|
||||
var table = new Table(["Directory", "Auto Watch", "Sync Interval", "Last Sync"], "sortDirs", sortKey, newOrder, null)
|
||||
|
||||
for (i = 0; i < listOfDirs.length; i++) {
|
||||
table.addRow(listOfDirs[i].getMapping())
|
||||
}
|
||||
|
||||
var dirsDiv = document.getElementById("dirsTable")
|
||||
if (listOfDirs.length > 0)
|
||||
dirsDiv.innerHTML = table.render()
|
||||
else
|
||||
dirsDiv.innerHTML = ""
|
||||
}
|
||||
}
|
||||
var sortParam = "&key=" + sortKey + "&order=" + sortOrder
|
||||
xmlhttp.open("GET", "/MuWire/AdvancedShare?section=dirs" + sortParam, true)
|
||||
xmlhttp.send()
|
||||
}
|
||||
|
||||
function sortDirs(key, order) {
|
||||
sortKey = key
|
||||
sortOrder = order
|
||||
refreshDirs()
|
||||
}
|
||||
|
||||
function configure(path) {
|
||||
var dir = pathToDir.get(path)
|
||||
|
||||
var html = "<form action='/MuWire/AdvancedShare' method='post'>"
|
||||
html += "<h3>" + _t("Directory configuration for {0}", dir.directory) + "</h3>"
|
||||
|
||||
html += "<table>"
|
||||
|
||||
html += "<tr>"
|
||||
html += "<td>" + _t("Auto-watch directory for changes using operating system") + "</td>"
|
||||
html += "<td><p align='right'><input type='checkbox' name='autoWatch' value='true'"
|
||||
if (dir.autoWatch == "true")
|
||||
html += " checked "
|
||||
html += "></p></td>"
|
||||
html += "</tr>"
|
||||
|
||||
html += "<tr>"
|
||||
html += "<td>" + _t("Directory sync frequency (seconds, 0 means never)") + "</td>"
|
||||
html += "<td><p align='right'><input type='text' size='3' name='syncInterval' value='" + dir.syncInterval + "'></p></td>"
|
||||
html += "</tr>"
|
||||
|
||||
html += "</table>"
|
||||
|
||||
html += "<input type='hidden' name='path' value='" + path + "'>"
|
||||
html += "<input type='hidden' name='action' value='configure'>"
|
||||
|
||||
html += "<input type='submit' value='" + _t("Save") + "'>"
|
||||
html += "<a href='#' onclick='window.cancelConfig();return false;'>" + _t("Cancel") + "</a>"
|
||||
html += "</form>"
|
||||
|
||||
var tableDiv = document.getElementById("dirConfig")
|
||||
tableDiv.innerHTML = html
|
||||
}
|
||||
|
||||
function cancelConfig() {
|
||||
var tableDiv = document.getElementById("dirConfig")
|
||||
tableDiv.innerHTML = ""
|
||||
}
|
||||
|
||||
function sync(path) {
|
||||
var xmlhttp = new XMLHttpRequest()
|
||||
xmlhttp.onreadystatechange = function() {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
refreshDirs()
|
||||
}
|
||||
}
|
||||
xmlhttp.open("POST", "/MuWire/AdvancedShare", true)
|
||||
xmlhttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
||||
xmlhttp.send("action=sync&path=" + path)
|
||||
}
|
||||
|
||||
var revision = -1
|
||||
var pathToDir = new Map()
|
||||
|
||||
var sortKey = "Directory"
|
||||
var sortOrder = "descending"
|
@@ -44,7 +44,10 @@ class Sender {
|
||||
mapping.set("Sender", this.getSenderLink())
|
||||
mapping.set("Results", this.results)
|
||||
|
||||
var trustHtml = this.trust + "<span class='right'>" + this.getTrustLinks() + "</span>"
|
||||
var trustActionHtml = "<span class='dropdown'><a class='droplink'>" + _t("Actions") + "</a><div class='dropdown-content-right'>" +
|
||||
this.getTrustLinks() +
|
||||
"</div></span>"
|
||||
var trustHtml = "<span class='center'>" + this.trust + " " + trustActionHtml + "</span>"
|
||||
trustHtml += "<div class='centercomment' id='trusted-" + this.b64 + "'></div>"
|
||||
trustHtml += "<div class='centercomment' id='distrusted-" + this.b64 + "'></div>"
|
||||
mapping.set("Trust", trustHtml)
|
||||
@@ -61,11 +64,11 @@ class Sender {
|
||||
|
||||
getTrustLinks() {
|
||||
if (this.trust == "NEUTRAL")
|
||||
return " " + this.getTrustLink() + " " + this.getDistrustLink()
|
||||
return this.getTrustLink() + this.getDistrustLink()
|
||||
else if (this.trust == "TRUSTED")
|
||||
return " " + this.getNeutralLink() + " " + this.getDistrustLink()
|
||||
return this.getNeutralLink() + this.getDistrustLink()
|
||||
else
|
||||
return " " + this.getTrustLink() + " " + this.getNeutralLink()
|
||||
return this.getTrustLink() + this.getNeutralLink()
|
||||
}
|
||||
|
||||
getTrustLink() {
|
||||
@@ -377,7 +380,11 @@ class SenderForResult {
|
||||
}
|
||||
|
||||
getTrustBlock() {
|
||||
return this.trust +"<span class='right'>" + this.getTrustLinks() + "</span>" +
|
||||
var dropdownBlock = "<span class='dropdown'><a class='droplink'>" + _t("Actions") + "</a><div class='dropdown-content-right'>" +
|
||||
this.getTrustLinks() +
|
||||
"</div></span>"
|
||||
|
||||
return "<span class='center'>"+ this.trust + " "+ dropdownBlock + "</span>" +
|
||||
"<div class='centercomment' id='trusted-" + this.b64 + "'></div>" +
|
||||
"<div class='centercomment' id='distrusted-" + this.b64 + "'></div>"
|
||||
}
|
||||
|
15
webui/src/main/js/sign.js
Normal file
15
webui/src/main/js/sign.js
Normal file
@@ -0,0 +1,15 @@
|
||||
function sign() {
|
||||
var signText = document.getElementById("sign").value
|
||||
|
||||
var xmlhttp = new XMLHttpRequest()
|
||||
xmlhttp.onreadystatechange = function() {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
var signed = this.responseXML.getElementsByTagName("Signed")[0].childNodes[0].nodeValue
|
||||
var signedDiv = document.getElementById("signed")
|
||||
signedDiv.innerHTML = "<pre>" + signed + "</pre>"
|
||||
}
|
||||
}
|
||||
xmlhttp.open("POST", "/MuWire/Sign", true)
|
||||
xmlhttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
||||
xmlhttp.send(encodeURI("text=" + signText))
|
||||
}
|
@@ -14,8 +14,13 @@ class TrustList {
|
||||
var userLink = new Link(this.user, "displayList", [this.user])
|
||||
var unsubscribeLink = new Link(_t("Unsubscribe"), "unsubscribe", [this.userB64])
|
||||
var refreshLink = new Link(_t("Refresh"), "forceUpdate", [this.userB64])
|
||||
|
||||
var nameHtml = userLink.render() + "<span class='right'>" + unsubscribeLink.render() + " " + refreshLink.render() + "</span>"
|
||||
|
||||
var actionsHtml = "<div class='dropdown'><a class='droplink'>" + _t("Actions") + "</a><div class='dropdown-content'>" +
|
||||
unsubscribeLink.render() +
|
||||
refreshLink.render() +
|
||||
"</div></div>"
|
||||
|
||||
var nameHtml = userLink.render() + "<span class='right'>" + actionsHtml + "</span>"
|
||||
|
||||
mapping.set("Name", nameHtml)
|
||||
mapping.set("Status", this.status)
|
||||
@@ -42,7 +47,11 @@ class Persona {
|
||||
getMapping() {
|
||||
var mapping = new Map()
|
||||
|
||||
var userHtml = this.user + "<div class='right'>" + this.getTrustActions().join(" ") + "</div>"
|
||||
var actionsHtml = "<div class='dropdown'><a class='droplink'>" + _t("Actions") + "</a><div class='dropdown-content'>" +
|
||||
this.getTrustActions().join("") +
|
||||
"</div></div>"
|
||||
|
||||
var userHtml = this.user + "<div class='right'>" + actionsHtml + "</div>"
|
||||
userHtml += "<div class='centercomment' id='trusted-" + this.userB64 + "'></div>"
|
||||
userHtml += "<div class='centercomment' id='distrusted-" + this.userB64 + "'></div>"
|
||||
mapping.set("Trusted User", userHtml)
|
||||
|
@@ -12,19 +12,18 @@ class Persona {
|
||||
getMapping(trusted) {
|
||||
var mapping = new Map()
|
||||
var nameHtml = this.user
|
||||
nameHtml += "<div class='right'><div class='dropdown'><a class='droplink'>" + _t("Actions") + "</a><div class='dropdown-content'>"
|
||||
if (trusted) {
|
||||
nameHtml += "<div class='right'>"
|
||||
nameHtml += this.getNeutralLink()
|
||||
nameHtml += " "
|
||||
nameHtml += this.getDistrustedLink()
|
||||
nameHtml += "</div>"
|
||||
} else {
|
||||
nameHtml += this.getTrustedLink()
|
||||
nameHtml += this.getNeutralLink()
|
||||
}
|
||||
nameHtml += "</div></div></div>"
|
||||
if (trusted) {
|
||||
nameHtml += "<div class='centercomment' id='distrusted-" + this.userB64 + "'></div>"
|
||||
} else {
|
||||
nameHtml += "<div class='right'>"
|
||||
nameHtml += this.getTrustedLink()
|
||||
nameHtml += " "
|
||||
nameHtml += this.getNeutralLink()
|
||||
nameHtml += "</div>"
|
||||
nameHtml += "<div class='centercomment' id='trusted-" + this.userB64 + "'></div>"
|
||||
}
|
||||
|
||||
|
@@ -10,6 +10,8 @@
|
||||
|
||||
<%
|
||||
String pagetitle=Util._t("About Me");
|
||||
String helptext = Util._t("This page shows information about your MuWire identity.");
|
||||
|
||||
Core core = (Core) application.getAttribute("core");
|
||||
|
||||
%>
|
||||
@@ -18,11 +20,13 @@ Core core = (Core) application.getAttribute("core");
|
||||
<head>
|
||||
<%@include file="css.jsi"%>
|
||||
<script src="js/util.js?<%=version%>" type="text/javascript"></script>
|
||||
<script src="js/sign.js?<%=version%>" type ="text/javascript"></script>
|
||||
<script>
|
||||
function copyFullId() {
|
||||
copyToClipboard("full-id")
|
||||
alert("Full Id Copied To Clipboard")
|
||||
alert("Full ID copied to clipboard")
|
||||
}
|
||||
openAccordion = 3;
|
||||
</script>
|
||||
</head>
|
||||
<body onload="initConnectionsCount();">
|
||||
@@ -32,10 +36,18 @@ function copyFullId() {
|
||||
<%@include file="sidebar.jsi"%>
|
||||
</aside>
|
||||
<section class="main foldermain">
|
||||
<p><%=Util._t("Your short MuWire id is {0}", core.getMe().getHumanReadableName())%></p>
|
||||
<p><%=Util._t("Your full MuWire id is")%></p>
|
||||
<p><textarea class="fullId" id="full-id" readOnly="true"><%=core.getMe().toBase64()%></textarea></p>
|
||||
<p><a href='#' onclick="window.copyFullId();return false;"><%=Util._t("Copy To Clipboard")%></a></p>
|
||||
<h3><%=Util._t("MuWire ID")%></h3>
|
||||
<p><%=Util._t("Your short MuWire ID: {0}", core.getMe().getHumanReadableName())%></p>
|
||||
<p><%=Util._t("Your full MuWire ID:")%></p>
|
||||
<p><textarea class="fullId" id="full-id" readonly><%=core.getMe().toBase64()%></textarea></p>
|
||||
<p><a href='#' onclick="window.copyFullId();return false;"><%=Util._t("Copy to clipboard")%></a></p>
|
||||
|
||||
<hr/>
|
||||
<h3><%=Util._t("Sign Tool")%></h3>
|
||||
<p><%=Util._t("Enter text to sign with your MuWire ID")%></p>
|
||||
<p><textarea class="sign" id="sign"></textarea></p>
|
||||
<p><a href='#' onclick="window.sign();return false;"><%=Util._t("Sign")%></a></p>
|
||||
<div id="signed"></div>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
|
39
webui/src/main/webapp/AdvancedSharing.jsp
Normal file
39
webui/src/main/webapp/AdvancedSharing.jsp
Normal file
@@ -0,0 +1,39 @@
|
||||
<%@ page language="java" contentType="text/html; charset=UTF-8"
|
||||
pageEncoding="UTF-8"%>
|
||||
<%@ page import="com.muwire.webui.*" %>
|
||||
<%@include file="initcode.jsi"%>
|
||||
|
||||
<%
|
||||
|
||||
String pagetitle=Util._t("Advanced Sharing");
|
||||
String helptext = Util._t("Use this page to configure advanced settings for each shared directory.");
|
||||
|
||||
%>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<%@ include file="css.jsi"%>
|
||||
<script src="js/util.js?<%=version%>" type="text/javascript"></script>
|
||||
<script src="js/tables.js?<%=version%> type="text/javascript"></script>
|
||||
<script src="js/advancedSharing.js?<%=version%>" type="text/javascript"></script>
|
||||
<script type="text/javascript">
|
||||
openAccordion = 2;
|
||||
</script>
|
||||
</head>
|
||||
<body onload="initTranslate(jsTranslations); initConnectionsCount(); initAdvancedSharing();">
|
||||
<%@ include file="header.jsi"%>
|
||||
<aside>
|
||||
<div class="menubox-divider"></div>
|
||||
<%@include file="sidebar.jsi"%>
|
||||
</aside>
|
||||
<section class="main foldermain">
|
||||
<p><%=Util._t("Shared directories can be watched automatically or periodically. Automatic watching is recommended, but may not work on some NAS devices.")%></p>
|
||||
<div id="table-wrapper">
|
||||
<div class="paddedTable" id="table-scroll">
|
||||
<div id="dirsTable"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="dirConfig"></div>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
@@ -9,6 +9,7 @@
|
||||
<%
|
||||
|
||||
String pagetitle=Util._t("Browse Host");
|
||||
String helptext = Util._t("Use this page to browse the files shared by other MuWire users.");
|
||||
|
||||
String currentBrowse = null;
|
||||
if (request.getParameter("currentHost") != null) {
|
||||
@@ -38,13 +39,18 @@ if (request.getParameter("currentHost") != null) {
|
||||
<aside>
|
||||
<div class="menubox-divider"></div>
|
||||
<div class="menubox">
|
||||
<h2><%=Util._t("Enter a full MuWire id")%></h2>
|
||||
<h2><%=Util._t("Enter a full MuWire ID")%></h2>
|
||||
<form action="/MuWire/Browse" method="post">
|
||||
<input type="text" name="host">
|
||||
<input type="hidden" name="action" value="browse">
|
||||
<div class="menuitem browse">
|
||||
<div class="menu-icon"></div>
|
||||
<input type="submit" value=<%=Util._t("Browse")%>>
|
||||
<div class="tooltip"><%=Util._t("Help")%>
|
||||
<span class="tooltiptext">
|
||||
<%=Util._t("Enter the full ID of a MuWire user to see what files they are sharing")%>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
@@ -12,6 +12,8 @@
|
||||
|
||||
<%
|
||||
String pagetitle=Util._t("Configuration");
|
||||
String helptext = Util._t("Use this page to change MuWire options.");
|
||||
|
||||
Core core = (Core) application.getAttribute("core");
|
||||
|
||||
String inboundLength = core.getI2pOptions().getProperty("inbound.length");
|
||||
@@ -25,6 +27,9 @@ Exception error = (Exception) application.getAttribute("MWConfigError");
|
||||
<html>
|
||||
<head>
|
||||
<%@include file="css.jsi"%>
|
||||
<script type="text/javascript">
|
||||
openAccordion = 2;
|
||||
</script>
|
||||
</head>
|
||||
<body onload="initConnectionsCount();">
|
||||
<%@include file="header.jsi"%>
|
||||
@@ -42,28 +47,53 @@ Exception error = (Exception) application.getAttribute("MWConfigError");
|
||||
<h3><%=Util._t("Search")%></h3>
|
||||
<table>
|
||||
<tr>
|
||||
<td><%=Util._t("Search in comments")%></td>
|
||||
<td>
|
||||
<div class="tooltip"><%=Util._t("Search in comments")%>
|
||||
<span class="tooltiptext"><%=Util._t("When searching the network, should MuWire search only file names or in the comments too?")%></span>
|
||||
</div>
|
||||
</td>
|
||||
<td><p align="right"><input type="checkbox" <% if (core.getMuOptions().getSearchComments()) out.write("checked"); %> name="searchComments" value="true"></p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><%=Util._t("Allow browsing")%></td>
|
||||
<td>
|
||||
<div class="tooltip"><%=Util._t("Allow browsing")%>
|
||||
<span class="tooltiptext"><%=Util._t("Allow other MuWire users to browse your shared files?")%></span>
|
||||
</div>
|
||||
</td>
|
||||
<td><p align="right"><input type="checkbox" <% if (core.getMuOptions().getBrowseFiles()) out.write("checked"); %> name="browseFiles" value="true"></p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="tooltip"><%=Util._t("Allow tracking")%>
|
||||
<span class="tooltiptext"><%=Util._t("Allow trackers to track your shared files?")%></span>
|
||||
</div>
|
||||
</td>
|
||||
<td><p align="right"><input type="checkbox" <% if (core.getMuOptions().getAllowTracking()) out.write("checked"); %> name="allowTracking" value="true"></p></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="configuration-section">
|
||||
<h3><%=Util._t("Downloads")%></h3>
|
||||
<table>
|
||||
<tr>
|
||||
<td><%=Util._t("Download retry frequency (seconds)")%></td>
|
||||
<td><div class="tooltip"><%=Util._t("Download retry frequency (seconds)")%>
|
||||
<span class="tooltiptext"><%=Util._t("MuWire retries failed downloads automatically. This value controls how often to retry.")%></span>
|
||||
</div>
|
||||
</td>
|
||||
<td><p align="right"><input type="text" size="1" name="downloadRetryInterval" class="right" value="<%= core.getMuOptions().getDownloadRetryInterval()%>"></p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><%=Util._t("Directory for downloaded files")%></td>
|
||||
<td><div class="tooltip"><%=Util._t("Directory for downloaded files")%>
|
||||
<span class="tooltiptext"><%=Util._t("Where to save downloaded files. MuWire must be able to write to this location")%></span>
|
||||
</div>
|
||||
</td>
|
||||
<td><p align="right"><input type="text" size="30" name="downloadLocation" value="<%= core.getMuOptions().getDownloadLocation().getAbsoluteFile()%>"></p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><%=Util._t("Directory for incomplete files")%></td>
|
||||
<td><div class="tooltip"><%=Util._t("Directory for incomplete files")%>
|
||||
<span class="tooltiptext"><%=Util._t("Where to store partial data of files which are currently being downloaded.")%></span>
|
||||
</div>
|
||||
</td>
|
||||
<td><p align="right"><input type="text" size="30" name="incompleteLocation" value="<%= core.getMuOptions().getIncompleteLocation().getAbsoluteFile()%>"></p></td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -72,11 +102,17 @@ Exception error = (Exception) application.getAttribute("MWConfigError");
|
||||
<h3><%=Util._t("Upload")%></h3>
|
||||
<table>
|
||||
<tr>
|
||||
<td><%=Util._t("Total upload slots (-1 means unlimited)")%></td>
|
||||
<td><div class="tooltip"><%=Util._t("Total upload slots (-1 means unlimited)")%>
|
||||
<span class="tooltiptext"><%=Util._t("How many files at most should MuWire upload at the same time")%></span>
|
||||
</div>
|
||||
</td>
|
||||
<td><p align="right"><input type="text" size="1" name="totalUploadSlots" class="right" value="<%= core.getMuOptions().getTotalUploadSlots() %>"></p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><%=Util._t("Upload slots per user (-1 means unlimited)")%></td>
|
||||
<td><div class="tooltip"><%=Util._t("Upload slots per user (-1 means unlimited)")%>
|
||||
<span class="tooltiptext"><%=Util._t("How many files should MuWire upload to any given user at the same time")%></span>
|
||||
</div>
|
||||
</td>
|
||||
<td><p align="right"><input type="text" size="1" name="uploadSlotsPerUser" class="right" value="<%= core.getMuOptions().getUploadSlotsPerUser() %>"></p></td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -85,11 +121,17 @@ Exception error = (Exception) application.getAttribute("MWConfigError");
|
||||
<h3><%=Util._t("Sharing")%></h3>
|
||||
<table>
|
||||
<tr>
|
||||
<td><%=Util._t("Share downloaded files")%></td>
|
||||
<td><div class="tooltip"><%=Util._t("Share downloaded files")%>
|
||||
<span class="tooltiptext"><%=Util._t("Whether to automatically share files which you have downloaded with MuWire")%></span>
|
||||
</div>
|
||||
</td>
|
||||
<td><p align="right"><input type="checkbox" <% if (core.getMuOptions().getShareDownloadedFiles()) out.write("checked"); %> name="shareDownloadedFiles" value="true"></p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><%=Util._t("Share hidden files")%></td>
|
||||
<td><div class="tooltip"><%=Util._t("Share hidden files")%>
|
||||
<span class="tooltiptext"><%=Util._t("Should MuWire share files marked as Hidden by the operating system?")%></span>
|
||||
</div>
|
||||
</td>
|
||||
<td><p align="right"><input type="checkbox" <% if (core.getMuOptions().getShareHiddenFiles()) out.write("checked"); %> name="shareHiddenFiles" value="true"></p></td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -98,15 +140,23 @@ Exception error = (Exception) application.getAttribute("MWConfigError");
|
||||
<h3><%=Util._t("Publishing")%></h3>
|
||||
<table>
|
||||
<tr>
|
||||
<td><%=Util._t("Enable my feed")%></td>
|
||||
<td><div class="tooltip"><%=Util._t("Enable my feed")%>
|
||||
<span class="tooltiptext"><%=Util._t("Enable your personal file feed?")%></span></div>
|
||||
</td>
|
||||
<td><p align="right"><input type="checkbox" <% if (core.getMuOptions().getFileFeed()) out.write("checked"); %> name="fileFeed" value="true"></p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><%=Util._t("Advertise my feed in search results")%></td>
|
||||
<td><div class="tooltip"><%=Util._t("Advertise my feed in search results")%>
|
||||
<span class="tooltiptext"><%=Util._t("If this is enabled MuWire will let other users know about your personal file feed")%></span>
|
||||
</div>
|
||||
</td>
|
||||
<td><p align="right"><input type="checkbox" <% if (core.getMuOptions().getAdvertiseFeed()) out.write("checked"); %> name="advertiseFeed" value="true"></p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><%=Util._t("Publish shared files automatically")%></td>
|
||||
<td><div class="tooltip"><%=Util._t("Publish shared files automatically")%>
|
||||
<span class="tooltiptext"><%=Util._t("If enabled, all files you share in the future will be published to your feed automatically.")%></span>
|
||||
</div>
|
||||
</td>
|
||||
<td><p align="right"><input type="checkbox" <% if (core.getMuOptions().getAutoPublishSharedFiles()) out.write("checked"); %> name="autoPublishSharedFiles" value="true"></p></td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -115,19 +165,32 @@ Exception error = (Exception) application.getAttribute("MWConfigError");
|
||||
<h3><%=Util._t("Default settings for new feeds")%></h3>
|
||||
<table>
|
||||
<tr>
|
||||
<td><%=Util._t("Download published files automatically")%></td>
|
||||
<td><div class="tooltip"><%=Util._t("Download published files automatically")%>
|
||||
<span class="tooltiptext"><%=Util._t("If enabled, MuWire will download every file published to the given feed")%></span>
|
||||
</div>
|
||||
</td>
|
||||
<td><p align="right"><input type="checkbox" <% if (core.getMuOptions().getDefaultFeedAutoDownload()) out.write("checked"); %> name="defaultFeedAutoDownload" value="true"></p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><%=Util._t("Download each file sequentially")%></td>
|
||||
<td><div class="tooltip"><%=Util._t("Download each file sequentially")%>
|
||||
<span class="tooltiptext"><%=Util._t("Whether to download files from this feed sequentially. This helps with previewing media files, but may reduce availability of the file for others.")%></span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td><p align="right"><input type="checkbox" <% if (core.getMuOptions().getDefaultFeedSequential()) out.write("checked"); %> name="defaultFeedAutoDownload" value="true"></p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><%=Util._t("Feed update frequency (minutes)")%></td>
|
||||
<td><div class="tooltip"><%=Util._t("Feed update frequency (minutes)")%>
|
||||
<span class="tooltiptext"><%=Util._t("How often to check for updates to this feed")%></span>
|
||||
</div>
|
||||
</td>
|
||||
<td><p align="right"><input type="text" size="1" name="defaultFeedUpdateInterval" class="right" value="<%= core.getMuOptions().getDefaultFeedUpdateInterval() / 60000 %>"></p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><%=Util._t("Number of items to keep on disk (-1 means unlimited)")%></td>
|
||||
<td><div class="tooltip"><%=Util._t("Number of items to keep on disk (-1 means unlimited)")%>
|
||||
<span class="tooltiptext"><%=Util._t("MuWire will only remember this many published items across restarts, unless you set the value to -1")%></span>
|
||||
</div>
|
||||
</td>
|
||||
<td><p align="right"><input type="text" size="1" name="defaultFeedItemsToKeep" class="right" value="<%= core.getMuOptions().getDefaultFeedItemsToKeep() %>"></p></td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -136,19 +199,31 @@ Exception error = (Exception) application.getAttribute("MWConfigError");
|
||||
<h3><%=Util._t("Trust")%></h3>
|
||||
<table>
|
||||
<tr>
|
||||
<td><%=Util._t("Allow only trusted connections")%></td>
|
||||
<td><div class="tooltip"><%=Util._t("Allow only trusted connections")%>
|
||||
<span class="tooltiptext"><%=Util._t("If enabled, MuWire will only connect to users you have marked as Trusted")%></span>
|
||||
</div>
|
||||
</td>
|
||||
<td><p align="right"><input type="checkbox" <% if (!core.getMuOptions().getAllowUntrusted()) out.write("checked"); %> name="allowUntrusted" value="true"></p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><%=Util._t("Search extra hop")%></td>
|
||||
<td><div class="tooltip"><%=Util._t("Search extra hop")%>
|
||||
<span class="tooltiptext"><%=Util._t("If only trusted connections are allowed, MuWire will search only users that are directly connected to you. Use this setting to search further out. It has no effect if untrusted connections are allowed")%></span>
|
||||
</div>
|
||||
</td>
|
||||
<td><p align="right"><input type="checkbox" <% if (core.getMuOptions().getSearchExtraHop()) out.write("checked"); %> name="searchExtraHop" value="true"></p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><%=Util._t("Allow others to view my trust list")%></td>
|
||||
<td><div class="tooltip"><%=Util._t("Allow others to view my trust list")%>
|
||||
<span class="tooltiptext"><%=Util._t("Whether to allow other MuWire users to see who you have marked as Trusted and Distrusted")%></span>
|
||||
</div>
|
||||
</td>
|
||||
<td><p align="right"><input type="checkbox" <% if (core.getMuOptions().getAllowTrustLists()) out.write("checked"); %> name="allowTrustLists" value="true"></p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><%=Util._t("Trust list update frequency (hours)")%></td>
|
||||
<td><div class="tooltip"><%=Util._t("Trust list update frequency (hours)")%>
|
||||
<span class="tooltiptext"><%=Util._t("How often to check for updates to the trust lists you are subscribed to.")%></span>
|
||||
</div>
|
||||
</td>
|
||||
<td><p align="right"><input type="text" size="1" name="trustListInterval" class="right" value="<%= core.getMuOptions().getTrustListInterval() %>"></p></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
@@ -10,7 +10,10 @@
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@include file="initcode.jsi"%>
|
||||
|
||||
<% String pagetitle=Util._t("Downloads"); %>
|
||||
<%
|
||||
String pagetitle=Util._t("Downloads");
|
||||
String helptext = Util._t("This page shows the files you are currently downloading from other MuWire users.");
|
||||
%>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
|
@@ -6,6 +6,10 @@
|
||||
<%
|
||||
|
||||
String pagetitle=Util._t("Feeds");
|
||||
String helptext = Util._t("Every MuWire user can have a file feed to publish shared files of their choosing. " +
|
||||
"You can subscribe to the feeds of other users. This is similar to following someone on a social network.") +
|
||||
"<br/>" + Util._t("On this page you can view the file feeds of users you are subscribed to. You can configure each feed " +
|
||||
"separately with various options, and you can download the published files.");
|
||||
|
||||
%>
|
||||
|
||||
@@ -30,6 +34,11 @@ String pagetitle=Util._t("Feeds");
|
||||
<div class="menuitem feeds">
|
||||
<div class="menu-icon"></div>
|
||||
<input type="submit" value=<%=Util._t("Subscribe")%>>
|
||||
<div class="tooltip"><%=Util._t("Help")%>
|
||||
<span class="tooltiptext">
|
||||
<%=Util._t("Enter the full ID of a MuWire user to see what files they have published to their feed")%>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
@@ -7,6 +7,7 @@
|
||||
<%
|
||||
|
||||
String pagetitle=Util._t("File Details");
|
||||
String helptext = Util._t("View details about the selected shared file here.");
|
||||
|
||||
String path = request.getParameter("path");
|
||||
File file = Util.getFromPathElements(path);
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user