Compare commits
27 Commits
muwire-0.1
...
muwire-0.1
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bb4d522572 | ||
![]() |
8052501e52 | ||
![]() |
66cc6d8ab7 | ||
![]() |
a45e57f5ec | ||
![]() |
7d8ca55d87 | ||
![]() |
de22f3c6b9 | ||
![]() |
3b0eb5678d | ||
![]() |
5a1f32e40b | ||
![]() |
ca3f2513e1 | ||
![]() |
658d9cf5a8 | ||
![]() |
e389090b7e | ||
![]() |
04ceaba514 | ||
![]() |
6a01d97a8d | ||
![]() |
747663e1dc | ||
![]() |
e426b3ccbd | ||
![]() |
5172e19627 | ||
![]() |
e826cfd8d5 | ||
![]() |
51004f6fe9 | ||
![]() |
08bb2b614d | ||
![]() |
d0e5d0ce8a | ||
![]() |
9e05802d1b | ||
![]() |
fb4f56eec9 | ||
![]() |
be2083d430 | ||
![]() |
af6275d0a3 | ||
![]() |
5269815329 | ||
![]() |
bd21cf65ea | ||
![]() |
dea592eb27 |
@@ -4,11 +4,11 @@ MuWire is an easy to use file-sharing program which offers anonymity using [I2P
|
|||||||
|
|
||||||
It is inspired by the LimeWire Gnutella client and developped by a former LimeWire developer.
|
It is inspired by the LimeWire Gnutella client and developped by a former LimeWire developer.
|
||||||
|
|
||||||
The first stable release - 0.1.0 is avaiable for download at http://muwire.com. You can find technical documentation in the "doc" folder.
|
The current stable release - 0.1.5 is avaiable for download at http://muwire.com. You can find technical documentation in the "doc" folder.
|
||||||
|
|
||||||
### Building
|
### Building
|
||||||
|
|
||||||
You need JDK 8 or newer. After installing that and setting up the appropriate paths, just type
|
You need JRE 8 or newer. After installing that and setting up the appropriate paths, just type
|
||||||
|
|
||||||
```
|
```
|
||||||
./gradlew assemble
|
./gradlew assemble
|
||||||
|
@@ -34,7 +34,7 @@ class Cli {
|
|||||||
|
|
||||||
Core core
|
Core core
|
||||||
try {
|
try {
|
||||||
core = new Core(props, home, "0.1.4")
|
core = new Core(props, home, "0.1.8")
|
||||||
} catch (Exception bad) {
|
} catch (Exception bad) {
|
||||||
bad.printStackTrace(System.out)
|
bad.printStackTrace(System.out)
|
||||||
println "Failed to initialize core, exiting"
|
println "Failed to initialize core, exiting"
|
||||||
|
@@ -53,7 +53,7 @@ class CliDownloader {
|
|||||||
|
|
||||||
Core core
|
Core core
|
||||||
try {
|
try {
|
||||||
core = new Core(props, home, "0.1.4")
|
core = new Core(props, home, "0.1.8")
|
||||||
} catch (Exception bad) {
|
} catch (Exception bad) {
|
||||||
bad.printStackTrace(System.out)
|
bad.printStackTrace(System.out)
|
||||||
println "Failed to initialize core, exiting"
|
println "Failed to initialize core, exiting"
|
||||||
|
@@ -12,6 +12,7 @@ import com.muwire.core.connection.I2PConnector
|
|||||||
import com.muwire.core.connection.LeafConnectionManager
|
import com.muwire.core.connection.LeafConnectionManager
|
||||||
import com.muwire.core.connection.UltrapeerConnectionManager
|
import com.muwire.core.connection.UltrapeerConnectionManager
|
||||||
import com.muwire.core.download.DownloadManager
|
import com.muwire.core.download.DownloadManager
|
||||||
|
import com.muwire.core.download.UIDownloadCancelledEvent
|
||||||
import com.muwire.core.download.UIDownloadEvent
|
import com.muwire.core.download.UIDownloadEvent
|
||||||
import com.muwire.core.files.FileDownloadedEvent
|
import com.muwire.core.files.FileDownloadedEvent
|
||||||
import com.muwire.core.files.FileHashedEvent
|
import com.muwire.core.files.FileHashedEvent
|
||||||
@@ -90,6 +91,7 @@ public class Core {
|
|||||||
def i2pOptionsFile = new File(home,"i2p.properties")
|
def i2pOptionsFile = new File(home,"i2p.properties")
|
||||||
if (i2pOptionsFile.exists()) {
|
if (i2pOptionsFile.exists()) {
|
||||||
i2pOptionsFile.withInputStream { i2pOptions.load(it) }
|
i2pOptionsFile.withInputStream { i2pOptions.load(it) }
|
||||||
|
|
||||||
if (!i2pOptions.hasProperty("inbound.nickname"))
|
if (!i2pOptions.hasProperty("inbound.nickname"))
|
||||||
i2pOptions["inbound.nickname"] = "MuWire"
|
i2pOptions["inbound.nickname"] = "MuWire"
|
||||||
if (!i2pOptions.hasProperty("outbound.nickname"))
|
if (!i2pOptions.hasProperty("outbound.nickname"))
|
||||||
@@ -101,13 +103,15 @@ public class Core {
|
|||||||
i2pOptions["inbound.quantity"] = "2"
|
i2pOptions["inbound.quantity"] = "2"
|
||||||
i2pOptions["outbound.length"] = "3"
|
i2pOptions["outbound.length"] = "3"
|
||||||
i2pOptions["outbound.quantity"] = "2"
|
i2pOptions["outbound.quantity"] = "2"
|
||||||
|
i2pOptions["i2cp.tcp.host"] = "127.0.0.1"
|
||||||
|
i2pOptions["i2cp.tcp.port"] = "7654"
|
||||||
}
|
}
|
||||||
|
|
||||||
// options like tunnel length and quantity
|
// options like tunnel length and quantity
|
||||||
I2PSession i2pSession
|
I2PSession i2pSession
|
||||||
I2PSocketManager socketManager
|
I2PSocketManager socketManager
|
||||||
keyDat.withInputStream {
|
keyDat.withInputStream {
|
||||||
socketManager = new I2PSocketManagerFactory().createManager(it, i2pOptions)
|
socketManager = new I2PSocketManagerFactory().createManager(it, i2pOptions["i2cp.tcp.host"], i2pOptions["i2cp.tcp.port"].toInteger(), i2pOptions)
|
||||||
}
|
}
|
||||||
socketManager.getDefaultOptions().setReadTimeout(60000)
|
socketManager.getDefaultOptions().setReadTimeout(60000)
|
||||||
socketManager.getDefaultOptions().setConnectTimeout(30000)
|
socketManager.getDefaultOptions().setConnectTimeout(30000)
|
||||||
@@ -156,7 +160,7 @@ public class Core {
|
|||||||
eventBus.register(SearchEvent.class, fileManager)
|
eventBus.register(SearchEvent.class, fileManager)
|
||||||
|
|
||||||
log.info "initializing persistence service"
|
log.info "initializing persistence service"
|
||||||
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 5000, fileManager)
|
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 15000, fileManager)
|
||||||
|
|
||||||
log.info("initializing host cache")
|
log.info("initializing host cache")
|
||||||
File hostStorage = new File(home, "hosts.json")
|
File hostStorage = new File(home, "hosts.json")
|
||||||
@@ -195,6 +199,7 @@ public class Core {
|
|||||||
eventBus.register(UIDownloadEvent.class, downloadManager)
|
eventBus.register(UIDownloadEvent.class, downloadManager)
|
||||||
eventBus.register(UILoadedEvent.class, downloadManager)
|
eventBus.register(UILoadedEvent.class, downloadManager)
|
||||||
eventBus.register(FileDownloadedEvent.class, downloadManager)
|
eventBus.register(FileDownloadedEvent.class, downloadManager)
|
||||||
|
eventBus.register(UIDownloadCancelledEvent.class, downloadManager)
|
||||||
|
|
||||||
log.info("initializing upload manager")
|
log.info("initializing upload manager")
|
||||||
UploadManager uploadManager = new UploadManager(eventBus, fileManager)
|
UploadManager uploadManager = new UploadManager(eventBus, fileManager)
|
||||||
@@ -255,7 +260,7 @@ public class Core {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Core core = new Core(props, home, "0.1.4")
|
Core core = new Core(props, home, "0.1.8")
|
||||||
core.startServices()
|
core.startServices()
|
||||||
|
|
||||||
// ... at the end, sleep or execute script
|
// ... at the end, sleep or execute script
|
||||||
|
@@ -108,6 +108,9 @@ class ConnectionAcceptor {
|
|||||||
case (byte)'G':
|
case (byte)'G':
|
||||||
processGET(e)
|
processGET(e)
|
||||||
break
|
break
|
||||||
|
case (byte)'H':
|
||||||
|
processHashList(e)
|
||||||
|
break
|
||||||
case (byte)'P':
|
case (byte)'P':
|
||||||
processPOST(e)
|
processPOST(e)
|
||||||
break
|
break
|
||||||
@@ -178,9 +181,18 @@ class ConnectionAcceptor {
|
|||||||
dis.readFully(et)
|
dis.readFully(et)
|
||||||
if (et != "ET ".getBytes(StandardCharsets.US_ASCII))
|
if (et != "ET ".getBytes(StandardCharsets.US_ASCII))
|
||||||
throw new IOException("Invalid GET connection")
|
throw new IOException("Invalid GET connection")
|
||||||
uploadManager.processEndpoint(e)
|
uploadManager.processGET(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void processHashList(Endpoint e) {
|
||||||
|
byte[] ashList = new byte[8]
|
||||||
|
final DataInputStream dis = new DataInputStream(e.getInputStream())
|
||||||
|
dis.readFully(ashList)
|
||||||
|
if (ashList != "ASHLIST ".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
throw new IOException("Invalid HASHLIST connection")
|
||||||
|
uploadManager.processHashList(e)
|
||||||
|
}
|
||||||
|
|
||||||
private void processPOST(final Endpoint e) throws IOException {
|
private void processPOST(final Endpoint e) throws IOException {
|
||||||
byte [] ost = new byte[4]
|
byte [] ost = new byte[4]
|
||||||
final DataInputStream dis = new DataInputStream(e.getInputStream())
|
final DataInputStream dis = new DataInputStream(e.getInputStream())
|
||||||
|
@@ -68,6 +68,11 @@ public class DownloadManager {
|
|||||||
eventBus.publish(new DownloadStartedEvent(downloader : downloader))
|
eventBus.publish(new DownloadStartedEvent(downloader : downloader))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void onUIDownloadCancelledEvent(UIDownloadCancelledEvent e) {
|
||||||
|
downloaders.remove(e.downloader)
|
||||||
|
persistDownloaders()
|
||||||
|
}
|
||||||
|
|
||||||
void resume(Downloader downloader) {
|
void resume(Downloader downloader) {
|
||||||
executor.execute({downloader.download() as Runnable})
|
executor.execute({downloader.download() as Runnable})
|
||||||
}
|
}
|
||||||
@@ -84,8 +89,14 @@ public class DownloadManager {
|
|||||||
json.destinations.each { destination ->
|
json.destinations.each { destination ->
|
||||||
destinations.add new Destination(destination)
|
destinations.add new Destination(destination)
|
||||||
}
|
}
|
||||||
byte[] hashList = Base64.decode(json.hashList)
|
InfoHash infoHash
|
||||||
InfoHash infoHash = InfoHash.fromHashList(hashList)
|
if (json.hashList != null) {
|
||||||
|
byte[] hashList = Base64.decode(json.hashList)
|
||||||
|
infoHash = InfoHash.fromHashList(hashList)
|
||||||
|
} else {
|
||||||
|
byte [] root = Base64.decode(json.hashRoot)
|
||||||
|
infoHash = new InfoHash(root)
|
||||||
|
}
|
||||||
def downloader = new Downloader(eventBus, this, me, file, (long)json.length,
|
def downloader = new Downloader(eventBus, this, me, file, (long)json.length,
|
||||||
infoHash, json.pieceSizePow2, connector, destinations, incompletes)
|
infoHash, json.pieceSizePow2, connector, destinations, incompletes)
|
||||||
downloader.download()
|
downloader.download()
|
||||||
@@ -113,8 +124,11 @@ public class DownloadManager {
|
|||||||
}
|
}
|
||||||
json.destinations = destinations
|
json.destinations = destinations
|
||||||
|
|
||||||
json.hashList = Base64.encode(downloader.infoHash.hashList)
|
InfoHash infoHash = downloader.getInfoHash()
|
||||||
|
if (infoHash.hashList != null)
|
||||||
|
json.hashList = Base64.encode(infoHash.hashList)
|
||||||
|
else
|
||||||
|
json.hashRoot = Base64.encode(infoHash.getRoot())
|
||||||
writer.println(JsonOutput.toJson(json))
|
writer.println(JsonOutput.toJson(json))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -106,7 +106,7 @@ class DownloadSession {
|
|||||||
if (!code.startsWith("200 ")) {
|
if (!code.startsWith("200 ")) {
|
||||||
log.warning("unknown code $code")
|
log.warning("unknown code $code")
|
||||||
endpoint.close()
|
endpoint.close()
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse all headers
|
// parse all headers
|
||||||
@@ -131,7 +131,7 @@ class DownloadSession {
|
|||||||
if (receivedStart != start || receivedEnd != end) {
|
if (receivedStart != start || receivedEnd != end) {
|
||||||
log.warning("We don't support mismatching ranges yet")
|
log.warning("We don't support mismatching ranges yet")
|
||||||
endpoint.close()
|
endpoint.close()
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// start the download
|
// start the download
|
||||||
|
@@ -20,8 +20,8 @@ import net.i2p.data.Destination
|
|||||||
|
|
||||||
@Log
|
@Log
|
||||||
public class Downloader {
|
public class Downloader {
|
||||||
public enum DownloadState { CONNECTING, DOWNLOADING, FAILED, CANCELLED, FINISHED }
|
public enum DownloadState { CONNECTING, HASHLIST, DOWNLOADING, FAILED, CANCELLED, FINISHED }
|
||||||
private enum WorkerState { CONNECTING, DOWNLOADING, FINISHED}
|
private enum WorkerState { CONNECTING, HASHLIST, DOWNLOADING, FINISHED}
|
||||||
|
|
||||||
private static final ExecutorService executorService = Executors.newCachedThreadPool({r ->
|
private static final ExecutorService executorService = Executors.newCachedThreadPool({r ->
|
||||||
Thread rv = new Thread(r)
|
Thread rv = new Thread(r)
|
||||||
@@ -36,7 +36,7 @@ public class Downloader {
|
|||||||
private final File file
|
private final File file
|
||||||
private final Pieces downloaded, claimed
|
private final Pieces downloaded, claimed
|
||||||
private final long length
|
private final long length
|
||||||
private final InfoHash infoHash
|
private InfoHash infoHash
|
||||||
private final int pieceSize
|
private final int pieceSize
|
||||||
private final I2PConnector connector
|
private final I2PConnector connector
|
||||||
private final Set<Destination> destinations
|
private final Set<Destination> destinations
|
||||||
@@ -76,6 +76,14 @@ public class Downloader {
|
|||||||
claimed = new Pieces(nPieces)
|
claimed = new Pieces(nPieces)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public synchronized InfoHash getInfoHash() {
|
||||||
|
infoHash
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void setInfoHash(InfoHash infoHash) {
|
||||||
|
this.infoHash = infoHash
|
||||||
|
}
|
||||||
|
|
||||||
void download() {
|
void download() {
|
||||||
readPieces()
|
readPieces()
|
||||||
destinations.each {
|
destinations.each {
|
||||||
@@ -145,6 +153,17 @@ public class Downloader {
|
|||||||
if (oneDownloading)
|
if (oneDownloading)
|
||||||
return DownloadState.DOWNLOADING
|
return DownloadState.DOWNLOADING
|
||||||
|
|
||||||
|
// at least one is requesting hashlist
|
||||||
|
boolean oneHashlist = false
|
||||||
|
activeWorkers.values().each {
|
||||||
|
if (it.currentState == WorkerState.HASHLIST) {
|
||||||
|
oneHashlist = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oneHashlist)
|
||||||
|
return DownloadState.HASHLIST
|
||||||
|
|
||||||
return DownloadState.CONNECTING
|
return DownloadState.CONNECTING
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,10 +217,16 @@ public class Downloader {
|
|||||||
Endpoint endpoint = null
|
Endpoint endpoint = null
|
||||||
try {
|
try {
|
||||||
endpoint = connector.connect(destination)
|
endpoint = connector.connect(destination)
|
||||||
|
while(getInfoHash().hashList == null) {
|
||||||
|
currentState = WorkerState.HASHLIST
|
||||||
|
HashListSession session = new HashListSession(me.toBase64(), infoHash, endpoint)
|
||||||
|
InfoHash received = session.request()
|
||||||
|
setInfoHash(received)
|
||||||
|
}
|
||||||
currentState = WorkerState.DOWNLOADING
|
currentState = WorkerState.DOWNLOADING
|
||||||
boolean requestPerformed
|
boolean requestPerformed
|
||||||
while(!downloaded.isComplete()) {
|
while(!downloaded.isComplete()) {
|
||||||
currentSession = new DownloadSession(me.toBase64(), downloaded, claimed, infoHash, endpoint, file, pieceSize, length)
|
currentSession = new DownloadSession(me.toBase64(), downloaded, claimed, getInfoHash(), endpoint, file, pieceSize, length)
|
||||||
requestPerformed = currentSession.request()
|
requestPerformed = currentSession.request()
|
||||||
if (!requestPerformed)
|
if (!requestPerformed)
|
||||||
break
|
break
|
||||||
@@ -214,9 +239,12 @@ public class Downloader {
|
|||||||
if (downloaded.isComplete() && !eventFired) {
|
if (downloaded.isComplete() && !eventFired) {
|
||||||
piecesFile.delete()
|
piecesFile.delete()
|
||||||
eventFired = true
|
eventFired = true
|
||||||
eventBus.publish(new FileDownloadedEvent(downloadedFile : new DownloadedFile(file, infoHash, pieceSize, Collections.emptySet())),
|
eventBus.publish(
|
||||||
downloader : Downloader.this)
|
new FileDownloadedEvent(
|
||||||
}
|
downloadedFile : new DownloadedFile(file, getInfoHash(), pieceSizePow2, Collections.emptySet()),
|
||||||
|
downloader : Downloader.this))
|
||||||
|
|
||||||
|
}
|
||||||
endpoint?.close()
|
endpoint?.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,82 @@
|
|||||||
|
package com.muwire.core.download
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.security.NoSuchAlgorithmException
|
||||||
|
|
||||||
|
import com.muwire.core.Constants
|
||||||
|
import com.muwire.core.InfoHash
|
||||||
|
import com.muwire.core.connection.Endpoint
|
||||||
|
|
||||||
|
import groovy.util.logging.Log
|
||||||
|
|
||||||
|
import static com.muwire.core.util.DataUtil.readTillRN
|
||||||
|
|
||||||
|
import net.i2p.data.Base64
|
||||||
|
|
||||||
|
@Log
|
||||||
|
class HashListSession {
|
||||||
|
private final String meB64
|
||||||
|
private final InfoHash infoHash
|
||||||
|
private final Endpoint endpoint
|
||||||
|
|
||||||
|
HashListSession(String meB64, InfoHash infoHash, Endpoint endpoint) {
|
||||||
|
this.meB64 = meB64
|
||||||
|
this.infoHash = infoHash
|
||||||
|
this.endpoint = endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
InfoHash request() throws IOException {
|
||||||
|
InputStream is = endpoint.getInputStream()
|
||||||
|
OutputStream os = endpoint.getOutputStream()
|
||||||
|
|
||||||
|
String root = Base64.encode(infoHash.getRoot())
|
||||||
|
os.write("HASHLIST $root\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
os.write("X-Persona: $meB64\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
os.flush()
|
||||||
|
|
||||||
|
String code = readTillRN(is)
|
||||||
|
if (!code.startsWith("200"))
|
||||||
|
throw new IOException("unknown code $code")
|
||||||
|
|
||||||
|
// parse all headers
|
||||||
|
Set<String> headers = new HashSet<>()
|
||||||
|
String header
|
||||||
|
while((header = readTillRN(is)) != "" && headers.size() < Constants.MAX_HEADERS)
|
||||||
|
headers.add(header)
|
||||||
|
|
||||||
|
long receivedStart = -1
|
||||||
|
long receivedEnd = -1
|
||||||
|
for (String receivedHeader : headers) {
|
||||||
|
def group = (receivedHeader =~ /^Content-Range: (\d+)-(\d+)$/)
|
||||||
|
if (group.size() != 1) {
|
||||||
|
log.info("ignoring header $receivedHeader")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
receivedStart = Long.parseLong(group[0][1])
|
||||||
|
receivedEnd = Long.parseLong(group[0][2])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (receivedStart != 0)
|
||||||
|
throw new IOException("hashlist started at $receivedStart")
|
||||||
|
|
||||||
|
byte[] hashList = new byte[receivedEnd]
|
||||||
|
ByteBuffer hashListBuf = ByteBuffer.wrap(hashList)
|
||||||
|
byte[] tmp = new byte[0x1 << 13]
|
||||||
|
while(hashListBuf.hasRemaining()) {
|
||||||
|
if (hashListBuf.remaining() > tmp.length)
|
||||||
|
tmp = new byte[hashListBuf.remaining()]
|
||||||
|
int read = is.read(tmp)
|
||||||
|
if (read == -1)
|
||||||
|
throw new IOException()
|
||||||
|
hashListBuf.put(tmp, 0, read)
|
||||||
|
}
|
||||||
|
|
||||||
|
InfoHash received = InfoHash.fromHashList(hashList)
|
||||||
|
if (received.getRoot() != infoHash.getRoot())
|
||||||
|
throw new IOException("fetched list doesn't match root")
|
||||||
|
received
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,7 @@
|
|||||||
|
package com.muwire.core.download
|
||||||
|
|
||||||
|
import com.muwire.core.Event
|
||||||
|
|
||||||
|
class UIDownloadCancelledEvent extends Event {
|
||||||
|
Downloader downloader
|
||||||
|
}
|
@@ -20,12 +20,12 @@ class FileHasher {
|
|||||||
* @return the size of each piece in power of 2
|
* @return the size of each piece in power of 2
|
||||||
*/
|
*/
|
||||||
static int getPieceSize(long size) {
|
static int getPieceSize(long size) {
|
||||||
if (size <= 0x1 << 27)
|
if (size <= 0x1 << 30)
|
||||||
return 17
|
return 17
|
||||||
|
|
||||||
for (int i = 28; i <= 37; i++) {
|
for (int i = 31; i <= 37; i++) {
|
||||||
if (size <= 0x1L << i) {
|
if (size <= 0x1L << i) {
|
||||||
return i-10
|
return i-13
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -108,7 +108,7 @@ class FileManager {
|
|||||||
found = rootToFiles.get new InfoHash(e.searchHash)
|
found = rootToFiles.get new InfoHash(e.searchHash)
|
||||||
found = filter(found, e.oobInfohash)
|
found = filter(found, e.oobInfohash)
|
||||||
if (found != null && !found.isEmpty())
|
if (found != null && !found.isEmpty())
|
||||||
re = new ResultsEvent(results: found.asList(), uuid: e.uuid)
|
re = new ResultsEvent(results: found.asList(), uuid: e.uuid, searchEvent: e)
|
||||||
} else {
|
} else {
|
||||||
def names = index.search e.searchTerms
|
def names = index.search e.searchTerms
|
||||||
Set<File> files = new HashSet<>()
|
Set<File> files = new HashSet<>()
|
||||||
@@ -117,7 +117,7 @@ class FileManager {
|
|||||||
files.each { sharedFiles.add fileToSharedFile[it] }
|
files.each { sharedFiles.add fileToSharedFile[it] }
|
||||||
files = filter(sharedFiles, e.oobInfohash)
|
files = filter(sharedFiles, e.oobInfohash)
|
||||||
if (!sharedFiles.isEmpty())
|
if (!sharedFiles.isEmpty())
|
||||||
re = new ResultsEvent(results: sharedFiles.asList(), uuid: e.uuid)
|
re = new ResultsEvent(results: sharedFiles.asList(), uuid: e.uuid, searchEvent: e)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -62,7 +62,9 @@ class PersisterService extends Service {
|
|||||||
} catch (IllegalArgumentException|NumberFormatException e) {
|
} catch (IllegalArgumentException|NumberFormatException e) {
|
||||||
log.log(Level.WARNING, "couldn't load files",e)
|
log.log(Level.WARNING, "couldn't load files",e)
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
listener.publish(new AllFilesLoadedEvent())
|
||||||
|
}
|
||||||
timer.schedule({persistFiles()} as TimerTask, 0, interval)
|
timer.schedule({persistFiles()} as TimerTask, 0, interval)
|
||||||
loaded = true
|
loaded = true
|
||||||
}
|
}
|
||||||
|
@@ -5,6 +5,7 @@ import com.muwire.core.SharedFile
|
|||||||
|
|
||||||
class ResultsEvent extends Event {
|
class ResultsEvent extends Event {
|
||||||
|
|
||||||
|
SearchEvent searchEvent
|
||||||
SharedFile[] results
|
SharedFile[] results
|
||||||
UUID uuid
|
UUID uuid
|
||||||
}
|
}
|
||||||
|
@@ -12,8 +12,19 @@ class ResultsParser {
|
|||||||
public static UIResultEvent parse(Persona p, UUID uuid, def json) throws InvalidSearchResultException {
|
public static UIResultEvent parse(Persona p, UUID uuid, def json) throws InvalidSearchResultException {
|
||||||
if (json.type != "Result")
|
if (json.type != "Result")
|
||||||
throw new InvalidSearchResultException("not a result json")
|
throw new InvalidSearchResultException("not a result json")
|
||||||
if (json.version != 1)
|
switch(json.version) {
|
||||||
throw new InvalidSearchResultException("unknown version $json.version")
|
case 1:
|
||||||
|
return parseV1(p, uuid, json)
|
||||||
|
case 2:
|
||||||
|
return parseV2(p, uuid, json)
|
||||||
|
default:
|
||||||
|
throw new InvalidSearchResultException("unknown version $json.version")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static parseV1(Persona p, UUID uuid, def json) {
|
||||||
if (json.name == null)
|
if (json.name == null)
|
||||||
throw new InvalidSearchResultException("name missing")
|
throw new InvalidSearchResultException("name missing")
|
||||||
if (json.size == null)
|
if (json.size == null)
|
||||||
@@ -52,4 +63,33 @@ class ResultsParser {
|
|||||||
throw new InvalidSearchResultException("parsing search result failed",e)
|
throw new InvalidSearchResultException("parsing search result failed",e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static UIResultEvent parseV2(Persona p, UUID uuid, def json) {
|
||||||
|
if (json.name == null)
|
||||||
|
throw new InvalidSearchResultException("name missing")
|
||||||
|
if (json.size == null)
|
||||||
|
throw new InvalidSearchResultException("length missing")
|
||||||
|
if (json.infohash == null)
|
||||||
|
throw new InvalidSearchResultException("infohash missing")
|
||||||
|
if (json.pieceSize == null)
|
||||||
|
throw new InvalidSearchResultException("pieceSize missing")
|
||||||
|
if (json.hashList != null)
|
||||||
|
throw new InvalidSearchResultException("V2 result with hashlist")
|
||||||
|
try {
|
||||||
|
String name = DataUtil.readi18nString(Base64.decode(json.name))
|
||||||
|
long size = json.size
|
||||||
|
byte [] infoHash = Base64.decode(json.infohash)
|
||||||
|
if (infoHash.length != InfoHash.SIZE)
|
||||||
|
throw new InvalidSearchResultException("invalid infohash size $infoHash.length")
|
||||||
|
int pieceSize = json.pieceSize
|
||||||
|
return new UIResultEvent( sender : p,
|
||||||
|
name : name,
|
||||||
|
size : size,
|
||||||
|
infohash : new InfoHash(infoHash),
|
||||||
|
pieceSize : pieceSize,
|
||||||
|
uuid: uuid)
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new InvalidSearchResultException("parsing search result failed",e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -46,8 +46,8 @@ class ResultsSender {
|
|||||||
this.me = me
|
this.me = me
|
||||||
}
|
}
|
||||||
|
|
||||||
void sendResults(UUID uuid, SharedFile[] results, Destination target) {
|
void sendResults(UUID uuid, SharedFile[] results, Destination target, boolean oobInfohash) {
|
||||||
log.info("Sending $results.length results for uuid $uuid to ${target.toBase32()}")
|
log.info("Sending $results.length results for uuid $uuid to ${target.toBase32()} oobInfohash : $oobInfohash")
|
||||||
if (target.equals(me.destination)) {
|
if (target.equals(me.destination)) {
|
||||||
results.each {
|
results.each {
|
||||||
long length = it.getFile().length()
|
long length = it.getFile().length()
|
||||||
@@ -64,7 +64,8 @@ class ResultsSender {
|
|||||||
eventBus.publish(uiResultEvent)
|
eventBus.publish(uiResultEvent)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
executor.execute(new ResultSendJob(uuid : uuid, results : results, target: target))
|
executor.execute(new ResultSendJob(uuid : uuid, results : results,
|
||||||
|
target: target, oobInfohash : oobInfohash))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +73,7 @@ class ResultsSender {
|
|||||||
UUID uuid
|
UUID uuid
|
||||||
SharedFile [] results
|
SharedFile [] results
|
||||||
Destination target
|
Destination target
|
||||||
|
boolean oobInfohash
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
@@ -94,19 +96,20 @@ class ResultsSender {
|
|||||||
String encodedName = Base64.encode(baos.toByteArray())
|
String encodedName = Base64.encode(baos.toByteArray())
|
||||||
def obj = [:]
|
def obj = [:]
|
||||||
obj.type = "Result"
|
obj.type = "Result"
|
||||||
obj.version = 1
|
obj.version = oobInfohash ? 2 : 1
|
||||||
obj.name = encodedName
|
obj.name = encodedName
|
||||||
obj.infohash = Base64.encode(it.getInfoHash().getRoot())
|
obj.infohash = Base64.encode(it.getInfoHash().getRoot())
|
||||||
obj.size = it.getFile().length()
|
obj.size = it.getFile().length()
|
||||||
obj.pieceSize = it.getPieceSize()
|
obj.pieceSize = it.getPieceSize()
|
||||||
byte [] hashList = it.getInfoHash().getHashList()
|
if (!oobInfohash) {
|
||||||
def hashListB64 = []
|
byte [] hashList = it.getInfoHash().getHashList()
|
||||||
for (int i = 0; i < hashList.length / InfoHash.SIZE; i++) {
|
def hashListB64 = []
|
||||||
System.arraycopy(hashList, InfoHash.SIZE * i, tmp, 0, InfoHash.SIZE)
|
for (int i = 0; i < hashList.length / InfoHash.SIZE; i++) {
|
||||||
hashListB64 << Base64.encode(tmp)
|
System.arraycopy(hashList, InfoHash.SIZE * i, tmp, 0, InfoHash.SIZE)
|
||||||
|
hashListB64 << Base64.encode(tmp)
|
||||||
|
}
|
||||||
|
obj.hashList = hashListB64
|
||||||
}
|
}
|
||||||
obj.hashList = hashListB64
|
|
||||||
|
|
||||||
def json = jsonOutput.toJson(obj)
|
def json = jsonOutput.toJson(obj)
|
||||||
os.writeShort((short)json.length())
|
os.writeShort((short)json.length())
|
||||||
os.write(json.getBytes(StandardCharsets.US_ASCII))
|
os.write(json.getBytes(StandardCharsets.US_ASCII))
|
||||||
|
@@ -44,7 +44,7 @@ public class SearchManager {
|
|||||||
log.info("No results for search uuid $event.uuid")
|
log.info("No results for search uuid $event.uuid")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resultsSender.sendResults(event.uuid, event.results, target)
|
resultsSender.sendResults(event.uuid, event.results, target, event.searchEvent.oobInfohash)
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean hasLocalSearch(UUID uuid) {
|
boolean hasLocalSearch(UUID uuid) {
|
||||||
|
@@ -11,4 +11,9 @@ class UIResultEvent extends Event {
|
|||||||
long size
|
long size
|
||||||
InfoHash infohash
|
InfoHash infohash
|
||||||
int pieceSize
|
int pieceSize
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
super.toString() + "name:$name size:$size sender:${sender.getHumanReadableName()} pieceSize $pieceSize"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,5 @@
|
|||||||
|
package com.muwire.core.upload
|
||||||
|
|
||||||
|
class ContentRequest extends Request {
|
||||||
|
Range range
|
||||||
|
}
|
@@ -0,0 +1,54 @@
|
|||||||
|
package com.muwire.core.upload
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.channels.FileChannel
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.StandardOpenOption
|
||||||
|
|
||||||
|
import com.muwire.core.connection.Endpoint
|
||||||
|
|
||||||
|
class ContentUploader extends Uploader {
|
||||||
|
|
||||||
|
private final File file
|
||||||
|
private final ContentRequest request
|
||||||
|
|
||||||
|
ContentUploader(File file, ContentRequest request, Endpoint endpoint) {
|
||||||
|
super(endpoint)
|
||||||
|
this.file = file
|
||||||
|
this.request = request
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void respond() {
|
||||||
|
OutputStream os = endpoint.getOutputStream()
|
||||||
|
Range range = request.getRange()
|
||||||
|
if (range.start >= file.length() || range.end >= file.length()) {
|
||||||
|
os.write("416 Range Not Satisfiable\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
os.flush()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
os.write("Content-Range: $range.start-$range.end\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
|
||||||
|
FileChannel channel
|
||||||
|
try {
|
||||||
|
channel = Files.newByteChannel(file.toPath(), EnumSet.of(StandardOpenOption.READ))
|
||||||
|
mapped = channel.map(FileChannel.MapMode.READ_ONLY, range.start, range.end - range.start + 1)
|
||||||
|
byte [] tmp = new byte[0x1 << 13]
|
||||||
|
while(mapped.hasRemaining()) {
|
||||||
|
int start = mapped.position()
|
||||||
|
synchronized(this) {
|
||||||
|
mapped.get(tmp, 0, Math.min(tmp.length, mapped.remaining()))
|
||||||
|
}
|
||||||
|
int read = mapped.position() - start
|
||||||
|
endpoint.getOutputStream().write(tmp, 0, read)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
try {channel?.close() } catch (IOException ignored) {}
|
||||||
|
endpoint.getOutputStream().flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,4 @@
|
|||||||
|
package com.muwire.core.upload
|
||||||
|
|
||||||
|
class HashListRequest extends Request {
|
||||||
|
}
|
@@ -0,0 +1,35 @@
|
|||||||
|
package com.muwire.core.upload
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
|
import com.muwire.core.InfoHash
|
||||||
|
import com.muwire.core.connection.Endpoint
|
||||||
|
|
||||||
|
class HashListUploader extends Uploader {
|
||||||
|
private final InfoHash infoHash
|
||||||
|
private final HashListRequest request
|
||||||
|
|
||||||
|
HashListUploader(Endpoint endpoint, InfoHash infoHash, HashListRequest request) {
|
||||||
|
super(endpoint)
|
||||||
|
this.infoHash = infoHash
|
||||||
|
mapped = ByteBuffer.wrap(infoHash.getHashList())
|
||||||
|
}
|
||||||
|
|
||||||
|
void respond() {
|
||||||
|
OutputStream os = endpoint.getOutputStream()
|
||||||
|
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
os.write("Content-Range: 0-${mapped.remaining()}\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
|
||||||
|
byte[]tmp = new byte[0x1 << 13]
|
||||||
|
while(mapped.hasRemaining()) {
|
||||||
|
int start = mapped.position()
|
||||||
|
synchronized(this) {
|
||||||
|
mapped.get(tmp, 0, Math.min(tmp.length, mapped.remaining()))
|
||||||
|
}
|
||||||
|
int read = mapped.position() - start
|
||||||
|
endpoint.getOutputStream().write(tmp, 0, read)
|
||||||
|
}
|
||||||
|
endpoint.getOutputStream().flush()
|
||||||
|
}
|
||||||
|
}
|
@@ -16,57 +16,12 @@ class Request {
|
|||||||
private static final byte N = "\n".getBytes(StandardCharsets.US_ASCII)[0]
|
private static final byte N = "\n".getBytes(StandardCharsets.US_ASCII)[0]
|
||||||
|
|
||||||
InfoHash infoHash
|
InfoHash infoHash
|
||||||
Range range
|
|
||||||
Persona downloader
|
Persona downloader
|
||||||
Map<String, String> headers
|
Map<String, String> headers
|
||||||
|
|
||||||
static Request parse(InfoHash infoHash, InputStream is) throws IOException {
|
static Request parseContentRequest(InfoHash infoHash, InputStream is) throws IOException {
|
||||||
Map<String,String> headers = new HashMap<>()
|
|
||||||
byte [] tmp = new byte[Constants.MAX_HEADER_SIZE]
|
Map<String, String> headers = parseHeaders(is)
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!headers.containsKey("Range"))
|
if (!headers.containsKey("Range"))
|
||||||
throw new IOException("Range header not found")
|
throw new IOException("Range header not found")
|
||||||
@@ -93,7 +48,69 @@ class Request {
|
|||||||
def decoded = Base64.decode(encoded)
|
def decoded = Base64.decode(encoded)
|
||||||
downloader = new Persona(new ByteArrayInputStream(decoded))
|
downloader = new Persona(new ByteArrayInputStream(decoded))
|
||||||
}
|
}
|
||||||
new Request( infoHash : infoHash, range : new Range(start, end), headers : headers, downloader : downloader)
|
new ContentRequest( infoHash : infoHash, range : new Range(start, end),
|
||||||
|
headers : headers, downloader : downloader)
|
||||||
|
}
|
||||||
|
|
||||||
|
static Request parseHashListRequest(InfoHash infoHash, InputStream is) throws IOException {
|
||||||
|
Map<String,String> headers = parseHeaders(is)
|
||||||
|
Persona downloader = null
|
||||||
|
if (headers.containsKey("X-Persona")) {
|
||||||
|
def encoded = headers["X-Persona"].trim()
|
||||||
|
def decoded = Base64.decode(encoded)
|
||||||
|
downloader = new Persona(new ByteArrayInputStream(decoded))
|
||||||
|
}
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -23,9 +23,9 @@ public class UploadManager {
|
|||||||
this.fileManager = fileManager
|
this.fileManager = fileManager
|
||||||
}
|
}
|
||||||
|
|
||||||
public void processEndpoint(Endpoint e) throws IOException {
|
public void processGET(Endpoint e) throws IOException {
|
||||||
byte [] infoHashStringBytes = new byte[44]
|
byte [] infoHashStringBytes = new byte[44]
|
||||||
DataInputStream dis = new DataInputStream(e.getInputStream())
|
DataInputStream dis = new DataInputStream(e.getInputStream())
|
||||||
boolean first = true
|
boolean first = true
|
||||||
while(true) {
|
while(true) {
|
||||||
if (first)
|
if (first)
|
||||||
@@ -61,13 +61,13 @@ public class UploadManager {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Request request = Request.parse(new InfoHash(infoHashRoot), e.getInputStream())
|
Request request = Request.parseContentRequest(new InfoHash(infoHashRoot), e.getInputStream())
|
||||||
if (request.downloader != null && request.downloader.destination != e.destination) {
|
if (request.downloader != null && request.downloader.destination != e.destination) {
|
||||||
log.info("Downloader persona doesn't match their destination")
|
log.info("Downloader persona doesn't match their destination")
|
||||||
e.close()
|
e.close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Uploader uploader = new Uploader(sharedFiles.iterator().next().file, request, e)
|
Uploader uploader = new ContentUploader(sharedFiles.iterator().next().file, request, e)
|
||||||
eventBus.publish(new UploadEvent(uploader : uploader))
|
eventBus.publish(new UploadEvent(uploader : uploader))
|
||||||
try {
|
try {
|
||||||
uploader.respond()
|
uploader.respond()
|
||||||
@@ -75,7 +75,92 @@ public class UploadManager {
|
|||||||
eventBus.publish(new UploadFinishedEvent(uploader : uploader))
|
eventBus.publish(new UploadFinishedEvent(uploader : uploader))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processHashList(Endpoint e) {
|
||||||
|
byte [] infoHashStringBytes = new byte[44]
|
||||||
|
DataInputStream dis = new DataInputStream(e.getInputStream())
|
||||||
|
dis.readFully(infoHashStringBytes)
|
||||||
|
String infoHashString = new String(infoHashStringBytes, StandardCharsets.US_ASCII)
|
||||||
|
log.info("Responding to hashlist request for root $infoHashString")
|
||||||
|
|
||||||
|
byte [] infoHashRoot = Base64.decode(infoHashString)
|
||||||
|
Set<SharedFile> sharedFiles = fileManager.getSharedFiles(infoHashRoot)
|
||||||
|
if (sharedFiles == null || sharedFiles.isEmpty()) {
|
||||||
|
log.info "file not found"
|
||||||
|
e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
e.getOutputStream().flush()
|
||||||
|
e.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
byte [] rn = new byte[2]
|
||||||
|
dis.readFully(rn)
|
||||||
|
if (rn != "\r\n".getBytes(StandardCharsets.US_ASCII)) {
|
||||||
|
log.warning("Malformed HASHLIST header")
|
||||||
|
e.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Request request = Request.parseHashListRequest(new InfoHash(infoHashRoot), e.getInputStream())
|
||||||
|
if (request.downloader != null && request.downloader.destination != e.destination) {
|
||||||
|
log.info("Downloader persona doesn't match their destination")
|
||||||
|
e.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Uploader uploader = new HashListUploader(e, sharedFiles.iterator().next().infoHash, request)
|
||||||
|
eventBus.publish(new UploadEvent(uploader : uploader))
|
||||||
|
try {
|
||||||
|
uploader.respond()
|
||||||
|
} finally {
|
||||||
|
eventBus.publish(new UploadFinishedEvent(uploader : uploader))
|
||||||
|
}
|
||||||
|
|
||||||
|
// proceed with content
|
||||||
|
while(true) {
|
||||||
|
byte[] get = new byte[4]
|
||||||
|
dis.readFully(get)
|
||||||
|
if (get != "GET ".getBytes(StandardCharsets.US_ASCII)) {
|
||||||
|
log.warning("received a method other than GET on subsequent call")
|
||||||
|
e.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dis.readFully(infoHashStringBytes)
|
||||||
|
infoHashString = new String(infoHashStringBytes, StandardCharsets.US_ASCII)
|
||||||
|
log.info("Responding to upload request for root $infoHashString")
|
||||||
|
|
||||||
|
infoHashRoot = Base64.decode(infoHashString)
|
||||||
|
sharedFiles = fileManager.getSharedFiles(infoHashRoot)
|
||||||
|
if (sharedFiles == null || sharedFiles.isEmpty()) {
|
||||||
|
log.info "file not found"
|
||||||
|
e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
e.getOutputStream().flush()
|
||||||
|
e.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rn = new byte[2]
|
||||||
|
dis.readFully(rn)
|
||||||
|
if (rn != "\r\n".getBytes(StandardCharsets.US_ASCII)) {
|
||||||
|
log.warning("Malformed GET header")
|
||||||
|
e.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
request = Request.parseContentRequest(new InfoHash(infoHashRoot), e.getInputStream())
|
||||||
|
if (request.downloader != null && request.downloader.destination != e.destination) {
|
||||||
|
log.info("Downloader persona doesn't match their destination")
|
||||||
|
e.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uploader = new ContentUploader(sharedFiles.iterator().next().file, request, e)
|
||||||
|
eventBus.publish(new UploadEvent(uploader : uploader))
|
||||||
|
try {
|
||||||
|
uploader.respond()
|
||||||
|
} finally {
|
||||||
|
eventBus.publish(new UploadFinishedEvent(uploader : uploader))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -8,49 +8,16 @@ import java.nio.file.StandardOpenOption
|
|||||||
|
|
||||||
import com.muwire.core.connection.Endpoint
|
import com.muwire.core.connection.Endpoint
|
||||||
|
|
||||||
class Uploader {
|
abstract class Uploader {
|
||||||
private final File file
|
protected final Endpoint endpoint
|
||||||
private final Request request
|
protected ByteBuffer mapped
|
||||||
private final Endpoint endpoint
|
|
||||||
private ByteBuffer mapped
|
|
||||||
|
|
||||||
Uploader(File file, Request request, Endpoint endpoint) {
|
Uploader(Endpoint endpoint) {
|
||||||
this.file = file
|
|
||||||
this.request = request
|
|
||||||
this.endpoint = endpoint
|
this.endpoint = endpoint
|
||||||
}
|
}
|
||||||
|
|
||||||
void respond() {
|
abstract void respond()
|
||||||
OutputStream os = endpoint.getOutputStream()
|
|
||||||
Range range = request.getRange()
|
|
||||||
if (range.start >= file.length() || range.end >= file.length()) {
|
|
||||||
os.write("416 Range Not Satisfiable\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
|
||||||
os.flush()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
|
|
||||||
os.write("Content-Range: $range.start-$range.end\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
|
||||||
|
|
||||||
FileChannel channel
|
|
||||||
try {
|
|
||||||
channel = Files.newByteChannel(file.toPath(), EnumSet.of(StandardOpenOption.READ))
|
|
||||||
mapped = channel.map(FileChannel.MapMode.READ_ONLY, range.start, range.end - range.start + 1)
|
|
||||||
byte [] tmp = new byte[0x1 << 13]
|
|
||||||
while(mapped.hasRemaining()) {
|
|
||||||
int start = mapped.position()
|
|
||||||
synchronized(this) {
|
|
||||||
mapped.get(tmp, 0, Math.min(tmp.length, mapped.remaining()))
|
|
||||||
}
|
|
||||||
int read = mapped.position() - start
|
|
||||||
endpoint.getOutputStream().write(tmp, 0, read)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
try {channel?.close() } catch (IOException ignored) {}
|
|
||||||
endpoint.getOutputStream().flush()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized int getPosition() {
|
public synchronized int getPosition() {
|
||||||
if (mapped == null)
|
if (mapped == null)
|
||||||
return -1
|
return -1
|
||||||
|
@@ -181,6 +181,8 @@ Search results are sent through and HTTP POST method from the responder to the o
|
|||||||
* The "altlocs" list contains list of alternate personas that the responder thinks may also have the file.
|
* The "altlocs" list contains list of alternate personas that the responder thinks may also have the file.
|
||||||
* The "pieceSize" field is the size of the each individual file piece (except possibly the last) in powers of 2
|
* The "pieceSize" field is the size of the each individual file piece (except possibly the last) in powers of 2
|
||||||
|
|
||||||
|
Results version 1 contain the full hashlist, version 2 does not contain that list. See the "infohash-upgrade" document for more information.
|
||||||
|
|
||||||
### "Who do you trust" query - any node to any node
|
### "Who do you trust" query - any node to any node
|
||||||
(See the "web-of-trust" document for more info on this query)
|
(See the "web-of-trust" document for more info on this query)
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
group = com.muwire
|
group = com.muwire
|
||||||
version = 0.1.4
|
version = 0.1.8
|
||||||
groovyVersion = 2.4.15
|
groovyVersion = 2.4.15
|
||||||
slf4jVersion = 1.7.25
|
slf4jVersion = 1.7.25
|
||||||
spockVersion = 1.1-groovy-2.4
|
spockVersion = 1.1-groovy-2.4
|
||||||
|
@@ -15,6 +15,7 @@ import javax.inject.Inject
|
|||||||
import com.muwire.core.Constants
|
import com.muwire.core.Constants
|
||||||
import com.muwire.core.Core
|
import com.muwire.core.Core
|
||||||
import com.muwire.core.download.DownloadStartedEvent
|
import com.muwire.core.download.DownloadStartedEvent
|
||||||
|
import com.muwire.core.download.UIDownloadCancelledEvent
|
||||||
import com.muwire.core.download.UIDownloadEvent
|
import com.muwire.core.download.UIDownloadEvent
|
||||||
import com.muwire.core.search.QueryEvent
|
import com.muwire.core.search.QueryEvent
|
||||||
import com.muwire.core.search.SearchEvent
|
import com.muwire.core.search.SearchEvent
|
||||||
@@ -52,7 +53,7 @@ class MainFrameController {
|
|||||||
// this can be improved a lot
|
// this can be improved a lot
|
||||||
def replaced = search.toLowerCase().trim().replaceAll(Constants.SPLIT_PATTERN, " ")
|
def replaced = search.toLowerCase().trim().replaceAll(Constants.SPLIT_PATTERN, " ")
|
||||||
def terms = replaced.split(" ")
|
def terms = replaced.split(" ")
|
||||||
searchEvent = new SearchEvent(searchTerms : terms, uuid : uuid, oobInfohash: false)
|
searchEvent = new SearchEvent(searchTerms : terms, uuid : uuid, oobInfohash: true)
|
||||||
}
|
}
|
||||||
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true,
|
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true,
|
||||||
replyTo: core.me.destination, receivedOn: core.me.destination,
|
replyTo: core.me.destination, receivedOn: core.me.destination,
|
||||||
@@ -69,7 +70,8 @@ class MainFrameController {
|
|||||||
def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params)
|
def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params)
|
||||||
model.results[uuid.toString()] = group
|
model.results[uuid.toString()] = group
|
||||||
|
|
||||||
def searchEvent = new SearchEvent(searchHash : Base64.decode(infoHash), uuid:uuid)
|
def searchEvent = new SearchEvent(searchHash : Base64.decode(infoHash), uuid:uuid,
|
||||||
|
oobInfohash: true)
|
||||||
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true,
|
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true,
|
||||||
replyTo: core.me.destination, receivedOn: core.me.destination,
|
replyTo: core.me.destination, receivedOn: core.me.destination,
|
||||||
originator : core.me))
|
originator : core.me))
|
||||||
@@ -133,6 +135,7 @@ class MainFrameController {
|
|||||||
void cancel() {
|
void cancel() {
|
||||||
def downloader = model.downloads[selectedDownload()].downloader
|
def downloader = model.downloads[selectedDownload()].downloader
|
||||||
downloader.cancel()
|
downloader.cancel()
|
||||||
|
core.eventBus.publish(new UIDownloadCancelledEvent(downloader : downloader))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ControllerAction
|
@ControllerAction
|
||||||
|
@@ -1,18 +1,24 @@
|
|||||||
import griffon.core.GriffonApplication
|
import griffon.core.GriffonApplication
|
||||||
import groovy.util.logging.Log
|
import groovy.util.logging.Log
|
||||||
|
import net.i2p.util.SystemVersion
|
||||||
|
|
||||||
import org.codehaus.griffon.runtime.core.AbstractLifecycleHandler
|
import org.codehaus.griffon.runtime.core.AbstractLifecycleHandler
|
||||||
|
|
||||||
import com.muwire.core.Core
|
import com.muwire.core.Core
|
||||||
import com.muwire.core.MuWireSettings
|
import com.muwire.core.MuWireSettings
|
||||||
|
import com.muwire.gui.UISettings
|
||||||
|
|
||||||
import javax.annotation.Nonnull
|
import javax.annotation.Nonnull
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.swing.JTable
|
import javax.swing.JTable
|
||||||
|
import javax.swing.LookAndFeel
|
||||||
|
import javax.swing.UIManager
|
||||||
|
|
||||||
import static griffon.util.GriffonApplicationUtils.isMacOSX
|
import static griffon.util.GriffonApplicationUtils.isMacOSX
|
||||||
import static groovy.swing.SwingBuilder.lookAndFeel
|
import static groovy.swing.SwingBuilder.lookAndFeel
|
||||||
|
|
||||||
|
import java.awt.Font
|
||||||
|
|
||||||
@Log
|
@Log
|
||||||
class Initialize extends AbstractLifecycleHandler {
|
class Initialize extends AbstractLifecycleHandler {
|
||||||
@Inject
|
@Inject
|
||||||
@@ -22,11 +28,82 @@ class Initialize extends AbstractLifecycleHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
void execute() {
|
void execute() {
|
||||||
if (isMacOSX()) {
|
log.info "Loading home dir"
|
||||||
lookAndFeel('nimbus') // otherwise the file chooser doesn't open???
|
def portableHome = System.getProperty("portable.home")
|
||||||
} else {
|
def home = portableHome == null ?
|
||||||
lookAndFeel('system', 'gtk')
|
selectHome() :
|
||||||
|
portableHome
|
||||||
|
|
||||||
|
home = new File(home)
|
||||||
|
if (!home.exists()) {
|
||||||
|
log.info("creating home dir $home")
|
||||||
|
home.mkdirs()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
application.context.put("muwire-home", home.getAbsolutePath())
|
||||||
|
|
||||||
|
def guiPropsFile = new File(home, "gui.properties")
|
||||||
|
UISettings uiSettings
|
||||||
|
if (guiPropsFile.exists()) {
|
||||||
|
Properties props = new Properties()
|
||||||
|
guiPropsFile.withInputStream { props.load(it) }
|
||||||
|
uiSettings = new UISettings(props)
|
||||||
|
|
||||||
|
log.info("settting user-specified lnf $uiSettings.lnf")
|
||||||
|
lookAndFeel(uiSettings.lnf)
|
||||||
|
|
||||||
|
if (uiSettings.font != null) {
|
||||||
|
log.info("setting user-specified font $uiSettings.font")
|
||||||
|
Font font = new Font(uiSettings.font, Font.PLAIN, 12)
|
||||||
|
def defaults = UIManager.getDefaults()
|
||||||
|
defaults.put("Button.font", font)
|
||||||
|
defaults.put("RadioButton.font", font)
|
||||||
|
defaults.put("Label.font", font)
|
||||||
|
defaults.put("CheckBox.font", font)
|
||||||
|
defaults.put("Table.font", font)
|
||||||
|
defaults.put("TableHeader.font", font)
|
||||||
|
// TODO: add others
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Properties props = new Properties()
|
||||||
|
uiSettings = new UISettings(props)
|
||||||
|
log.info "will try default lnfs"
|
||||||
|
if (isMacOSX()) {
|
||||||
|
if (SystemVersion.isJava9()) {
|
||||||
|
uiSettings.lnf = "metal"
|
||||||
|
lookAndFeel("metal")
|
||||||
|
} else {
|
||||||
|
uiSettings.lnf = "nimbus"
|
||||||
|
lookAndFeel('nimbus') // otherwise the file chooser doesn't open???
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LookAndFeel chosen = lookAndFeel('system', 'gtk')
|
||||||
|
uiSettings.lnf = chosen.name
|
||||||
|
log.info("ended up applying $chosen.name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
application.context.put("ui-settings", uiSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String selectHome() {
|
||||||
|
def home = new File(System.properties["user.home"])
|
||||||
|
def defaultHome = new File(home, ".MuWire")
|
||||||
|
if (defaultHome.exists())
|
||||||
|
return defaultHome.getAbsolutePath()
|
||||||
|
if (SystemVersion.isMac()) {
|
||||||
|
def library = new File(home, "Library")
|
||||||
|
def appSupport = new File(library, "Application Support")
|
||||||
|
def muwire = new File(appSupport,"MuWire")
|
||||||
|
return muwire.getAbsolutePath()
|
||||||
|
}
|
||||||
|
if (SystemVersion.isWindows()) {
|
||||||
|
def appData = new File(home,"AppData")
|
||||||
|
def roaming = new File(appData, "Roaming")
|
||||||
|
def muwire = new File(roaming, "MuWire")
|
||||||
|
return muwire.getAbsolutePath()
|
||||||
|
}
|
||||||
|
defaultHome.getAbsolutePath()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -34,17 +34,8 @@ class Ready extends AbstractLifecycleHandler {
|
|||||||
@Override
|
@Override
|
||||||
void execute() {
|
void execute() {
|
||||||
log.info "starting core services"
|
log.info "starting core services"
|
||||||
def portableHome = System.getProperty("portable.home")
|
|
||||||
def home = portableHome == null ?
|
|
||||||
selectHome() :
|
|
||||||
portableHome
|
|
||||||
|
|
||||||
home = new File(home)
|
|
||||||
if (!home.exists()) {
|
|
||||||
log.info("creating home dir $home")
|
|
||||||
home.mkdirs()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
def home = new File(application.getContext().getAsString("muwire-home"))
|
||||||
def props = new Properties()
|
def props = new Properties()
|
||||||
def propsFile = new File(home, "MuWire.properties")
|
def propsFile = new File(home, "MuWire.properties")
|
||||||
if (propsFile.exists()) {
|
if (propsFile.exists()) {
|
||||||
@@ -121,25 +112,5 @@ class Ready extends AbstractLifecycleHandler {
|
|||||||
|
|
||||||
core.eventBus.publish(new UILoadedEvent())
|
core.eventBus.publish(new UILoadedEvent())
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String selectHome() {
|
|
||||||
def home = new File(System.properties["user.home"])
|
|
||||||
def defaultHome = new File(home, ".MuWire")
|
|
||||||
if (defaultHome.exists())
|
|
||||||
return defaultHome.getAbsolutePath()
|
|
||||||
if (SystemVersion.isMac()) {
|
|
||||||
def library = new File(home, "Library")
|
|
||||||
def appSupport = new File(library, "Application Support")
|
|
||||||
def muwire = new File(appSupport,"MuWire")
|
|
||||||
return muwire.getAbsolutePath()
|
|
||||||
}
|
|
||||||
if (SystemVersion.isWindows()) {
|
|
||||||
def appData = new File(home,"AppData")
|
|
||||||
def roaming = new File(appData, "Roaming")
|
|
||||||
def muwire = new File(roaming, "MuWire")
|
|
||||||
return muwire.getAbsolutePath()
|
|
||||||
}
|
|
||||||
defaultHome.getAbsolutePath()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
23
gui/src/main/groovy/com/muwire/gui/UISettings.groovy
Normal file
23
gui/src/main/groovy/com/muwire/gui/UISettings.groovy
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package com.muwire.gui
|
||||||
|
|
||||||
|
class UISettings {
|
||||||
|
|
||||||
|
String lnf
|
||||||
|
boolean showMonitor
|
||||||
|
String font
|
||||||
|
|
||||||
|
UISettings(Properties props) {
|
||||||
|
lnf = props.getProperty("lnf", "system")
|
||||||
|
showMonitor = Boolean.parseBoolean(props.getProperty("showMonitor", "true"))
|
||||||
|
font = props.getProperty("font",null)
|
||||||
|
}
|
||||||
|
|
||||||
|
void write(OutputStream out) throws IOException {
|
||||||
|
Properties props = new Properties()
|
||||||
|
props.setProperty("lnf", lnf)
|
||||||
|
props.setProperty("showMonitor", showMonitor)
|
||||||
|
if (font != null)
|
||||||
|
props.setProperty("font", font)
|
||||||
|
props.store(out, "UI Properties")
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user