Compare commits

...

15 Commits

22 changed files with 153 additions and 58 deletions

View File

@ -1,10 +1,10 @@
# MuWire - Easy Anonymous File-Sharing
MuWire is an easy to use file-sharing program which offers anonymity using [I2P technology](http://geti2p.net).
MuWire is an easy to use file-sharing program which offers anonymity using [I2P technology](http://geti2p.net). It works on any platform Java works on, including Windows,MacOS,Linux.
It is inspired by the LimeWire Gnutella client and developped by a former LimeWire developer.
The project is in development. You can find technical documentation in the "doc" folder.
The first stable release - 0.1.0 is avaiable for download at http://muwire.com. You can find technical documentation in the "doc" folder.
### Building
@ -23,15 +23,14 @@ Some of the UI tests will fail because they haven't been written yet :-/
### Running
You need to have an I2P router up and running on the same machine. After you build the application, look inside "gui/build/distributions". Untar/unzip one of the "shadow" files and then run the jar contained inside.
You need to have an I2P router up and running on the same machine. After you build the application, look inside "gui/build/distributions". Untar/unzip one of the "shadow" files and then run the jar contained inside by typing "java -jar MuWire-x.y.z.jar" in a terminal or command prompt. If you use a custom I2CP host and port, create a file $HOME/.MuWire/i2p.properties and put "i2cp.tcp.host=<host>" and "i2cp.tcp.post=<port>" in there.
The first time you run MuWire it will ask you to select a nickname. This nickname will be displayed with search results, so that others can verify the file was shared by you.
At the moment there are very few nodes on the network, so you will see very few connections and search results. It is best to leave MuWire running all the time, just like I2P.
The first time you run MuWire it will ask you to select a nickname. This nickname will be displayed with search results, so that others can verify the file was shared by you. It is best to leave MuWire running all the time, just like I2P.
### Known bugs and limitations
* Many UI features you would expect are not there yet
* On windows the preferences are stored in %HOME%\.MuWire instead of the user profile directory
* Downloads in progress do not get remembered between restarts

View File

@ -34,7 +34,7 @@ class Cli {
Core core
try {
core = new Core(props, home, "0.1.0")
core = new Core(props, home, "0.1.1")
} catch (Exception bad) {
bad.printStackTrace(System.out)
println "Failed to initialize core, exiting"

View File

@ -53,7 +53,7 @@ class CliDownloader {
Core core
try {
core = new Core(props, home, "0.1.0")
core = new Core(props, home, "0.1.1")
} catch (Exception bad) {
bad.printStackTrace(System.out)
println "Failed to initialize core, exiting"

View File

@ -90,6 +90,10 @@ public class Core {
def i2pOptionsFile = new File(home,"i2p.properties")
if (i2pOptionsFile.exists()) {
i2pOptionsFile.withInputStream { i2pOptions.load(it) }
if (!i2pOptions.hasProperty("inbound.nickname"))
i2pOptions["inbound.nickname"] = "MuWire"
if (!i2pOptions.hasProperty("outbound.nickname"))
i2pOptions["outbound.nickname"] = "MuWire"
} else {
i2pOptions["inbound.nickname"] = "MuWire"
i2pOptions["outbound.nickname"] = "MuWire"
@ -249,7 +253,7 @@ public class Core {
}
}
Core core = new Core(props, home, "0.1.0")
Core core = new Core(props, home, "0.1.1")
core.startServices()
// ... at the end, sleep or execute script

View File

@ -127,6 +127,7 @@ abstract class Connection implements Closeable {
query.uuid = e.searchEvent.getUuid()
query.firstHop = e.firstHop
query.keywords = e.searchEvent.getSearchTerms()
query.oobInfohash = e.searchEvent.oobInfohash
if (e.searchEvent.searchHash != null)
query.infohash = Base64.encode(e.searchEvent.searchHash)
query.replyTo = e.replyTo.toBase64()
@ -183,10 +184,14 @@ abstract class Connection implements Closeable {
}
}
boolean oob = false
if (search.oobInfohash != null)
oob = search.oobInfohash
SearchEvent searchEvent = new SearchEvent(searchTerms : search.keywords,
searchHash : infohash,
uuid : uuid)
uuid : uuid,
oobInfohash : oob)
QueryEvent event = new QueryEvent ( searchEvent : searchEvent,
replyTo : replyTo,
originator : originator,

View File

@ -205,7 +205,7 @@ public class Downloader {
if (downloaded.isComplete() && !eventFired) {
piecesFile.delete()
eventFired = true
eventBus.publish(new FileDownloadedEvent(downloadedFile : new DownloadedFile(file, infoHash, Collections.emptySet())))
eventBus.publish(new FileDownloadedEvent(downloadedFile : new DownloadedFile(file, infoHash, pieceSize, Collections.emptySet())))
}
endpoint?.close()
}

View File

@ -17,12 +17,12 @@ class FileHasher {
* @return the size of each piece in power of 2
*/
static int getPieceSize(long size) {
if (size <= 0x1 << 25)
return 18
if (size <= 0x1 << 27)
return 17
for (int i = 26; i <= 37; i++) {
for (int i = 28; i <= 37; i++) {
if (size <= 0x1L << i) {
return i-7
return i-10
}
}

View File

@ -26,20 +26,20 @@ class FileManager {
this.eventBus = eventBus
}
void onFileHashedEvent(FileHashedEvent e) {
if (settings.shareDownloadedFiles) {
if (e.sharedFile != null)
addToIndex(e.sharedFile)
}
}
void onFileHashedEvent(FileHashedEvent e) {
if (e.sharedFile != null)
addToIndex(e.sharedFile)
}
void onFileLoadedEvent(FileLoadedEvent e) {
addToIndex(e.loadedFile)
}
void onFileDownloadedEvent(FileDownloadedEvent e) {
addToIndex(e.downloadedFile)
}
void onFileDownloadedEvent(FileDownloadedEvent e) {
if (settings.shareDownloadedFiles) {
addToIndex(e.downloadedFile)
}
}
private void addToIndex(SharedFile sf) {
log.info("Adding shared file " + sf.getFile())
@ -105,6 +105,7 @@ class FileManager {
if (e.searchHash != null) {
Set<SharedFile> found
found = rootToFiles.get new InfoHash(e.searchHash)
found = filter(found, e.oobInfohash)
if (found != null && !found.isEmpty())
re = new ResultsEvent(results: found.asList(), uuid: e.uuid)
} else {
@ -113,6 +114,7 @@ class FileManager {
names.each { files.addAll nameToFiles.getOrDefault(it, []) }
Set<SharedFile> sharedFiles = new HashSet<>()
files.each { sharedFiles.add fileToSharedFile[it] }
files = filter(sharedFiles, e.oobInfohash)
if (!sharedFiles.isEmpty())
re = new ResultsEvent(results: sharedFiles.asList(), uuid: e.uuid)
@ -121,4 +123,15 @@ class FileManager {
if (re != null)
eventBus.publish(re)
}
private static Set<SharedFile> filter(Set<SharedFile> files, boolean oob) {
if (!oob)
return files
Set<SharedFile> rv = new HashSet<>()
files.each {
if (it.getPieceSize() != 0)
rv.add(it)
}
rv
}
}

View File

@ -40,7 +40,7 @@ class HasherService {
eventBus.publish new FileHashedEvent(error: "$f is too large to be shared ${f.length()}")
} else {
def hash = hasher.hashFile f
eventBus.publish new FileHashedEvent(sharedFile: new SharedFile(f, hash))
eventBus.publish new FileHashedEvent(sharedFile: new SharedFile(f, hash, FileHasher.getPieceSize(f.length())))
}
}
}

View File

@ -98,14 +98,19 @@ class PersisterService extends Service {
if (!Arrays.equals(root, ih.getRoot()))
return null
if (json.sources != null) {
int pieceSize = 0
if (json.pieceSize != null)
pieceSize = json.pieceSize
if (json.sources != null) {
List sources = (List)json.sources
Set<Destination> sourceSet = sources.stream().map({d -> new Destination(d.toString())}).collect Collectors.toSet()
DownloadedFile df = new DownloadedFile(file, ih, sourceSet)
DownloadedFile df = new DownloadedFile(file, ih, pieceSize, sourceSet)
return new FileLoadedEvent(loadedFile : df)
}
SharedFile sf = new SharedFile(file, ih)
SharedFile sf = new SharedFile(file, ih, pieceSize)
return new FileLoadedEvent(loadedFile: sf)
}
@ -132,6 +137,7 @@ class PersisterService extends Service {
json.length = f.length()
InfoHash ih = sf.getInfoHash()
json.infoHash = Base64.encode ih.getRoot()
json.pieceSize = sf.getPieceSize()
byte [] tmp = new byte [32]
json.hashList = []
for (int i = 0;i < ih.getHashList().length / 32; i++) {

View File

@ -51,11 +51,14 @@ class ResultsSender {
if (target.equals(me.destination)) {
results.each {
long length = it.getFile().length()
int pieceSize = it.getPieceSize()
if (pieceSize == 0)
pieceSize = FileHasher.getPieceSize(length)
def uiResultEvent = new UIResultEvent( sender : me,
name : it.getFile().getName(),
size : length,
infohash : it.getInfoHash(),
pieceSize : FileHasher.getPieceSize(length),
pieceSize : pieceSize,
uuid : uuid
)
eventBus.publish(uiResultEvent)
@ -95,7 +98,7 @@ class ResultsSender {
obj.name = encodedName
obj.infohash = Base64.encode(it.getInfoHash().getRoot())
obj.size = it.getFile().length()
obj.pieceSize = FileHasher.getPieceSize(it.getFile().length())
obj.pieceSize = it.getPieceSize()
byte [] hashList = it.getInfoHash().getHashList()
def hashListB64 = []
for (int i = 0; i < hashList.length / InfoHash.SIZE; i++) {

View File

@ -8,11 +8,12 @@ class SearchEvent extends Event {
List<String> searchTerms
byte [] searchHash
UUID uuid
boolean oobInfohash
String toString() {
def infoHash = null
if (searchHash != null)
infoHash = new InfoHash(searchHash)
"searchTerms: $searchTerms searchHash:$infoHash, uuid:$uuid"
"searchTerms: $searchTerms searchHash:$infoHash, uuid:$uuid oobInfohash:$oobInfohash"
}
}

View File

@ -9,8 +9,8 @@ public class DownloadedFile extends SharedFile {
private final Set<Destination> sources;
public DownloadedFile(File file, InfoHash infoHash, Set<Destination> sources) {
super(file, infoHash);
public DownloadedFile(File file, InfoHash infoHash, int pieceSize, Set<Destination> sources) {
super(file, infoHash, pieceSize);
this.sources = sources;
}

View File

@ -6,10 +6,12 @@ public class SharedFile {
private final File file;
private final InfoHash infoHash;
private final int pieceSize;
public SharedFile(File file, InfoHash infoHash) {
public SharedFile(File file, InfoHash infoHash, int pieceSize) {
this.file = file;
this.infoHash = infoHash;
this.pieceSize = pieceSize;
}
public File getFile() {
@ -20,4 +22,7 @@ public class SharedFile {
return infoHash;
}
public int getPieceSize() {
return pieceSize;
}
}

View File

@ -35,7 +35,7 @@ class FileManagerTest {
void testHash1Result() {
File f = new File("a b.c")
InfoHash ih = InfoHash.fromHashList(new byte[32])
SharedFile sf = new SharedFile(f,ih)
SharedFile sf = new SharedFile(f,ih, 0)
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
manager.onFileHashedEvent(fhe)
@ -54,8 +54,8 @@ class FileManagerTest {
@Test
void testHash2Results() {
InfoHash ih = InfoHash.fromHashList(new byte[32])
SharedFile sf1 = new SharedFile(new File("a b.c"), ih)
SharedFile sf2 = new SharedFile(new File("d e.f"), ih)
SharedFile sf1 = new SharedFile(new File("a b.c"), ih, 0)
SharedFile sf2 = new SharedFile(new File("d e.f"), ih, 0)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf1)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf2)
@ -76,7 +76,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)
SharedFile sf = new SharedFile(f,ih, 0)
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
manager.onFileHashedEvent(fhe)
@ -90,7 +90,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)
SharedFile sf = new SharedFile(f,ih,0)
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
manager.onFileHashedEvent(fhe)
@ -108,12 +108,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)
SharedFile sf1 = new SharedFile(f1, ih1, 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)
SharedFile sf2 = new SharedFile(f2, ih2, 0)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf2)
UUID uuid = UUID.randomUUID()
@ -131,7 +131,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)
SharedFile sf = new SharedFile(f,ih,0)
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
manager.onFileHashedEvent(fhe)
@ -144,8 +144,8 @@ class FileManagerTest {
@Test
void testRemoveFileExistingHash() {
InfoHash ih = InfoHash.fromHashList(new byte[32])
SharedFile sf1 = new SharedFile(new File("a b.c"), ih)
SharedFile sf2 = new SharedFile(new File("d e.f"), ih)
SharedFile sf1 = new SharedFile(new File("a b.c"), ih, 0)
SharedFile sf2 = new SharedFile(new File("d e.f"), ih, 0)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf1)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf2)
@ -162,12 +162,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)
SharedFile sf1 = new SharedFile(f1, ih1, 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)
SharedFile sf2 = new SharedFile(f2, ih2, 0)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf2)
manager.onFileUnsharedEvent new FileUnsharedEvent(unsharedFile: sf2)

View File

@ -55,7 +55,7 @@ class PersisterServiceSavingTest {
@Test
void testSavingSharedFile() {
sf = new SharedFile(f, ih)
sf = new SharedFile(f, ih, 0)
ps = new PersisterService(persisted, eventBus, 100, fileSource)
ps.start()
@ -74,7 +74,7 @@ class PersisterServiceSavingTest {
@Test
void testSavingDownloadedFile() {
Destinations dests = new Destinations()
sf = new DownloadedFile(f, ih, new HashSet([dests.dest1, dests.dest2]))
sf = new DownloadedFile(f, ih, 0, new HashSet([dests.dest1, dests.dest2]))
ps = new PersisterService(persisted, eventBus, 100, fileSource)
ps.start()

37
doc/infohash-upgrade.md Normal file
View File

@ -0,0 +1,37 @@
# InfoHash Upgrade
An infohash is a list of hashes of the pieces of the file. In MuWire 0.1.0 the piece size is determined by policy based on the file size, with the intention being to keep the list of hashes to maximum 128 in number. The reason for this is that infohashes get returned with search results, and smaller piece size will result in larger infohash which will slow down the transmission of search results.
### The problem
This presents the following problem - larger files have larger piece sizes: a 2GB file will have a 16MB piece size, a 4GB file 32MB and so on. Pieces are atomic, i.e. if a download fails halfway through a piece it will resume since the beginning of the piece. Unfortunately in the current state of I2P the failure rate of streaming connections is too high and transmitting an entire piece over a single connection is not likely to succeed as the size of the piece increases. This makes downloading multi-gigabyte files nearly impossible.
### Out-of-band request proposal
Barring any improvement to the reliability of I2P connections, the following approach can be used to enable smaller piece sizes and the corresponding increase in download success rate of large files:
* Search results do carry the full infohash of the file, instead they carry just the root and the number of 32-byte hashes in the infohash
* When a downloader begins a download, it issues a request for the full infohash first. Only after that is fetched and verified, the download proceeds as usual.
Such approach is more complicated in nature than the current simplistic one, but it has the additional benefit of reducing the size of search results.
### Wire protocol changes
A new request method - "HASHLIST" is introduced. It is immediately followed by the Base64 encoded root of the infohash and '\r\n'. The request may contain HTTP headers in the same format as in HTTP 1.1. One such header may be the X-Persona header, but that is optional. After all the headers a blank line with just '\r\n' is sent.
The response is identical to that of regular GET request, with the same response codes. The response may also carry headers, and is also followed by a blank line with '\r\n'. What follows immediately after that is a binary representation of the hashlist. After sending the full hashlist, the uploader keeps the connection open in anticipation of the first content GET request.
The downloader verifies the hashlist by hashing it with SHA256 and comparing it to the advertised infohash root. If there is a match, it proceeds with the rest of the download as in MuWire 0.1.0.
### Necessary changes to MuWire 0.1.0
To accommodate this proposal in a backwards compatible manner, it is necessary to first de-hardcode the piece count computation logic which is currently hardcoded in a few places. Then it is necessary to:
* persist the piece size to disk when a file is being shared so that it can be returned in search results
* search queries need to carry a flag of some kind that indicates support for out-of-band infohash support
* that in turn requires nodes to support passing of that flag as the queries are being routed through the network
* the returned results need to indicate whether they are returning a full infohash or just a root; the "version" field in the json can be used for that
### Roadmap
Support for this proposal is currently intended for MuWire 0.2.0. However, in order to make rollout smooth, in MuWire 0.1.1 support for the first two items will be introduced. Since there already are users on the network who have shared files without persisting the size of their pieces on disk, those files will not be eligible to participate in this scheme unless re-shared (which implies re-hashing).

View File

@ -52,7 +52,7 @@ class MainFrameController {
// this can be improved a lot
def replaced = search.toLowerCase().trim().replaceAll(Constants.SPLIT_PATTERN, " ")
def terms = replaced.split(" ")
searchEvent = new SearchEvent(searchTerms : terms, uuid : uuid)
searchEvent = new SearchEvent(searchTerms : terms, uuid : uuid, oobInfohash: false)
}
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true,
replyTo: core.me.destination, receivedOn: core.me.destination,

View File

@ -8,6 +8,7 @@ import com.muwire.core.MuWireSettings
import javax.annotation.Nonnull
import javax.inject.Inject
import javax.swing.JTable
import static griffon.util.GriffonApplicationUtils.isMacOSX
import static groovy.swing.SwingBuilder.lookAndFeel

View File

@ -1,6 +1,7 @@
import griffon.core.GriffonApplication
import griffon.core.env.Metadata
import groovy.util.logging.Log
import net.i2p.util.SystemVersion
import org.codehaus.griffon.runtime.core.AbstractLifecycleHandler
@ -34,13 +35,13 @@ class Ready extends AbstractLifecycleHandler {
log.info "starting core services"
def portableHome = System.getProperty("portable.home")
def home = portableHome == null ?
System.getProperty("user.home") + File.separator + ".MuWire" :
selectHome() :
portableHome
home = new File(home)
if (!home.exists()) {
log.info("creating home dir")
home.mkdir()
log.info("creating home dir $home")
home.mkdirs()
}
def props = new Properties()
@ -117,5 +118,25 @@ class Ready extends AbstractLifecycleHandler {
}
}
}
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()
}
}

View File

@ -105,7 +105,7 @@ class MainFrameView {
panel (constraints : JSplitPane.BOTTOM) {
borderLayout()
scrollPane (constraints : BorderLayout.CENTER) {
table(id : "downloads-table") {
table(id : "downloads-table", autoCreateRowSorter : true) {
tableModel(list: model.downloads) {
closureColumn(header: "Name", preferredWidth: 350, type: String, read : {row -> row.downloader.file.getName()})
closureColumn(header: "Status", preferredWidth: 50, type: String, read : {row -> row.downloader.getCurrentState()})
@ -136,7 +136,7 @@ class MainFrameView {
button(text : "Click here to share files", actionPerformed : shareFiles)
}
scrollPane ( constraints : BorderLayout.CENTER) {
table(id : "shared-files-table") {
table(id : "shared-files-table", autoCreateRowSorter: true) {
tableModel(list : model.shared) {
closureColumn(header : "Name", preferredWidth : 550, type : String, read : {row -> row.file.getAbsolutePath()})
closureColumn(header : "Size", preferredWidth : 50, type : String,
@ -213,7 +213,7 @@ class MainFrameView {
panel (border : etchedBorder()){
borderLayout()
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "trusted-table") {
table(id : "trusted-table", autoCreateRowSorter : true) {
tableModel(list : model.trusted) {
closureColumn(header : "Trusted Users", type : String, read : { it.getHumanReadableName() } )
}
@ -228,7 +228,7 @@ class MainFrameView {
panel (border : etchedBorder()){
borderLayout()
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "distrusted-table") {
table(id : "distrusted-table", autoCreateRowSorter : true) {
tableModel(list : model.distrusted) {
closureColumn(header: "Distrusted Users", type : String, read : { it.getHumanReadableName() } )
}

View File

@ -31,7 +31,7 @@ class SearchTabView {
builder.with {
def resultsTable
def pane = scrollPane {
resultsTable = table(id : "results-table") {
resultsTable = table(id : "results-table", autoCreateRowSorter : true) {
tableModel(list: model.results) {
closureColumn(header: "Name", preferredWidth: 350, type: String, read : {row -> row.name.replace('<','_')})
closureColumn(header: "Size", preferredWidth: 50, type: String, read : {row -> DataHelper.formatSize2Decimal(row.size, false)+"B"})