forked from I2P_Developers/i2p.i2p

The build in tracker has been removed for simplicity. Example usage: java -jar lib/i2psnark.jar myFile.torrent or, a more verbose setting: java -jar lib/i2psnark.jar --eepproxy 127.0.0.1 4444 \ --i2cp 127.0.0.1 7654 "inbound.length=2 outbound.length=2" \ --debug 6 myFile.torrent
599 lines
19 KiB
Java
599 lines
19 KiB
Java
/* Snark - Main snark program startup class.
|
|
Copyright (C) 2003 Mark J. Wielaard
|
|
|
|
This file is part of Snark.
|
|
|
|
This program is free software; you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation; either version 2, or (at your option)
|
|
any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program; if not, write to the Free Software Foundation,
|
|
Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
|
*/
|
|
|
|
package org.klomp.snark;
|
|
|
|
import java.io.*;
|
|
import java.net.*;
|
|
import java.util.*;
|
|
|
|
import org.klomp.snark.bencode.*;
|
|
|
|
import net.i2p.client.streaming.I2PSocket;
|
|
import net.i2p.client.streaming.I2PServerSocket;
|
|
|
|
/**
|
|
* Main Snark program startup class.
|
|
*
|
|
* @author Mark Wielaard (mark@klomp.org)
|
|
*/
|
|
public class Snark
|
|
implements StorageListener, CoordinatorListener, ShutdownListener
|
|
{
|
|
private final static int MIN_PORT = 6881;
|
|
private final static int MAX_PORT = 6889;
|
|
|
|
// Error messages (non-fatal)
|
|
public final static int ERROR = 1;
|
|
|
|
// Warning messages
|
|
public final static int WARNING = 2;
|
|
|
|
// Notices (peer level)
|
|
public final static int NOTICE = 3;
|
|
|
|
// Info messages (protocol policy level)
|
|
public final static int INFO = 4;
|
|
|
|
// Debug info (protocol level)
|
|
public final static int DEBUG = 5;
|
|
|
|
// Very low level stuff (network level)
|
|
public final static int ALL = 6;
|
|
|
|
/**
|
|
* What level of debug info to show.
|
|
*/
|
|
public static int debug = NOTICE;
|
|
|
|
// Whether or not to ask the user for commands while sharing
|
|
private static boolean command_interpreter = true;
|
|
|
|
private static final String newline = System.getProperty("line.separator");
|
|
|
|
private static final String copyright =
|
|
"The Hunting of the Snark Project - Copyright (C) 2003 Mark J. Wielaard"
|
|
+ newline + newline
|
|
+ "Snark comes with ABSOLUTELY NO WARRANTY. This is free software, and"
|
|
+ newline
|
|
+ "you are welcome to redistribute it under certain conditions; read the"
|
|
+ newline
|
|
+ "COPYING file for details." + newline + newline
|
|
+ "This is the I2P port, allowing anonymous bittorrent (http://www.i2p.net/)" + newline
|
|
+ "It will not work with normal torrents, so don't even try ;)";
|
|
|
|
private static final String usage =
|
|
"Press return for help. Type \"quit\" and return to stop.";
|
|
private static final String help =
|
|
"Commands: 'info', 'list', 'quit'.";
|
|
|
|
// String indicating main activity
|
|
static String activity = "Not started";
|
|
|
|
public static void main(String[] args)
|
|
{
|
|
System.out.println(copyright);
|
|
System.out.println();
|
|
|
|
// Parse debug, share/ip and torrent file options.
|
|
Snark snark = parseArguments(args);
|
|
|
|
SnarkShutdown snarkhook
|
|
= new SnarkShutdown(snark.storage,
|
|
snark.coordinator,
|
|
snark.acceptor,
|
|
snark.trackerclient,
|
|
snark);
|
|
Runtime.getRuntime().addShutdownHook(snarkhook);
|
|
|
|
Timer timer = new Timer(true);
|
|
TimerTask monitor = new PeerMonitorTask(snark.coordinator);
|
|
timer.schedule(monitor,
|
|
PeerMonitorTask.MONITOR_PERIOD,
|
|
PeerMonitorTask.MONITOR_PERIOD);
|
|
|
|
// Start command interpreter
|
|
if (Snark.command_interpreter)
|
|
{
|
|
boolean quit = false;
|
|
|
|
System.out.println();
|
|
System.out.println(usage);
|
|
System.out.println();
|
|
|
|
try
|
|
{
|
|
BufferedReader br = new BufferedReader
|
|
(new InputStreamReader(System.in));
|
|
String line = br.readLine();
|
|
while(!quit && line != null)
|
|
{
|
|
line = line.toLowerCase();
|
|
if ("quit".equals(line))
|
|
quit = true;
|
|
else if ("list".equals(line))
|
|
{
|
|
synchronized(coordinator.peers)
|
|
{
|
|
System.out.println(coordinator.peers.size()
|
|
+ " peers -"
|
|
+ " (i)nterested,"
|
|
+ " (I)nteresting,"
|
|
+ " (c)hoking,"
|
|
+ " (C)hoked:");
|
|
Iterator it = coordinator.peers.iterator();
|
|
while (it.hasNext())
|
|
{
|
|
Peer peer = (Peer)it.next();
|
|
System.out.println(peer);
|
|
System.out.println("\ti: " + peer.isInterested()
|
|
+ " I: " + peer.isInteresting()
|
|
+ " c: " + peer.isChoking()
|
|
+ " C: " + peer.isChoked());
|
|
}
|
|
}
|
|
}
|
|
else if ("info".equals(line))
|
|
{
|
|
System.out.println("Name: " + meta.getName());
|
|
System.out.println("Torrent: " + torrent);
|
|
System.out.println("Tracker: " + meta.getAnnounce());
|
|
List files = meta.getFiles();
|
|
System.out.println("Files: "
|
|
+ ((files == null) ? 1 : files.size()));
|
|
System.out.println("Pieces: " + meta.getPieces());
|
|
System.out.println("Piece size: "
|
|
+ meta.getPieceLength(0) / 1024
|
|
+ " KB");
|
|
System.out.println("Total size: "
|
|
+ meta.getTotalLength() / (1024 * 1024)
|
|
+ " MB");
|
|
}
|
|
else if ("".equals(line) || "help".equals(line))
|
|
{
|
|
System.out.println(usage);
|
|
System.out.println(help);
|
|
}
|
|
else
|
|
{
|
|
System.out.println("Unknown command: " + line);
|
|
System.out.println(usage);
|
|
}
|
|
|
|
if (!quit)
|
|
{
|
|
System.out.println();
|
|
line = br.readLine();
|
|
}
|
|
}
|
|
}
|
|
catch(IOException ioe)
|
|
{
|
|
debug("ERROR while reading stdin: " + ioe, ERROR);
|
|
}
|
|
|
|
// Explicit shutdown.
|
|
Runtime.getRuntime().removeShutdownHook(snarkhook);
|
|
snarkhook.start();
|
|
}
|
|
}
|
|
|
|
static String torrent;
|
|
static MetaInfo meta;
|
|
static Storage storage;
|
|
static PeerCoordinator coordinator;
|
|
static ConnectionAcceptor acceptor;
|
|
static TrackerClient trackerclient;
|
|
|
|
private Snark(String torrent, String ip, int user_port,
|
|
StorageListener slistener, CoordinatorListener clistener)
|
|
{
|
|
if (slistener == null)
|
|
slistener = this;
|
|
|
|
if (clistener == null)
|
|
clistener = this;
|
|
|
|
this.torrent = torrent;
|
|
|
|
activity = "Network setup";
|
|
|
|
// "Taking Three as the subject to reason about--
|
|
// A convenient number to state--
|
|
// We add Seven, and Ten, and then multiply out
|
|
// By One Thousand diminished by Eight.
|
|
//
|
|
// "The result we proceed to divide, as you see,
|
|
// By Nine Hundred and Ninety Two:
|
|
// Then subtract Seventeen, and the answer must be
|
|
// Exactly and perfectly true.
|
|
|
|
// Create a new ID and fill it with something random. First nine
|
|
// zeros bytes, then three bytes filled with snark and then
|
|
// sixteen random bytes.
|
|
byte snark = (((3 + 7 + 10) * (1000 - 8)) / 992) - 17;
|
|
byte[] id = new byte[20];
|
|
Random random = new Random();
|
|
int i;
|
|
for (i = 0; i < 9; i++)
|
|
id[i] = 0;
|
|
id[i++] = snark;
|
|
id[i++] = snark;
|
|
id[i++] = snark;
|
|
while (i < 20)
|
|
id[i++] = (byte)random.nextInt(256);
|
|
|
|
Snark.debug("My peer id: " + PeerID.idencode(id), Snark.INFO);
|
|
|
|
int port;
|
|
IOException lastException = null;
|
|
boolean ok = I2PSnarkUtil.instance().connect();
|
|
if (!ok) fatal("Unable to connect to I2P");
|
|
I2PServerSocket serversocket = I2PSnarkUtil.instance().getServerSocket();
|
|
if (serversocket == null)
|
|
fatal("Unable to listen for I2P connections");
|
|
else
|
|
debug("Listening on I2P destination " + serversocket.getManager().getSession().getMyDestination().toBase64(), NOTICE);
|
|
|
|
// Figure out what the torrent argument represents.
|
|
meta = null;
|
|
File f = null;
|
|
try
|
|
{
|
|
InputStream in = null;
|
|
f = new File(torrent);
|
|
if (f.exists())
|
|
in = new FileInputStream(f);
|
|
else
|
|
{
|
|
activity = "Getting torrent";
|
|
File torrentFile = I2PSnarkUtil.instance().get(torrent);
|
|
if (torrentFile == null) {
|
|
fatal("Unable to fetch " + torrent);
|
|
if (false) return; // never reached - fatal(..) throws
|
|
} else {
|
|
torrentFile.deleteOnExit();
|
|
in = new FileInputStream(torrentFile);
|
|
}
|
|
}
|
|
meta = new MetaInfo(new BDecoder(in));
|
|
}
|
|
catch(IOException ioe)
|
|
{
|
|
// OK, so it wasn't a torrent metainfo file.
|
|
if (f != null && f.exists())
|
|
if (ip == null)
|
|
fatal("'" + torrent + "' exists,"
|
|
+ " but is not a valid torrent metainfo file."
|
|
+ System.getProperty("line.separator"), ioe);
|
|
else
|
|
fatal("I2PSnark does not support creating and tracking a torrent at the moment");
|
|
/*
|
|
{
|
|
// Try to create a new metainfo file
|
|
Snark.debug
|
|
("Trying to create metainfo torrent for '" + torrent + "'",
|
|
NOTICE);
|
|
try
|
|
{
|
|
activity = "Creating torrent";
|
|
storage = new Storage
|
|
(f, "http://" + ip + ":" + port + "/announce", slistener);
|
|
storage.create();
|
|
meta = storage.getMetaInfo();
|
|
}
|
|
catch (IOException ioe2)
|
|
{
|
|
fatal("Could not create torrent for '" + torrent + "'", ioe2);
|
|
}
|
|
}
|
|
*/
|
|
else
|
|
fatal("Cannot open '" + torrent + "'", ioe);
|
|
}
|
|
|
|
debug(meta.toString(), INFO);
|
|
|
|
// When the metainfo torrent was created from an existing file/dir
|
|
// it already exists.
|
|
if (storage == null)
|
|
{
|
|
try
|
|
{
|
|
activity = "Checking storage";
|
|
storage = new Storage(meta, slistener);
|
|
storage.check();
|
|
}
|
|
catch (IOException ioe)
|
|
{
|
|
fatal("Could not create storage", ioe);
|
|
}
|
|
}
|
|
|
|
activity = "Collecting pieces";
|
|
coordinator = new PeerCoordinator(id, meta, storage, clistener);
|
|
PeerAcceptor peeracceptor = new PeerAcceptor(coordinator);
|
|
ConnectionAcceptor acceptor = new ConnectionAcceptor(serversocket,
|
|
peeracceptor);
|
|
|
|
trackerclient = new TrackerClient(meta, coordinator);
|
|
trackerclient.start();
|
|
|
|
}
|
|
|
|
static Snark parseArguments(String[] args)
|
|
{
|
|
return parseArguments(args, null, null);
|
|
}
|
|
|
|
/**
|
|
* Sets debug, ip and torrent variables then creates a Snark
|
|
* instance. Calls usage(), which terminates the program, if
|
|
* non-valid argument list. The given listeners will be
|
|
* passed to all components that take one.
|
|
*/
|
|
static Snark parseArguments(String[] args,
|
|
StorageListener slistener,
|
|
CoordinatorListener clistener)
|
|
{
|
|
int user_port = -1;
|
|
String ip = null;
|
|
String torrent = null;
|
|
|
|
int i = 0;
|
|
while (i < args.length)
|
|
{
|
|
if (args[i].equals("--debug"))
|
|
{
|
|
debug = INFO;
|
|
i++;
|
|
|
|
// Try if there is an level argument.
|
|
if (i < args.length)
|
|
{
|
|
try
|
|
{
|
|
int level = Integer.parseInt(args[i]);
|
|
if (level >= 0)
|
|
{
|
|
debug = level;
|
|
i++;
|
|
}
|
|
}
|
|
catch (NumberFormatException nfe) { }
|
|
}
|
|
}
|
|
else if (args[i].equals("--port"))
|
|
{
|
|
if (args.length - 1 < i + 1)
|
|
usage("--port needs port number to listen on");
|
|
try
|
|
{
|
|
user_port = Integer.parseInt(args[i + 1]);
|
|
}
|
|
catch (NumberFormatException nfe)
|
|
{
|
|
usage("--port argument must be a number (" + nfe + ")");
|
|
}
|
|
i += 2;
|
|
}
|
|
else if (args[i].equals("--no-commands"))
|
|
{
|
|
command_interpreter = false;
|
|
i++;
|
|
}
|
|
else if (args[i].equals("--eepproxy"))
|
|
{
|
|
String proxyHost = args[i+1];
|
|
String proxyPort = args[i+2];
|
|
I2PSnarkUtil.instance().setProxy(proxyHost, Integer.parseInt(proxyPort));
|
|
i += 3;
|
|
}
|
|
else if (args[i].equals("--i2cp"))
|
|
{
|
|
String i2cpHost = args[i+1];
|
|
String i2cpPort = args[i+2];
|
|
Properties opts = null;
|
|
if (i+3 < args.length) {
|
|
if (!args[i+3].startsWith("--")) {
|
|
opts = new Properties();
|
|
StringTokenizer tok = new StringTokenizer(args[i+3], " \t");
|
|
while (tok.hasMoreTokens()) {
|
|
String str = tok.nextToken();
|
|
int split = str.indexOf('=');
|
|
if (split > 0) {
|
|
opts.setProperty(str.substring(0, split), str.substring(split+1));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
I2PSnarkUtil.instance().setI2CPConfig(i2cpHost, Integer.parseInt(i2cpPort), opts);
|
|
i += 3 + (opts != null ? 1 : 0);
|
|
}
|
|
else
|
|
{
|
|
torrent = args[i];
|
|
i++;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (torrent == null || i != args.length)
|
|
if (torrent != null && torrent.startsWith("-"))
|
|
usage("Unknow option '" + torrent + "'.");
|
|
else
|
|
usage("Need exactly one <url>, <file> or <dir>.");
|
|
|
|
return new Snark(torrent, ip, user_port, slistener, clistener);
|
|
}
|
|
|
|
private static void usage(String s)
|
|
{
|
|
System.out.println("snark: " + s);
|
|
usage();
|
|
}
|
|
|
|
private static void usage()
|
|
{
|
|
System.out.println
|
|
("Usage: snark [--debug [level]] [--no-commands] [--port <port>]");
|
|
System.out.println
|
|
(" [--eepproxy hostname portnum]");
|
|
System.out.println
|
|
(" [--i2cp routerHost routerPort ['name=val name=val name=val']]");
|
|
System.out.println
|
|
(" (<url>|<file>)");
|
|
System.out.println
|
|
(" --debug\tShows some extra info and stacktraces");
|
|
System.out.println
|
|
(" level\tHow much debug details to show");
|
|
System.out.println
|
|
(" \t(defaults to "
|
|
+ NOTICE + ", with --debug to "
|
|
+ INFO + ", highest level is "
|
|
+ ALL + ").");
|
|
System.out.println
|
|
(" --no-commands\tDon't read interactive commands or show usage info.");
|
|
System.out.println
|
|
(" --port\tThe port to listen on for incomming connections");
|
|
System.out.println
|
|
(" \t(if not given defaults to first free port between "
|
|
+ MIN_PORT + "-" + MAX_PORT + ").");
|
|
System.out.println
|
|
(" --share\tStart torrent tracker on <ip> address or <host> name.");
|
|
System.out.println
|
|
(" --eepproxy\thttp proxy to use (default of 127.0.0.1 port 4444)");
|
|
System.out.println
|
|
(" --i2cp\tlocation of your I2P router (default of 127.0.0.1 port 7654)");
|
|
System.out.println
|
|
(" \toptional settings may be included, such as");
|
|
System.out.println
|
|
(" \tinbound.length=2 outbound.length=2 inbound.lengthVariance=-1 ");
|
|
System.out.println
|
|
(" <url> \tURL pointing to .torrent metainfo file to download/share.");
|
|
System.out.println
|
|
(" <file> \tEither a local .torrent metainfo file to download");
|
|
System.out.println
|
|
(" \tor (with --share) a file to share.");
|
|
System.exit(-1);
|
|
}
|
|
|
|
/**
|
|
* Aborts program abnormally.
|
|
*/
|
|
public static void fatal(String s)
|
|
{
|
|
fatal(s, null);
|
|
}
|
|
|
|
/**
|
|
* Aborts program abnormally.
|
|
*/
|
|
public static void fatal(String s, Throwable t)
|
|
{
|
|
I2PSnarkUtil.instance().debug(s, ERROR, t);
|
|
//System.err.println("snark: " + s + ((t == null) ? "" : (": " + t)));
|
|
//if (debug >= INFO && t != null)
|
|
// t.printStackTrace();
|
|
throw new RuntimeException("die bart die");
|
|
}
|
|
|
|
/**
|
|
* Show debug info if debug is true.
|
|
*/
|
|
public static void debug(String s, int level)
|
|
{
|
|
I2PSnarkUtil.instance().debug(s, level, null);
|
|
//if (debug >= level)
|
|
// System.out.println(s);
|
|
}
|
|
|
|
public void peerChange(PeerCoordinator coordinator, Peer peer)
|
|
{
|
|
// System.out.println(peer.toString());
|
|
}
|
|
|
|
boolean allocating = false;
|
|
public void storageCreateFile(Storage storage, String name, long length)
|
|
{
|
|
if (allocating)
|
|
System.out.println(); // Done with last file.
|
|
|
|
System.out.print("Creating file '" + name
|
|
+ "' of length " + length + ": ");
|
|
allocating = true;
|
|
}
|
|
|
|
// How much storage space has been allocated
|
|
private long allocated = 0;
|
|
|
|
public void storageAllocated(Storage storage, long length)
|
|
{
|
|
allocating = true;
|
|
System.out.print(".");
|
|
allocated += length;
|
|
if (allocated == meta.getTotalLength())
|
|
System.out.println(); // We have all the disk space we need.
|
|
}
|
|
|
|
boolean allChecked = false;
|
|
boolean checking = false;
|
|
boolean prechecking = true;
|
|
public void storageChecked(Storage storage, int num, boolean checked)
|
|
{
|
|
allocating = false;
|
|
if (!allChecked && !checking)
|
|
{
|
|
// Use the MetaInfo from the storage since our own might not
|
|
// yet be setup correctly.
|
|
MetaInfo meta = storage.getMetaInfo();
|
|
if (meta != null)
|
|
System.out.print("Checking existing "
|
|
+ meta.getPieces()
|
|
+ " pieces: ");
|
|
checking = true;
|
|
}
|
|
if (checking)
|
|
if (checked)
|
|
System.out.print("+");
|
|
else
|
|
System.out.print("-");
|
|
else
|
|
Snark.debug("Got " + (checked ? "" : "BAD ") + "piece: " + num,
|
|
Snark.INFO);
|
|
}
|
|
|
|
public void storageAllChecked(Storage storage)
|
|
{
|
|
if (checking)
|
|
System.out.println();
|
|
|
|
allChecked = true;
|
|
checking = false;
|
|
}
|
|
|
|
public void shutdown()
|
|
{
|
|
// Should not be necessary since all non-deamon threads should
|
|
// have died. But in reality this does not always happen.
|
|
System.exit(0);
|
|
}
|
|
}
|