From 3ec92c8b621effe93c448d187a76c678d9d25121 Mon Sep 17 00:00:00 2001 From: jrandom <jrandom> Date: Fri, 16 Dec 2005 03:00:48 +0000 Subject: [PATCH] 2005-12-15 jrandom * Added a first pass to the I2PSnark web UI (see /i2psnark/) --- apps/i2psnark/java/build.xml | 13 +- .../org/klomp/snark/ConnectionAcceptor.java | 102 ++-- .../src/org/klomp/snark/I2PSnarkUtil.java | 56 +- .../java/src/org/klomp/snark/Peer.java | 2 +- .../src/org/klomp/snark/PeerCheckerTask.java | 3 + .../src/org/klomp/snark/PeerConnectionIn.java | 27 + .../org/klomp/snark/PeerConnectionOut.java | 13 +- .../src/org/klomp/snark/PeerCoordinator.java | 25 +- .../java/src/org/klomp/snark/Snark.java | 39 +- .../src/org/klomp/snark/SnarkManager.java | 149 ++++- .../src/org/klomp/snark/TrackerClient.java | 47 +- .../org/klomp/snark/web/I2PSnarkServlet.java | 524 ++++++++++++++++++ apps/i2psnark/web.xml | 25 + apps/routerconsole/jsp/nav.jsp | 1 + build.xml | 6 +- history.txt | 5 +- 16 files changed, 930 insertions(+), 107 deletions(-) create mode 100644 apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java create mode 100644 apps/i2psnark/web.xml diff --git a/apps/i2psnark/java/build.xml b/apps/i2psnark/java/build.xml index 4a447a6f75..3d23a8ba68 100644 --- a/apps/i2psnark/java/build.xml +++ b/apps/i2psnark/java/build.xml @@ -1,8 +1,9 @@ <?xml version="1.0" encoding="UTF-8"?> <project basedir="." default="all" name="i2psnark"> <target name="all" depends="clean, build" /> - <target name="build" depends="builddep, jar" /> + <target name="build" depends="builddep, jar, war" /> <target name="builddep"> + <ant dir="../../jetty/" target="build" /> <ant dir="../../ministreaming/java/" target="build" /> <!-- ministreaming will build core --> </target> @@ -13,18 +14,24 @@ srcdir="./src" debug="true" deprecation="on" source="1.3" target="1.3" destdir="./build/obj" - classpath="../../../core/java/build/i2p.jar:../../ministreaming/java/build/mstreaming.jar" /> + classpath="../../../core/java/build/i2p.jar:../../jetty/jettylib/org.mortbay.jetty.jar:../../jetty/jettylib/javax.servlet.jar:../../ministreaming/java/build/mstreaming.jar" /> </target> <target name="jar" depends="builddep, compile"> - <jar destfile="./build/i2psnark.jar" basedir="./build/obj" includes="**/*.class"> + <jar destfile="./build/i2psnark.jar" basedir="./build/obj" includes="**/*.class" excludes="**/*Servlet.class"> <manifest> <attribute name="Main-Class" value="org.klomp.snark.Snark" /> <attribute name="Class-Path" value="i2p.jar mstreaming.jar streaming.jar" /> </manifest> </jar> + </target> + <target name="war" depends="jar"> + <war destfile="../i2psnark.war" webxml="../web.xml"> + <classes dir="./build/obj" includes="**/*" /> + </war> </target> <target name="clean"> <delete dir="./build" /> + <delete file="../i2psnark.war" /> </target> <target name="cleandep" depends="clean"> <ant dir="../../ministreaming/java/" target="distclean" /> diff --git a/apps/i2psnark/java/src/org/klomp/snark/ConnectionAcceptor.java b/apps/i2psnark/java/src/org/klomp/snark/ConnectionAcceptor.java index c5b21d5c34..4a41d63e14 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/ConnectionAcceptor.java +++ b/apps/i2psnark/java/src/org/klomp/snark/ConnectionAcceptor.java @@ -32,11 +32,12 @@ import net.i2p.client.streaming.I2PSocket; */ public class ConnectionAcceptor implements Runnable { - private final I2PServerSocket serverSocket; + private I2PServerSocket serverSocket; private final PeerAcceptor peeracceptor; private Thread thread; private boolean stop; + private boolean socketChanged; public ConnectionAcceptor(I2PServerSocket serverSocket, PeerAcceptor peeracceptor) @@ -44,8 +45,10 @@ public class ConnectionAcceptor implements Runnable this.serverSocket = serverSocket; this.peeracceptor = peeracceptor; + socketChanged = false; stop = false; - thread = new Thread(this); + thread = new Thread(this, "I2PSnark acceptor"); + thread.setDaemon(true); thread.start(); } @@ -65,6 +68,14 @@ public class ConnectionAcceptor implements Runnable if (t != null) t.interrupt(); } + + public void restart() { + serverSocket = I2PSnarkUtil.instance().getServerSocket(); + socketChanged = true; + Thread t = thread; + if (t != null) + t.interrupt(); + } public int getPort() { @@ -75,57 +86,35 @@ public class ConnectionAcceptor implements Runnable { while(!stop) { + if (socketChanged) { + // ok, already updated + socketChanged = false; + } + if (serverSocket == null) { + Snark.debug("Server socket went away.. boo hiss", Snark.ERROR); + stop = true; + return; + } try { - final I2PSocket socket = serverSocket.accept(); - Thread t = new Thread("Connection-" + socket) - { - public void run() - { - try - { - InputStream in = socket.getInputStream(); - OutputStream out = socket.getOutputStream(); - BufferedInputStream bis = new BufferedInputStream(in); - BufferedOutputStream bos = new BufferedOutputStream(out); - - // See what kind of connection it is. - /* - if (httpacceptor != null) - { - byte[] scratch = new byte[4]; - bis.mark(4); - int len = bis.read(scratch); - if (len != 4) - throw new IOException("Need at least 4 bytes"); - bis.reset(); - if (scratch[0] == 19 && scratch[1] == 'B' - && scratch[2] == 'i' && scratch[3] == 't') - peeracceptor.connection(socket, bis, bos); - else if (scratch[0] == 'G' && scratch[1] == 'E' - && scratch[2] == 'T' && scratch[3] == ' ') - httpacceptor.connection(socket, bis, bos); - } - else - */ - peeracceptor.connection(socket, bis, bos); - } - catch (IOException ioe) - { - try - { - socket.close(); - } - catch (IOException ignored) { } - } + I2PSocket socket = serverSocket.accept(); + if (socket == null) { + if (socketChanged) { + continue; + } else { + Snark.debug("Null socket accepted, but socket wasn't changed?", Snark.ERROR); } - }; - t.start(); + } else { + Thread t = new Thread(new Handler(socket), "Connection-" + socket); + t.start(); + } } catch (I2PException ioe) { - Snark.debug("Error while accepting: " + ioe, Snark.ERROR); - stop = true; + if (!socketChanged) { + Snark.debug("Error while accepting: " + ioe, Snark.ERROR); + stop = true; + } } catch (IOException ioe) { @@ -140,4 +129,23 @@ public class ConnectionAcceptor implements Runnable } catch (I2PException ignored) { } } + + private class Handler implements Runnable { + private I2PSocket _socket; + public Handler(I2PSocket socket) { + _socket = socket; + } + public void run() { + try { + InputStream in = _socket.getInputStream(); + OutputStream out = _socket.getOutputStream(); + BufferedInputStream bis = new BufferedInputStream(in); + BufferedOutputStream bos = new BufferedOutputStream(out); + + peeracceptor.connection(_socket, bis, bos); + } catch (IOException ioe) { + try { _socket.close(); } catch (IOException ignored) { } + } + } + } } diff --git a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java index 25da3c2983..339eec8dde 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java +++ b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java @@ -13,7 +13,7 @@ import net.i2p.client.streaming.I2PSocketManagerFactory; import net.i2p.util.Log; import java.io.*; -import java.util.Properties; +import java.util.*; /** * I2P specific helpers for I2PSnark @@ -29,14 +29,17 @@ public class I2PSnarkUtil { private int _proxyPort; private String _i2cpHost; private int _i2cpPort; - private Properties _opts; + private Map _opts; private I2PSocketManager _manager; + private boolean _configured; private I2PSnarkUtil() { _context = I2PAppContext.getGlobalContext(); _log = _context.logManager().getLog(Snark.class); + _opts = new HashMap(); setProxy("127.0.0.1", 4444); setI2CPConfig("127.0.0.1", 7654, null); + _configured = false; } /** @@ -54,25 +57,55 @@ public class I2PSnarkUtil { _proxyHost = null; _proxyPort = -1; } + _configured = true; } - public void setI2CPConfig(String i2cpHost, int i2cpPort, Properties opts) { + public boolean configured() { return _configured; } + + public void setI2CPConfig(String i2cpHost, int i2cpPort, Map opts) { _i2cpHost = i2cpHost; _i2cpPort = i2cpPort; if (opts != null) - _opts = opts; + _opts.putAll(opts); + _configured = true; } + public String getI2CPHost() { return _i2cpHost; } + public int getI2CPPort() { return _i2cpPort; } + public Map getI2CPOptions() { return _opts; } + public String getEepProxyHost() { return _proxyHost; } + public int getEepProxyPort() { return _proxyPort; } + public boolean getEepProxySet() { return _shouldProxy; } + /** * Connect to the router, if we aren't already */ - boolean connect() { + public boolean connect() { if (_manager == null) { - _manager = I2PSocketManagerFactory.createManager(_i2cpHost, _i2cpPort, _opts); + Properties opts = new Properties(); + if (_opts != null) { + for (Iterator iter = _opts.keySet().iterator(); iter.hasNext(); ) { + String key = (String)iter.next(); + opts.setProperty(key, _opts.get(key).toString()); + } + } + if (opts.getProperty("inbound.nickname") == null) + opts.setProperty("inbound.nickname", "I2PSnark"); + _manager = I2PSocketManagerFactory.createManager(_i2cpHost, _i2cpPort, opts); } return (_manager != null); } + public boolean connected() { return _manager != null; } + /** + * Destroy the destination itself + */ + public void disconnect() { + I2PSocketManager mgr = _manager; + _manager = null; + mgr.destroySocketManager(); + } + /** connect to the given destination */ I2PSocket connect(PeerID peer) throws IOException { try { @@ -85,7 +118,8 @@ public class I2PSnarkUtil { /** * fetch the given URL, returning the file it is stored in, or null on error */ - File get(String url) { + public File get(String url) { return get(url, true); } + public File get(String url, boolean rewrite) { _log.debug("Fetching [" + url + "] proxy=" + _proxyHost + ":" + _proxyPort + ": " + _shouldProxy); File out = null; try { @@ -95,8 +129,10 @@ public class I2PSnarkUtil { out.delete(); return null; } - String fetchURL = rewriteAnnounce(url); - _log.debug("Rewritten url [" + fetchURL + "]"); + String fetchURL = url; + if (rewrite) + fetchURL = rewriteAnnounce(url); + //_log.debug("Rewritten url [" + fetchURL + "]"); EepGet get = new EepGet(_context, _shouldProxy, _proxyHost, _proxyPort, 1, out.getAbsolutePath(), fetchURL); if (get.fetch()) { _log.debug("Fetch successful [" + url + "]: size=" + out.length()); @@ -108,7 +144,7 @@ public class I2PSnarkUtil { } } - I2PServerSocket getServerSocket() { + public I2PServerSocket getServerSocket() { return _manager.getServerSocket(); } diff --git a/apps/i2psnark/java/src/org/klomp/snark/Peer.java b/apps/i2psnark/java/src/org/klomp/snark/Peer.java index aafe529f43..ef3fe2b27d 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/Peer.java +++ b/apps/i2psnark/java/src/org/klomp/snark/Peer.java @@ -37,7 +37,7 @@ public class Peer implements Comparable private final PeerID peerID; private final byte[] my_id; - private final MetaInfo metainfo; + final MetaInfo metainfo; // The data in/output streams set during the handshake and used by // the actual connections. diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerCheckerTask.java b/apps/i2psnark/java/src/org/klomp/snark/PeerCheckerTask.java index 90d772f68c..532d48aab1 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/PeerCheckerTask.java +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerCheckerTask.java @@ -70,6 +70,7 @@ class PeerCheckerTask extends TimerTask { it.remove(); coordinator.removePeerFromPieces(peer); + coordinator.peerCount = coordinator.peers.size(); continue; } @@ -185,6 +186,7 @@ class PeerCheckerTask extends TimerTask // Put it at the back of the list coordinator.peers.remove(worstDownloader); + coordinator.peerCount = coordinator.peers.size(); removed.add(worstDownloader); } @@ -193,6 +195,7 @@ class PeerCheckerTask extends TimerTask // Put peers back at the end of the list that we removed earlier. coordinator.peers.addAll(removed); + coordinator.peerCount = coordinator.peers.size(); } } } diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionIn.java b/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionIn.java index c3d5f95bef..35b428a9c3 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionIn.java +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionIn.java @@ -24,8 +24,11 @@ import java.io.*; import java.net.*; import java.util.*; +import net.i2p.util.Log; + class PeerConnectionIn implements Runnable { + private Log _log = new Log(PeerConnectionIn.class); private final Peer peer; private final DataInputStream din; @@ -72,6 +75,8 @@ class PeerConnectionIn implements Runnable if (i == 0) { ps.keepAliveMessage(); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Received keepalive from " + peer + " on " + peer.metainfo.getName()); continue; } @@ -82,30 +87,44 @@ class PeerConnectionIn implements Runnable { case 0: ps.chokeMessage(true); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Received choke from " + peer + " on " + peer.metainfo.getName()); break; case 1: ps.chokeMessage(false); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Received unchoke from " + peer + " on " + peer.metainfo.getName()); break; case 2: ps.interestedMessage(true); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Received interested from " + peer + " on " + peer.metainfo.getName()); break; case 3: ps.interestedMessage(false); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Received not interested from " + peer + " on " + peer.metainfo.getName()); break; case 4: piece = din.readInt(); ps.haveMessage(piece); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Received havePiece(" + piece + ") from " + peer + " on " + peer.metainfo.getName()); break; case 5: byte[] bitmap = new byte[i-1]; din.readFully(bitmap); ps.bitfieldMessage(bitmap); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Received bitmap from " + peer + " on " + peer.metainfo.getName()); break; case 6: piece = din.readInt(); begin = din.readInt(); len = din.readInt(); ps.requestMessage(piece, begin, len); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Received request(" + piece + "," + begin + ") from " + peer + " on " + peer.metainfo.getName()); break; case 7: piece = din.readInt(); @@ -118,12 +137,16 @@ class PeerConnectionIn implements Runnable piece_bytes = req.bs; din.readFully(piece_bytes, begin, len); ps.pieceMessage(req); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Received data(" + piece + "," + begin + ") from " + peer + " on " + peer.metainfo.getName()); } else { // XXX - Consume but throw away afterwards. piece_bytes = new byte[len]; din.readFully(piece_bytes); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Received UNWANTED data(" + piece + "," + begin + ") from " + peer + " on " + peer.metainfo.getName()); } break; case 8: @@ -131,11 +154,15 @@ class PeerConnectionIn implements Runnable begin = din.readInt(); len = din.readInt(); ps.cancelMessage(piece, begin, len); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Received cancel(" + piece + "," + begin + ") from " + peer + " on " + peer.metainfo.getName()); break; default: byte[] bs = new byte[i-1]; din.readFully(bs); ps.unknownMessage(b, bs); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Received unknown message from " + peer + " on " + peer.metainfo.getName()); } } } diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionOut.java b/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionOut.java index 8d09859a74..817d98927a 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionOut.java +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionOut.java @@ -24,8 +24,11 @@ import java.io.*; import java.net.*; import java.util.*; +import net.i2p.util.Log; + class PeerConnectionOut implements Runnable { + private Log _log = new Log(PeerConnectionOut.class); private final Peer peer; private final DataOutputStream dout; @@ -34,14 +37,18 @@ class PeerConnectionOut implements Runnable // Contains Messages. private List sendQueue = new ArrayList(); + + private static long __id = 0; + private long _id; public PeerConnectionOut(Peer peer, DataOutputStream dout) { this.peer = peer; this.dout = dout; + _id = ++__id; quit = false; - thread = new Thread(this); + thread = new Thread(this, "Snark sender " + _id); thread.start(); } @@ -64,6 +71,8 @@ class PeerConnectionOut implements Runnable try { // Make sure everything will reach the other side. + // i2p flushes passively, no need to force it + // ... maybe not though dout.flush(); // Wait till more data arrives. @@ -114,6 +123,8 @@ class PeerConnectionOut implements Runnable { if (Snark.debug >= Snark.ALL) Snark.debug("Send " + peer + ": " + m, Snark.ALL); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Send " + peer + ": " + m + " on " + peer.metainfo.getName()); m.sendMessage(dout); // Remove all piece messages after sending a choke message. diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java b/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java index 7d8e77555c..f98e45d499 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java @@ -51,6 +51,8 @@ public class PeerCoordinator implements PeerListener // synchronize on this when changing peers or downloaders final List peers = new ArrayList(); + /** estimate of the peers, without requiring any synchronization */ + volatile int peerCount; /** Timer to handle all periodical tasks. */ private final Timer timer = new Timer(true); @@ -63,6 +65,9 @@ public class PeerCoordinator implements PeerListener private boolean halted = false; private final CoordinatorListener listener; + + public String trackerProblems = null; + public int trackerSeenPeers = 0; public PeerCoordinator(byte[] id, MetaInfo metainfo, Storage storage, CoordinatorListener listener) @@ -97,12 +102,15 @@ public class PeerCoordinator implements PeerListener return storage.complete(); } + public int getPeerCount() { return peerCount; } public int getPeers() { synchronized(peers) { - return peers.size(); + int rv = peers.size(); + peerCount = rv; + return rv; } } @@ -163,6 +171,7 @@ public class PeerCoordinator implements PeerListener it.remove(); removePeerFromPieces(peer); } + peerCount = peers.size(); } } @@ -187,9 +196,11 @@ public class PeerCoordinator implements PeerListener if (Snark.debug >= Snark.INFO) Snark.debug("New connection to peer: " + peer, Snark.INFO); + _log.info("New connection to peer " + peer + " for " + metainfo.getName()); // Add it to the beginning of the list. // And try to optimistically make it a uploader. peers.add(0, peer); + peerCount = peers.size(); unchokePeer(); if (listener != null) @@ -223,7 +234,7 @@ public class PeerCoordinator implements PeerListener if (need_more) { - _log.debug("Addng a peer " + peer.getPeerID().getAddress().calculateHash().toBase64(), new Exception("add/run")); + _log.debug("Adding a peer " + peer.getPeerID().getAddress().calculateHash().toBase64() + " for " + metainfo.getName(), new Exception("add/run")); // Run the peer with us as listener and the current bitfield. final PeerListener listener = this; @@ -281,6 +292,7 @@ public class PeerCoordinator implements PeerListener // Put peer back at the end of the list. peers.remove(peer); peers.add(peer); + peerCount = peers.size(); } } @@ -422,8 +434,10 @@ public class PeerCoordinator implements PeerListener */ public boolean gotPiece(Peer peer, int piece, byte[] bs) { - if (halted) + if (halted) { + _log.info("Got while-halted piece " + piece + "/" + metainfo.getPieces() +" from " + peer + " for " + metainfo.getName()); return true; // We don't actually care anymore. + } synchronized(wantedPieces) { @@ -433,6 +447,8 @@ public class PeerCoordinator implements PeerListener if (Snark.debug >= Snark.INFO) Snark.debug(peer + " piece " + piece + " no longer needed", Snark.INFO); + + _log.info("Got unwanted piece " + piece + "/" + metainfo.getPieces() +" from " + peer + " for " + metainfo.getName()); // No need to announce have piece to peers. // Assume we got a good piece, we don't really care anymore. @@ -445,6 +461,7 @@ public class PeerCoordinator implements PeerListener { if (Snark.debug >= Snark.INFO) Snark.debug("Recv p" + piece + " " + peer, Snark.INFO); + _log.info("Got valid piece " + piece + "/" + metainfo.getPieces() +" from " + peer + " for " + metainfo.getName()); } else { @@ -453,6 +470,7 @@ public class PeerCoordinator implements PeerListener if (Snark.debug >= Snark.NOTICE) Snark.debug("Got BAD piece " + piece + " from " + peer, Snark.NOTICE); + _log.warn("Got BAD piece " + piece + "/" + metainfo.getPieces() + " from " + peer + " for " + metainfo.getName()); return false; // No need to announce BAD piece to peers. } } @@ -524,6 +542,7 @@ public class PeerCoordinator implements PeerListener unchokePeer(); removePeerFromPieces(peer); } + peerCount = peers.size(); } if (listener != null) diff --git a/apps/i2psnark/java/src/org/klomp/snark/Snark.java b/apps/i2psnark/java/src/org/klomp/snark/Snark.java index 3f0768c9aa..a5917fd820 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/Snark.java +++ b/apps/i2psnark/java/src/org/klomp/snark/Snark.java @@ -209,13 +209,15 @@ public class Snark } } - String torrent; - MetaInfo meta; - Storage storage; - PeerCoordinator coordinator; - ConnectionAcceptor acceptor; - TrackerClient trackerclient; - String rootDataDir = "."; + public String torrent; + public MetaInfo meta; + public Storage storage; + public PeerCoordinator coordinator; + public ConnectionAcceptor acceptor; + public TrackerClient trackerclient; + public String rootDataDir = "."; + public CompleteListener completeListener; + public boolean stopped; Snark(String torrent, String ip, int user_port, StorageListener slistener, CoordinatorListener clistener) { @@ -233,6 +235,7 @@ public class Snark this.torrent = torrent; this.rootDataDir = rootDir; + stopped = true; activity = "Network setup"; // "Taking Three as the subject to reason about-- @@ -363,6 +366,7 @@ public class Snark * Start up contacting peers and querying the tracker */ public void startTorrent() { + stopped = false; boolean coordinatorChanged = false; if (coordinator.halted()) { // ok, we have already started and stopped, but the coordinator seems a bit annoying to @@ -375,18 +379,21 @@ public class Snark coordinator = newCoord; coordinatorChanged = true; } - if (trackerclient.halted() || coordinatorChanged) { + if (!trackerclient.started() && !coordinatorChanged) { + trackerclient.start(); + } else if (trackerclient.halted() || coordinatorChanged) { TrackerClient newClient = new TrackerClient(coordinator.getMetaInfo(), coordinator); if (!trackerclient.halted()) trackerclient.halt(); trackerclient = newClient; + trackerclient.start(); } - trackerclient.start(); } /** * Stop contacting the tracker and talking with peers */ public void stopTorrent() { + stopped = true; trackerclient.halt(); coordinator.halt(); try { @@ -417,6 +424,8 @@ public class Snark String ip = null; String torrent = null; + boolean configured = I2PSnarkUtil.instance().configured(); + int i = 0; while (i < args.length) { @@ -463,7 +472,8 @@ public class Snark { String proxyHost = args[i+1]; String proxyPort = args[i+2]; - I2PSnarkUtil.instance().setProxy(proxyHost, Integer.parseInt(proxyPort)); + if (!configured) + I2PSnarkUtil.instance().setProxy(proxyHost, Integer.parseInt(proxyPort)); i += 3; } else if (args[i].equals("--i2cp")) @@ -484,7 +494,8 @@ public class Snark } } } - I2PSnarkUtil.instance().setI2CPConfig(i2cpHost, Integer.parseInt(i2cpPort), opts); + if (!configured) + I2PSnarkUtil.instance().setI2CPConfig(i2cpHost, Integer.parseInt(i2cpPort), opts); i += 3 + (opts != null ? 1 : 0); } else @@ -654,6 +665,8 @@ public class Snark Snark.debug("Completely received " + torrent, Snark.INFO); //storage.close(); System.out.println("Completely received: " + torrent); + if (completeListener != null) + completeListener.torrentComplete(this); } public void shutdown() @@ -662,4 +675,8 @@ public class Snark // have died. But in reality this does not always happen. System.exit(0); } + + public interface CompleteListener { + public void torrentComplete(Snark snark); + } } diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java index 17cd1db4a7..a091ced1b1 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java +++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java @@ -10,7 +10,7 @@ import net.i2p.util.Log; /** * Manage multiple snarks */ -public class SnarkManager { +public class SnarkManager implements Snark.CompleteListener { private static SnarkManager _instance = new SnarkManager(); public static SnarkManager instance() { return _instance; } @@ -35,6 +35,8 @@ public class SnarkManager { _log = _context.logManager().getLog(SnarkManager.class); _messages = new ArrayList(16); loadConfig("i2psnark.config"); + int minutes = getStartupDelayMinutes(); + _messages.add("Starting up torrents in " + minutes + (minutes == 1 ? " minute" : " minutes")); I2PThread monitor = new I2PThread(new DirMonitor(), "Snark DirMonitor"); monitor.setDaemon(true); monitor.start(); @@ -50,9 +52,16 @@ public class SnarkManager { _log.info("MSG: " + message); } - private boolean shouldAutoStart() { return true; } + /** newest last */ + public List getMessages() { + synchronized (_messages) { + return new ArrayList(_messages); + } + } + + public boolean shouldAutoStart() { return true; } private int getStartupDelayMinutes() { return 1; } - private File getDataDir() { + public File getDataDir() { String dir = _config.getProperty(PROP_DIR); if ( (dir == null) || (dir.trim().length() <= 0) ) dir = "i2psnark"; @@ -89,14 +98,14 @@ public class SnarkManager { String i2cpHost = _config.getProperty(PROP_I2CP_HOST); int i2cpPort = getInt(PROP_I2CP_PORT, 7654); String opts = _config.getProperty(PROP_I2CP_OPTS); - Properties i2cpOpts = new Properties(); + Map i2cpOpts = new HashMap(); if (opts != null) { StringTokenizer tok = new StringTokenizer(opts, " "); while (tok.hasMoreTokens()) { String pair = tok.nextToken(); int split = pair.indexOf('='); - if (split > 0) - i2cpOpts.setProperty(pair.substring(0, split), pair.substring(split+1)); + if (split > 0) + i2cpOpts.put(pair.substring(0, split), pair.substring(split+1)); } } if (i2cpHost != null) { @@ -122,6 +131,108 @@ public class SnarkManager { return defaultVal; } + public void updateConfig(String dataDir, boolean autoStart, String seedPct, String eepHost, + String eepPort, String i2cpHost, String i2cpPort, String i2cpOpts) { + boolean changed = false; + if (eepHost != null) { + int port = I2PSnarkUtil.instance().getEepProxyPort(); + try { port = Integer.parseInt(eepPort); } catch (NumberFormatException nfe) {} + String host = I2PSnarkUtil.instance().getEepProxyHost(); + if ( (eepHost.trim().length() > 0) && (port > 0) && + ((!host.equals(eepHost) || (port != I2PSnarkUtil.instance().getEepProxyPort()) )) ) { + I2PSnarkUtil.instance().setProxy(eepHost, port); + changed = true; + _config.setProperty(PROP_EEP_HOST, eepHost); + _config.setProperty(PROP_EEP_PORT, eepPort+""); + addMessage("EepProxy location changed to " + eepHost + ":" + port); + } + } + if (i2cpHost != null) { + int oldI2CPPort = I2PSnarkUtil.instance().getI2CPPort(); + String oldI2CPHost = I2PSnarkUtil.instance().getI2CPHost(); + int port = oldI2CPPort; + try { port = Integer.parseInt(i2cpPort); } catch (NumberFormatException nfe) {} + String host = oldI2CPHost; + Map opts = new HashMap(); + if (i2cpOpts == null) i2cpOpts = ""; + StringTokenizer tok = new StringTokenizer(i2cpOpts, " \t\n"); + while (tok.hasMoreTokens()) { + String pair = tok.nextToken(); + int split = pair.indexOf('='); + if (split > 0) + opts.put(pair.substring(0, split), pair.substring(split+1)); + } + Map oldOpts = new HashMap(); + String oldI2CPOpts = _config.getProperty(PROP_I2CP_OPTS); + if (oldI2CPOpts == null) oldI2CPOpts = ""; + tok = new StringTokenizer(oldI2CPOpts, " \t\n"); + while (tok.hasMoreTokens()) { + String pair = tok.nextToken(); + int split = pair.indexOf('='); + if (split > 0) + oldOpts.put(pair.substring(0, split), pair.substring(split+1)); + } + + if ( (i2cpHost.trim().length() > 0) && (port > 0) && + ((!host.equals(i2cpHost) || + (port != I2PSnarkUtil.instance().getI2CPPort()) || + (!oldOpts.equals(opts)))) ) { + boolean snarksActive = false; + Set names = listTorrentFiles(); + for (Iterator iter = names.iterator(); iter.hasNext(); ) { + Snark snark = getTorrent((String)iter.next()); + if ( (snark != null) && (!snark.stopped) ) { + snarksActive = true; + break; + } + } + if (snarksActive) { + addMessage("Cannot change the I2CP settings while torrents are active"); + _log.debug("i2cp host [" + i2cpHost + "] i2cp port " + port + " opts [" + opts + + "] oldOpts [" + oldOpts + "]"); + } else { + if (I2PSnarkUtil.instance().connected()) { + I2PSnarkUtil.instance().disconnect(); + addMessage("Disconnecting old I2CP destination"); + } + Properties p = new Properties(); + p.putAll(opts); + addMessage("I2CP settings changed to " + i2cpHost + ":" + port + " (" + i2cpOpts.trim() + ")"); + I2PSnarkUtil.instance().setI2CPConfig(i2cpHost, port, p); + boolean ok = I2PSnarkUtil.instance().connect(); + if (!ok) { + addMessage("Unable to connect with the new settings, reverting to the old I2CP settings"); + I2PSnarkUtil.instance().setI2CPConfig(oldI2CPHost, oldI2CPPort, oldOpts); + ok = I2PSnarkUtil.instance().connect(); + if (!ok) + addMessage("Unable to reconnect with the old settings!"); + } else { + addMessage("Reconnected on the new I2CP destination"); + _config.setProperty(PROP_I2CP_HOST, i2cpHost.trim()); + _config.setProperty(PROP_I2CP_PORT, "" + port); + _config.setProperty(PROP_I2CP_OPTS, i2cpOpts.trim()); + changed = true; + // no PeerAcceptors/I2PServerSockets to deal with, since all snarks are inactive + for (Iterator iter = names.iterator(); iter.hasNext(); ) { + String name = (String)iter.next(); + Snark snark = getTorrent(name); + if ( (snark != null) && (snark.acceptor != null) ) { + snark.acceptor.restart(); + addMessage("I2CP listener restarted for " + snark.meta.getName()); + } + } + } + } + changed = true; + } + } + if (changed) { + saveConfig(); + } else { + addMessage("Configuration unchanged"); + } + } + public void saveConfig() { try { DataHelper.storeProps(_config, new File(_configFile)); @@ -130,6 +241,8 @@ public class SnarkManager { } } + public Properties getConfig() { return _config; } + /** set of filenames that we are dealing with */ public Set listTorrentFiles() { synchronized (_snarks) { return new HashSet(_snarks.keySet()); } } /** @@ -151,17 +264,19 @@ public class SnarkManager { torrent = (Snark)_snarks.get(filename); if (torrent == null) { torrent = new Snark(filename, null, -1, null, null, false, dataDir.getPath()); + torrent.completeListener = this; _snarks.put(filename, torrent); } else { return; } } // ok, snark created, now lets start it up or configure it further + File f = new File(filename); if (shouldAutoStart()) { torrent.startTorrent(); - addMessage("Torrent added and started: '" + filename + "'"); + addMessage("Torrent added and started: '" + f.getName() + "'"); } else { - addMessage("Torrent added: '" + filename + "'"); + addMessage("Torrent added: '" + f.getName() + "'"); } } @@ -187,13 +302,15 @@ public class SnarkManager { remaining = _snarks.size(); } if (torrent != null) { + boolean wasStopped = torrent.stopped; torrent.stopTorrent(); if (remaining == 0) { // should we disconnect/reconnect here (taking care to deal with the other thread's // I2PServerSocket.accept() call properly?) ////I2PSnarkUtil.instance(). } - addMessage("Torrent stopped: '" + filename + "'"); + if (!wasStopped) + addMessage("Torrent stopped: '" + sfile.getName() + "'"); } return torrent; } @@ -206,7 +323,7 @@ public class SnarkManager { if (torrent != null) { File torrentFile = new File(filename); torrentFile.delete(); - addMessage("Torrent removed: '" + filename + "'"); + addMessage("Torrent removed: '" + torrentFile.getName() + "'"); } } @@ -222,6 +339,13 @@ public class SnarkManager { } } + public void torrentComplete(Snark snark) { + File f = new File(snark.torrent); + long len = snark.meta.getTotalLength(); + addMessage("Download complete of " + f.getName() + + (len < 5*1024*1024 ? " (size: " + (len/1024) + "KB)" : " (size: " + (len/1024*1024) + "MB)")); + } + private void monitorTorrents(File dir) { String fileNames[] = dir.list(TorrentFilenameFilter.instance()); List foundNames = new ArrayList(0); @@ -241,7 +365,10 @@ public class SnarkManager { if (existingNames.contains(foundNames.get(i))) { // already known. noop } else { - addTorrent((String)foundNames.get(i)); + if (I2PSnarkUtil.instance().connect()) + addTorrent((String)foundNames.get(i)); + else + addMessage("Unable to connect to I2P"); } } // now lets see which ones have been removed... diff --git a/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java b/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java index 9a07b476df..df2e243772 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java +++ b/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java @@ -48,6 +48,7 @@ public class TrackerClient extends Thread private final int port; private boolean stop; + private boolean started; private long interval; private long lastRequestTime; @@ -62,14 +63,17 @@ public class TrackerClient extends Thread this.port = 6881; //(port == -1) ? 9 : port; stop = false; + started = false; } public void start() { - stop = false; + if (stop) throw new RuntimeException("Dont rerun me, create a copy"); super.start(); + started = true; } public boolean halted() { return stop; } + public boolean started() { return started; } /** * Interrupts this Thread to stop it. @@ -107,8 +111,10 @@ public class TrackerClient extends Thread TrackerInfo info = doRequest(announce, infoHash, peerID, uploaded, downloaded, left, STARTED_EVENT); + Set peers = info.getPeers(); + coordinator.trackerSeenPeers = peers.size(); if (!completed) { - Iterator it = info.getPeers().iterator(); + Iterator it = peers.iterator(); while (it.hasNext()) { Peer cur = (Peer)it.next(); coordinator.addPeer(cur); @@ -118,6 +124,7 @@ public class TrackerClient extends Thread } } started = true; + coordinator.trackerProblems = null; } catch (IOException ioe) { @@ -125,6 +132,7 @@ public class TrackerClient extends Thread Snark.debug ("WARNING: Could not contact tracker at '" + announce + "': " + ioe, Snark.WARNING); + coordinator.trackerProblems = ioe.getMessage(); } if (!started && !stop) @@ -182,10 +190,12 @@ public class TrackerClient extends Thread uploaded, downloaded, left, event); + Set peers = info.getPeers(); + coordinator.trackerSeenPeers = peers.size(); if ( (left > 0) && (!completed) ) { // we only want to talk to new people if we need things // from them (duh) - Iterator it = info.getPeers().iterator(); + Iterator it = peers.iterator(); while (it.hasNext()) { Peer cur = (Peer)it.next(); coordinator.addPeer(cur); @@ -244,21 +254,24 @@ public class TrackerClient extends Thread throw new IOException("Error fetching " + s); } - fetched.deleteOnExit(); - InputStream in = new FileInputStream(fetched); + try { + InputStream in = new FileInputStream(fetched); - TrackerInfo info = new TrackerInfo(in, coordinator.getID(), - coordinator.getMetaInfo()); - if (Snark.debug >= Snark.INFO) - Snark.debug("TrackerClient response: " + info, Snark.INFO); - lastRequestTime = System.currentTimeMillis(); - - String failure = info.getFailureReason(); - if (failure != null) - throw new IOException(failure); - - interval = info.getInterval() * 1000; - return info; + TrackerInfo info = new TrackerInfo(in, coordinator.getID(), + coordinator.getMetaInfo()); + if (Snark.debug >= Snark.INFO) + Snark.debug("TrackerClient response: " + info, Snark.INFO); + lastRequestTime = System.currentTimeMillis(); + + String failure = info.getFailureReason(); + if (failure != null) + throw new IOException(failure); + + interval = info.getInterval() * 1000; + return info; + } finally { + fetched.delete(); + } } /** diff --git a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java new file mode 100644 index 0000000000..5f4bafab6f --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java @@ -0,0 +1,524 @@ +package org.klomp.snark.web; + +import java.io.*; +import java.util.*; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServlet; + +import net.i2p.I2PAppContext; +import net.i2p.data.Base64; +import net.i2p.data.DataHelper; +import net.i2p.util.FileUtil; +import net.i2p.util.I2PThread; +import net.i2p.util.Log; +import org.klomp.snark.*; + +/** + * + */ +public class I2PSnarkServlet extends HttpServlet { + private I2PAppContext _context; + private Log _log; + private SnarkManager _manager; + private static long _nonce; + + public static final String PROP_CONFIG_FILE = "i2psnark.configFile"; + + public void init(ServletConfig cfg) throws ServletException { + super.init(cfg); + _context = I2PAppContext.getGlobalContext(); + _log = _context.logManager().getLog(I2PSnarkServlet.class); + _nonce = _context.random().nextLong(); + _manager = SnarkManager.instance(); + String configFile = _context.getProperty(PROP_CONFIG_FILE); + if ( (configFile == null) || (configFile.trim().length() <= 0) ) + configFile = "i2psnark.config"; + _manager.loadConfig(configFile); + } + + public void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + req.setCharacterEncoding("UTF-8"); + resp.setCharacterEncoding("UTF-8"); + resp.setContentType("text/html; charset=UTF-8"); + + String nonce = req.getParameter("nonce"); + if ( (nonce != null) && (nonce.equals(String.valueOf(_nonce))) ) + processRequest(req); + + PrintWriter out = resp.getWriter(); + out.write(HEADER_BEGIN); + // we want it to go to the base URI so we don't refresh with some funky action= value + out.write("<meta http-equiv=\"refresh\" content=\"60;" + req.getRequestURI() + "\">\n"); + out.write(HEADER); + out.write("<textarea class=\"snarkMessages\" rows=\"2\" cols=\"100\" wrap=\"off\" >"); + List msgs = _manager.getMessages(); + for (int i = msgs.size()-1; i >= 0; i--) { + String msg = (String)msgs.get(i); + out.write(msg + "\n"); + } + out.write("</textarea>\n"); + + out.write(TABLE_HEADER); + + List snarks = getSortedSnarks(req); + String uri = req.getRequestURI(); + for (int i = 0; i < snarks.size(); i++) { + Snark snark = (Snark)snarks.get(i); + displaySnark(out, snark, uri, i); + } + if (snarks.size() <= 0) { + out.write(TABLE_EMPTY); + } + + out.write(TABLE_FOOTER); + writeAddForm(out, req); + writeConfigForm(out, req); + out.write(FOOTER); + } + + /** + * Do what they ask, adding messages to _manager.addMessage as necessary + */ + private void processRequest(HttpServletRequest req) { + String action = req.getParameter("action"); + if (action == null) { + // noop + } else if ("Add torrent".equals(action)) { + String newFile = req.getParameter("newFile"); + String newURL = req.getParameter("newURL"); + File f = null; + if ( (newFile != null) && (newFile.trim().length() > 0) ) + f = new File(newFile.trim()); + if ( (f != null) && (!f.exists()) ) { + _manager.addMessage("Torrent file " + newFile +" does not exist"); + } + if ( (f != null) && (f.exists()) ) { + File local = new File(_manager.getDataDir(), f.getName()); + String canonical = null; + try { + canonical = local.getCanonicalPath(); + + if (local.exists()) { + if (_manager.getTorrent(canonical) != null) + _manager.addMessage("Torrent already running: " + newFile); + else + _manager.addMessage("Torrent already in the queue: " + newFile); + } else { + boolean ok = FileUtil.copy(f.getAbsolutePath(), local.getAbsolutePath(), true); + if (ok) { + _manager.addMessage("Copying torrent to " + local.getAbsolutePath()); + _manager.addTorrent(canonical); + } else { + _manager.addMessage("Unable to copy the torrent to " + local.getAbsolutePath() + " from " + f.getAbsolutePath()); + } + } + } catch (IOException ioe) { + _log.warn("hrm: " + local, ioe); + } + } else if ( (newURL != null) && (newURL.trim().length() > "http://.i2p/".length()) ) { + _manager.addMessage("Fetching " + newURL); + I2PThread fetch = new I2PThread(new FetchAndAdd(newURL), "Fetch and add"); + fetch.start(); + } else { + // no file or URL specified + } + } else if ("Stop".equals(action)) { + String torrent = req.getParameter("torrent"); + if (torrent != null) { + byte infoHash[] = Base64.decode(torrent); + if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1 + for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) { + String name = (String)iter.next(); + Snark snark = _manager.getTorrent(name); + if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) { + _manager.stopTorrent(name, false); + break; + } + } + } + } + } else if ("Start".equals(action)) { + String torrent = req.getParameter("torrent"); + if (torrent != null) { + byte infoHash[] = Base64.decode(torrent); + if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1 + for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) { + String name = (String)iter.next(); + Snark snark = _manager.getTorrent(name); + if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) { + snark.startTorrent(); + _manager.addMessage("Starting up torrent " + name); + break; + } + } + } + } + } else if ("Remove".equals(action)) { + String torrent = req.getParameter("torrent"); + if (torrent != null) { + byte infoHash[] = Base64.decode(torrent); + if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1 + for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) { + String name = (String)iter.next(); + Snark snark = _manager.getTorrent(name); + if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) { + _manager.stopTorrent(name, true); + // should we delete the torrent file? + break; + } + } + } + } + } else if ("Delete".equals(action)) { + String torrent = req.getParameter("torrent"); + if (torrent != null) { + byte infoHash[] = Base64.decode(torrent); + if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1 + for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) { + String name = (String)iter.next(); + Snark snark = _manager.getTorrent(name); + if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) { + _manager.stopTorrent(name, true); + File f = new File(name); + f.delete(); + _manager.addMessage("Torrent file deleted: " + f.getAbsolutePath()); + List files = snark.meta.getFiles(); + String dataFile = snark.meta.getName(); + for (int i = 0; files != null && i < files.size(); i++) { + File df = new File(_manager.getDataDir(), (String)files.get(i)); + boolean deleted = FileUtil.rmdir(df, false); + if (deleted) + _manager.addMessage("Data dir deleted: " + df.getAbsolutePath()); + else + _manager.addMessage("Data dir could not be deleted: " + df.getAbsolutePath()); + } + if (dataFile != null) { + f = new File(_manager.getDataDir(), dataFile); + boolean deleted = f.delete(); + if (deleted) + _manager.addMessage("Data file deleted: " + f.getAbsolutePath()); + else + _manager.addMessage("Data file could not be deleted: " + f.getAbsolutePath()); + } + break; + } + } + } + } + } else if ("Save configuration".equals(action)) { + String dataDir = req.getParameter("dataDir"); + boolean autoStart = req.getParameter("autoStart") != null; + String seedPct = req.getParameter("seedPct"); + String eepHost = req.getParameter("eepHost"); + String eepPort = req.getParameter("eepPort"); + String i2cpHost = req.getParameter("i2cpHost"); + String i2cpPort = req.getParameter("i2cpPort"); + String i2cpOpts = req.getParameter("i2cpOpts"); + _manager.updateConfig(dataDir, autoStart, seedPct, eepHost, eepPort, i2cpHost, i2cpPort, i2cpOpts); + } + } + + private class FetchAndAdd implements Runnable { + private String _url; + public FetchAndAdd(String url) { + _url = url; + } + public void run() { + _url = _url.trim(); + File file = I2PSnarkUtil.instance().get(_url, false); + try { + if ( (file != null) && (file.exists()) && (file.length() > 0) ) { + _manager.addMessage("Torrent fetched from " + _url); + FileInputStream in = null; + try { + in = new FileInputStream(file); + MetaInfo info = new MetaInfo(in); + String name = info.getName(); + name = name.replace('/', '_'); + name = name.replace('\\', '_'); + name = name.replace('&', '+'); + name = name.replace('\'', '_'); + name = name.replace('"', '_'); + name = name.replace('`', '_'); + name = name + ".torrent"; + File torrentFile = new File(_manager.getDataDir(), name); + + String canonical = torrentFile.getCanonicalPath(); + + if (torrentFile.exists()) { + if (_manager.getTorrent(canonical) != null) + _manager.addMessage("Torrent already running: " + name); + else + _manager.addMessage("Torrent already in the queue: " + name); + } else { + FileUtil.copy(file.getAbsolutePath(), canonical, true); + _manager.addTorrent(canonical); + } + } catch (IOException ioe) { + _manager.addMessage("Torrent at " + _url + " was not valid: " + ioe.getMessage()); + } finally { + try { in.close(); } catch (IOException ioe) {} + } + } else { + _manager.addMessage("Torrent was not retrieved from " + _url); + } + } finally { + if (file != null) file.delete(); + } + } + } + + private List getSortedSnarks(HttpServletRequest req) { + Set files = _manager.listTorrentFiles(); + TreeSet fileNames = new TreeSet(files); // sorts it alphabetically + ArrayList rv = new ArrayList(fileNames.size()); + for (Iterator iter = fileNames.iterator(); iter.hasNext(); ) { + String name = (String)iter.next(); + Snark snark = _manager.getTorrent(name); + if (snark != null) + rv.add(snark); + } + return rv; + } + + private static final int MAX_DISPLAYED_FILENAME_LENGTH = 60; + private void displaySnark(PrintWriter out, Snark snark, String uri, int row) throws IOException { + String filename = snark.torrent; + File f = new File(filename); + filename = f.getName(); // the torrent may be the canonical name, so lets just grab the local name + if (filename.length() > MAX_DISPLAYED_FILENAME_LENGTH) + filename = filename.substring(0, MAX_DISPLAYED_FILENAME_LENGTH) + "..."; + long total = snark.meta.getTotalLength(); + long remaining = snark.storage.needed() * snark.meta.getPieceLength(0); + if (remaining > total) + remaining = total; + int totalBps = 4096; // should probably grab this from the snark... + long remainingSeconds = remaining / totalBps; + long uploaded = snark.coordinator.getUploaded(); + + boolean isRunning = !snark.stopped; + boolean isValid = snark.meta != null; + + String err = snark.coordinator.trackerProblems; + int curPeers = snark.coordinator.getPeerCount(); + int knownPeers = snark.coordinator.trackerSeenPeers; + + String statusString = "Unknown"; + if (err != null) { + if (isRunning) + statusString = "TrackerErr (" + curPeers + "/" + knownPeers + " peers)"; + else + statusString = "TrackerErr (" + err + ")"; + } else if (remaining <= 0) { + if (isRunning) + statusString = "Seeding (" + curPeers + "/" + knownPeers + " peers)"; + else + statusString = "Complete"; + } else { + if (isRunning) + statusString = "OK (" + curPeers + "/" + knownPeers + " peers)"; + else + statusString = "Stopped"; + } + + String rowClass = (row % 2 == 0 ? "snarkTorrentEven" : "snarkTorrentOdd"); + out.write("<tr class=\"" + rowClass + "\">"); + out.write("<td valign=\"top\" align=\"left\" class=\"snarkTorrentStatus " + rowClass + "\">"); + out.write(statusString + "</td>\n\t"); + out.write("<td valign=\"top\" align=\"left\" class=\"snarkTorrentName " + rowClass + "\">"); + out.write(filename + "</td>\n\t"); + out.write("<td valign=\"top\" align=\"left\" class=\"snarkTorrentDownloaded " + rowClass + "\">"); + if (remaining > 0) { + out.write(formatSize(total-remaining) + "/" + formatSize(total)); // 18MB/3GB + // lets hold off on the ETA until we have rates sorted... + //out.write(" (eta " + DataHelper.formatDuration(remainingSeconds*1000) + ")"); // (eta 6h) + } else { + out.write(formatSize(total)); // 3GB + } + out.write("</td>\n\t"); + out.write("<td valign=\"top\" align=\"left\" class=\"snarkTorrentUploaded " + rowClass + + "\">" + formatSize(uploaded) + "</td>\n\t"); + //out.write("<td valign=\"top\" align=\"left\" class=\"snarkTorrentRate\">"); + //out.write("n/a"); //2KBps/12KBps/4KBps + //out.write("</td>\n\t"); + out.write("<td valign=\"top\" align=\"left\" class=\"snarkTorrentAction " + rowClass + "\">"); + if (isRunning) { + out.write("<a href=\"" + uri + "?action=Stop&nonce=" + _nonce + + "&torrent=" + Base64.encode(snark.meta.getInfoHash()) + + "\" title=\"Stop the torrent\">Stop</a>"); + } else { + if (isValid) + out.write("<a href=\"" + uri + "?action=Start&nonce=" + _nonce + + "&torrent=" + Base64.encode(snark.meta.getInfoHash()) + + "\" title=\"Start the torrent\">Start</a> "); + out.write("<a href=\"" + uri + "?action=Remove&nonce=" + _nonce + + "&torrent=" + Base64.encode(snark.meta.getInfoHash()) + + "\" title=\"Remove the torrent from the active list, deleting the .torrent file\">Remove</a><br />"); + out.write("<a href=\"" + uri + "?action=Delete&nonce=" + _nonce + + "&torrent=" + Base64.encode(snark.meta.getInfoHash()) + + "\" title=\"Delete the .torrent file and the associated data file(s)\">Delete</a> "); + } + out.write("</td>\n</tr>\n"); + } + + private void writeAddForm(PrintWriter out, HttpServletRequest req) throws IOException { + String uri = req.getRequestURI(); + String newURL = req.getParameter("newURL"); + if ( (newURL == null) || (newURL.trim().length() <= 0) ) newURL = "http://"; + String newFile = req.getParameter("newFile"); + if ( (newFile == null) || (newFile.trim().length() <= 0) ) newFile = ""; + + out.write("<span class=\"snarkNewTorrent\">\n"); + // *not* enctype="multipart/form-data", so that the input type=file sends the filename, not the file + out.write("<form action=\"" + uri + "\" method=\"POST\">\n"); + out.write("<input type=\"hidden\" name=\"nonce\" value=\"" + _nonce + "\" />\n"); + out.write("From URL : <input type=\"text\" name=\"newURL\" size=\"50\" value=\"" + newURL + "\" /> \n"); + // not supporting from file at the moment, since the file name passed isn't always absolute (so it may not resolve) + //out.write("From file: <input type=\"file\" name=\"newFile\" size=\"50\" value=\"" + newFile + "\" /><br />\n"); + out.write("<input type=\"submit\" value=\"Add torrent\" name=\"action\" /><br />\n"); + out.write("Alternately, you can copy .torrent files to " + _manager.getDataDir().getAbsolutePath() + "<br />\n"); + out.write("</form>\n</span>\n"); + } + + private void writeConfigForm(PrintWriter out, HttpServletRequest req) throws IOException { + String uri = req.getRequestURI(); + String dataDir = _manager.getDataDir().getAbsolutePath(); + boolean autoStart = _manager.shouldAutoStart(); + int seedPct = 0; + + out.write("<span class=\"snarkConfig\">\n"); + out.write("<form action=\"" + uri + "\" method=\"POST\">\n"); + out.write("<input type=\"hidden\" name=\"nonce\" value=\"" + _nonce + "\" />\n"); + out.write("<hr /><span class=\"snarkConfigTitle\">Configuration:</span><br />\n"); + out.write("Data directory: <input type=\"text\" size=\"40\" name=\"dataDir\" value=\"" + dataDir + "\" "); + out.write("title=\"Directory to store torrents and data\" disabled=\"true\" /><br />\n"); + out.write("Auto start: <input type=\"checkbox\" name=\"autoStart\" value=\"true\" " + + (autoStart ? "checked " : "") + + "title=\"If true, automatically start torrents that are added\" disabled=\"true\" />"); + //Auto add: <input type="checkbox" name="autoAdd" value="true" title="If true, automatically add torrents that are found in the data directory" /> + //Auto stop: <input type="checkbox" name="autoStop" value="true" title="If true, automatically stop torrents that are removed from the data directory" /> + //out.write("<br />\n"); + out.write("Seed percentage: <select name=\"seedPct\" disabled=\"true\" >\n\t"); + if (seedPct <= 0) + out.write("<option value=\"0\" selected=\"true\">Unlimited</option>\n\t"); + else + out.write("<option value=\"0\">Unlimited</option>\n\t"); + if (seedPct == 100) + out.write("<option value=\"100\" selected=\"true\">100%</option>\n\t"); + else + out.write("<option value=\"100\">100%</option>\n\t"); + if (seedPct == 150) + out.write("<option value=\"150\" selected=\"true\">150%</option>\n\t"); + else + out.write("<option value=\"150\">150%</option>\n\t"); + out.write("</select><br />\n"); + + out.write("<hr />\n"); + out.write("EepProxy host: <input type=\"text\" name=\"eepHost\" value=\"" + + I2PSnarkUtil.instance().getEepProxyHost() + "\" size=\"15\" /> "); + out.write("EepProxy port: <input type=\"text\" name=\"eepPort\" value=\"" + + I2PSnarkUtil.instance().getEepProxyPort() + "\" size=\"5\" /> <br />\n"); + out.write("I2CP host: <input type=\"text\" name=\"i2cpHost\" value=\"" + + I2PSnarkUtil.instance().getI2CPHost() + "\" size=\"15\" /> "); + out.write("I2CP port: <input type=\"text\" name=\"i2cpPort\" value=\"" + + + I2PSnarkUtil.instance().getI2CPPort() + "\" size=\"5\" /> <br />\n"); + StringBuffer opts = new StringBuffer(64); + Map options = new TreeMap(I2PSnarkUtil.instance().getI2CPOptions()); + for (Iterator iter = options.keySet().iterator(); iter.hasNext(); ) { + String key = (String)iter.next(); + String val = (String)options.get(key); + opts.append(key).append('=').append(val).append(' '); + } + out.write("I2CP options: <input type=\"text\" name=\"i2cpOpts\" size=\"80\" value=\"" + + opts.toString() + "\" /><br />\n"); + out.write("<input type=\"submit\" value=\"Save configuration\" name=\"action\" />\n"); + out.write("</form>\n</span>\n"); + } + + private String formatSize(long bytes) { + if (bytes < 5*1024) + return bytes + "B"; + else if (bytes < 5*1024*1024) + return (bytes/1024) + "KB"; + else if (bytes < 5*1024*1024*1024) + return (bytes/(1024*1024)) + "MB"; + else + return (bytes/(1024*1024*1024)) + "GB"; + } + + private static final String HEADER_BEGIN = "<html>\n" + + "<head>\n" + + "<title>I2PSnark - anonymous bittorrent</title>\n"; + + private static final String HEADER = "<style>\n" + + "body {\n" + + " background-color: #C7CFB4;\n" + + "}\n" + + ".snarkTitle {\n" + + " text-align: left;\n" + + " float: left;\n" + + " margin: 0px 0px 5px 5px;\n" + + " display: inline;\n" + + " font-size: 16pt;\n" + + " font-weight: bold;\n" + + "}\n" + + ".snarkMessages {\n" + + " border: none;\n" + + " background-color: #CECFC6;\n" + + " font-family: monospace;\n" + + " font-size: 10pt;\n" + + " font-weight: 100;\n" + + "}\n" + + "table {\n" + + " margin: 0px 0px 0px 0px;\n" + + " border: 0px;\n" + + " padding: 0px;\n" + + " border-width: 0px;\n" + + " border-spacing: 0px;\n" + + "}\n" + + "th {\n" + + " background-color: #C7D5D5;\n" + + " margin: 0px 0px 0px 0px;\n" + + "}\n" + + ".snarkTorrentEven {\n" + + " background-color: #E7E7E7;\n" + + "}\n" + + ".snarkTorrentOdd {\n" + + " background-color: #DDDDCC;\n" + + "}\n" + + ".snarkNewTorrent {\n" + + " font-size: 12pt;\n" + + " font-family: monospace;\n" + + " background-color: #ADAE9;\n" + + "}\n" + + ".snarkConfigTitle {\n" + + " font-size: 16pt;\n" + + " font-weight: bold;\n" + + "}\n" + + "</style>\n" + + "</head>\n" + + "<body>\n" + + "<p class=\"snarkTitle\">I2PSnark </p>\n"; + + + private static final String TABLE_HEADER = "<table border=\"0\" class=\"snarkTorrents\" width=\"100%\">\n" + + "<thead>\n" + + "<tr><th align=\"left\" valign=\"top\">Status</th>\n" + + " <th align=\"left\" valign=\"top\">Torrent</th>\n" + + " <th align=\"left\" valign=\"top\">Downloaded</th>\n" + + " <th align=\"left\" valign=\"top\">Uploaded</th>\n" + + //" <th align=\"left\" valign=\"top\">Rate</th>\n" + + " <th> </th></tr>\n" + + "</thead>\n"; + + private static final String TABLE_EMPTY = "<tr class=\"snarkTorrentEven\">" + + "<td class=\"snarkTorrentEven\" align=\"left\"" + + " valign=\"top\" colspan=\"5\">No torrents</td></tr>\n"; + + private static final String TABLE_FOOTER = "</table>\n"; + + private static final String FOOTER = "</body></html>"; +} \ No newline at end of file diff --git a/apps/i2psnark/web.xml b/apps/i2psnark/web.xml new file mode 100644 index 0000000000..71260245f1 --- /dev/null +++ b/apps/i2psnark/web.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> +<!DOCTYPE web-app + PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN" + "http://java.sun.com/j2ee/dtds/web-app_2.2.dtd"> + +<web-app> + <servlet> + <servlet-name>org.klomp.snark.web.I2PSnarkServlet</servlet-name> + <servlet-class>org.klomp.snark.web.I2PSnarkServlet</servlet-class> + <load-on-startup>1</load-on-startup> + </servlet> + + <!-- precompiled servlets --> + + <servlet-mapping> + <servlet-name>org.klomp.snark.web.I2PSnarkServlet</servlet-name> + <url-pattern>/</url-pattern> + </servlet-mapping> + + <session-config> + <session-timeout> + 30 + </session-timeout> + </session-config> +</web-app> diff --git a/apps/routerconsole/jsp/nav.jsp b/apps/routerconsole/jsp/nav.jsp index d555922d1b..1a3aefc6c7 100644 --- a/apps/routerconsole/jsp/nav.jsp +++ b/apps/routerconsole/jsp/nav.jsp @@ -25,6 +25,7 @@ <a href="susimail/susimail">Susimail</a> | <a href="susidns/index.jsp">SusiDNS</a> | <a href="syndie/">Syndie</a> | + <a href="i2psnark/">I2PSnark</a> | <a href="http://localhost:7658/">My Eepsite</a> <br> <a href="i2ptunnel/index.jsp">I2PTunnel</a> | <a href="tunnels.jsp">Tunnels</a> | diff --git a/build.xml b/build.xml index 6000d79a5a..ed229b8f46 100644 --- a/build.xml +++ b/build.xml @@ -31,7 +31,7 @@ <ant dir="apps/susimail/" target="war" /> <ant dir="apps/susidns/src" target="all" /> <ant dir="apps/syndie/java/" target="jar" /> - <ant dir="apps/i2psnark/java/" target="jar" /> + <ant dir="apps/i2psnark/java/" target="war" /> </target> <target name="buildrouter"> <ant dir="core/java/" target="distclean" /> @@ -108,7 +108,7 @@ <copy file="apps/syndie/syndie.war" todir="build/" /> <copy file="apps/syndie/java/build/syndie.jar" todir="build/" /> <copy file="apps/syndie/java/build/sucker.jar" todir="build/" /> - <copy file="apps/syndie/syndie.war" todir="build/" /> + <copy file="apps/i2psnark/i2psnark.war" todir="build/" /> <copy file="apps/i2psnark/java/build/i2psnark.jar" todir="build/" /> <copy file="apps/jdom/jdom.jar" todir="build/" /> <copy file="apps/rome/rome-0.7.jar" todir="build/" /> @@ -250,6 +250,7 @@ <copy file="build/susimail.war" todir="pkg-temp/webapps/" /> <copy file="build/susidns.war" todir="pkg-temp/webapps/" /> <copy file="build/syndie.war" todir="pkg-temp/webapps/" /> + <copy file="build/i2psnark.war" todir="pkg-temp/webapps/" /> <copy file="installer/resources/clients.config" todir="pkg-temp/" /> <copy file="installer/resources/eepget" todir="pkg-temp/" /> <copy file="installer/resources/i2prouter" todir="pkg-temp/" /> @@ -359,6 +360,7 @@ <copy file="build/susimail.war" todir="pkg-temp/webapps/" /> <copy file="build/susidns.war" todir="pkg-temp/webapps/" /> <copy file="build/syndie.war" todir="pkg-temp/webapps/" /> + <copy file="build/i2psnark.war" todir="pkg-temp/webapps/" /> <copy file="history.txt" todir="pkg-temp/" /> <mkdir dir="pkg-temp/docs/" /> <copy file="news.xml" todir="pkg-temp/docs/" /> diff --git a/history.txt b/history.txt index 9f2506acad..e1d33d12b3 100644 --- a/history.txt +++ b/history.txt @@ -1,4 +1,7 @@ -$Id: history.txt,v 1.355 2005/12/14 04:32:52 jrandom Exp $ +$Id: history.txt,v 1.356 2005/12/15 03:58:31 jrandom Exp $ + +2005-12-15 jrandom + * Added a first pass to the I2PSnark web UI (see /i2psnark/) 2005-12-15 jrandom * Added multitorrent support to I2PSnark, accessible currently by running -- GitLab