From a7225a2379721a344366ffcd39f180a8af17bbca Mon Sep 17 00:00:00 2001 From: zzz Date: Wed, 12 Nov 2014 14:48:51 +0000 Subject: [PATCH] Fix parameter decoding for scrape also Add caching for info hashes and peer ids; move from ByteArray to SDS Stop cleaner when plugin stops Move to the ClientApp interface, remove all static refs Attempt to fix crash after update --- CHANGES.txt | 8 + src/java/net/i2p/zzzot/InfoHash.java | 16 +- src/java/net/i2p/zzzot/PID.java | 16 +- src/java/net/i2p/zzzot/Torrents.java | 61 +++++++ src/java/net/i2p/zzzot/ZzzOT.java | 30 +++- src/java/net/i2p/zzzot/ZzzOTController.java | 184 ++++++++++++++++---- src/jsp/announce.jsp | 27 ++- src/jsp/index.jsp | 17 +- src/jsp/scrape.jsp | 16 +- src/jsp/seedless.jsp | 4 + 10 files changed, 302 insertions(+), 77 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 849bfae..675236e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,11 @@ +0.12.0 + 2014-xx-xx + Fix parameter decoding for scrape also + Add caching for info hashes and peer ids + Stop cleaner when plugin stops + Move to the ClientApp interface, remove all static refs + Attempt to fix crash after update + 0.11.0 2014-11-11 Critical fix for announce parameter decoding, triggered by recent Jetty versions diff --git a/src/java/net/i2p/zzzot/InfoHash.java b/src/java/net/i2p/zzzot/InfoHash.java index a8636ee..7924413 100644 --- a/src/java/net/i2p/zzzot/InfoHash.java +++ b/src/java/net/i2p/zzzot/InfoHash.java @@ -16,22 +16,20 @@ package net.i2p.zzzot; * */ -import java.io.UnsupportedEncodingException; - -import net.i2p.data.ByteArray; +import net.i2p.data.SimpleDataStructure; /** * A 20-byte SHA1 info hash */ -public class InfoHash extends ByteArray { +public class InfoHash extends SimpleDataStructure { - public InfoHash(String data) throws UnsupportedEncodingException { - this(data.getBytes("ISO-8859-1")); - } + public static final int LENGTH = 20; public InfoHash(byte[] data) { super(data); - if (data.length != 20) - throw new IllegalArgumentException("Bad infohash length: " + data.length); + } + + public int length() { + return LENGTH; } } diff --git a/src/java/net/i2p/zzzot/PID.java b/src/java/net/i2p/zzzot/PID.java index 142ddee..2e3d137 100644 --- a/src/java/net/i2p/zzzot/PID.java +++ b/src/java/net/i2p/zzzot/PID.java @@ -16,22 +16,20 @@ package net.i2p.zzzot; * */ -import java.io.UnsupportedEncodingException; - -import net.i2p.data.ByteArray; +import net.i2p.data.SimpleDataStructure; /** * A 20-byte peer ID */ -public class PID extends ByteArray { +public class PID extends SimpleDataStructure { - public PID(String data) throws UnsupportedEncodingException { - this(data.getBytes("ISO-8859-1")); - } + public static final int LENGTH = 20; public PID(byte[] data) { super(data); - if (data.length != 20) - throw new IllegalArgumentException("Bad peer ID length: " + data.length); + } + + public int length() { + return LENGTH; } } diff --git a/src/java/net/i2p/zzzot/Torrents.java b/src/java/net/i2p/zzzot/Torrents.java index a01dc73..e45b903 100644 --- a/src/java/net/i2p/zzzot/Torrents.java +++ b/src/java/net/i2p/zzzot/Torrents.java @@ -18,13 +18,26 @@ package net.i2p.zzzot; import java.util.concurrent.ConcurrentHashMap; +import net.i2p.CoreVersion; +import net.i2p.data.DataHelper; +import net.i2p.data.SDSCache; +import net.i2p.util.VersionComparator; + + /** * All the torrents */ public class Torrents extends ConcurrentHashMap { + private static final int CACHE_SIZE = 2048; + private final SDSCache _hashCache; + private final SDSCache _pidCache; + + public Torrents() { super(); + _hashCache = new SDSCache(InfoHash.class, InfoHash.LENGTH, CACHE_SIZE); + _pidCache = new SDSCache(PID.class, PID.LENGTH, CACHE_SIZE); } public int countPeers() { @@ -34,4 +47,52 @@ public class Torrents extends ConcurrentHashMap { } return rv; } + + /** + * Pull from cache or return new + * + * @throws IllegalArgumentException if data is not the correct number of bytes + * @since 0.12.0 + */ + public InfoHash createInfoHash(String data) throws IllegalArgumentException { + byte[] d = DataHelper.getASCII(data); + if (d.length != InfoHash.LENGTH) + throw new IllegalArgumentException("bad infohash length " + d.length); + return _hashCache.get(d); + } + + /** + * Pull from cache or return new + * + * @throws IllegalArgumentException if data is not the correct number of bytes + * @since 0.12.0 + */ + public PID createPID(String data) throws IllegalArgumentException { + byte[] d = DataHelper.getASCII(data); + if (d.length != PID.LENGTH) + throw new IllegalArgumentException("bad peer id length " + d.length); + return _pidCache.get(d); + } + + /** + * @since 0.12.0 + */ + @Override + public void clear() { + super.clear(); + clearCaches(); + } + + /** + * @since 0.12.0 + */ + private void clearCaches() { + // not available until 0.9.17 + if (VersionComparator.comp(CoreVersion.VERSION, "0.9.17") >= 0) { + try { + _hashCache.clear(); + _pidCache.clear(); + } catch (Throwable t) {} + } + } } diff --git a/src/java/net/i2p/zzzot/ZzzOT.java b/src/java/net/i2p/zzzot/ZzzOT.java index fc4dc54..98267df 100644 --- a/src/java/net/i2p/zzzot/ZzzOT.java +++ b/src/java/net/i2p/zzzot/ZzzOT.java @@ -18,33 +18,44 @@ package net.i2p.zzzot; import java.util.Iterator; -import net.i2p.util.SimpleScheduler; -import net.i2p.util.SimpleTimer; +import net.i2p.I2PAppContext; +import net.i2p.util.SimpleTimer2; /** * Instantiate this to fire it up */ class ZzzOT { - private Torrents _torrents; + private final Torrents _torrents; + private final Cleaner _cleaner; + private static final long CLEAN_TIME = 4*60*1000; private static final long EXPIRE_TIME = 60*60*1000; - ZzzOT() { + ZzzOT(I2PAppContext ctx) { _torrents = new Torrents(); - SimpleScheduler.getInstance().addPeriodicEvent(new Cleaner(), CLEAN_TIME); + _cleaner = new Cleaner(ctx); } Torrents getTorrents() { return _torrents; } - void stop() { - _torrents.clear(); - // no way to stop the cleaner + void start() { + _cleaner.forceReschedule(CLEAN_TIME); } - private class Cleaner implements SimpleTimer.TimedEvent { + void stop() { + _cleaner.cancel(); + _torrents.clear(); + } + + private class Cleaner extends SimpleTimer2.TimedEvent { + + /** must schedule later */ + public Cleaner(I2PAppContext ctx) { + super(ctx.simpleTimer2()); + } public void timeReached() { long now = System.currentTimeMillis(); @@ -61,6 +72,7 @@ class ZzzOT { if (recent <= 0) iter.remove(); } + schedule(CLEAN_TIME); } } } diff --git a/src/java/net/i2p/zzzot/ZzzOTController.java b/src/java/net/i2p/zzzot/ZzzOTController.java index 2812d17..c595ac9 100644 --- a/src/java/net/i2p/zzzot/ZzzOTController.java +++ b/src/java/net/i2p/zzzot/ZzzOTController.java @@ -22,18 +22,25 @@ import java.io.IOException; import java.util.List; import java.util.Properties; +import net.i2p.CoreVersion; import net.i2p.I2PAppContext; +import net.i2p.app.ClientApp; +import net.i2p.app.ClientAppManager; +import net.i2p.app.ClientAppState; +import static net.i2p.app.ClientAppState.*; +import net.i2p.apps.systray.UrlLauncher; import net.i2p.data.Base32; import net.i2p.data.DataHelper; import net.i2p.data.Destination; import net.i2p.data.PrivateKeyFile; +import net.i2p.i2ptunnel.TunnelController; import net.i2p.util.FileUtil; import net.i2p.util.I2PAppThread; import net.i2p.util.Log; -import net.i2p.i2ptunnel.TunnelController; -import net.i2p.apps.systray.UrlLauncher; +import net.i2p.util.VersionComparator; import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.xml.XmlConfiguration; /** @@ -47,39 +54,64 @@ import org.eclipse.jetty.xml.XmlConfiguration; * * @author zzz */ -public class ZzzOTController { - private static final Log _log = I2PAppContext.getGlobalContext().logManager().getLog(ZzzOTController.class); - private static Server _server; - private static TunnelController _tunnel; - private static ZzzOT _zzzot; - private static Object _lock = new Object(); +public class ZzzOTController implements ClientApp { + private final I2PAppContext _context; + private final Log _log; + private final String[] _args; + private final ClientAppManager _mgr; + private Server _server; + private TunnelController _tunnel; + private final ZzzOT _zzzot; + /** only for main() */ + private static volatile ZzzOTController _controller; + private ClientAppState _state = UNINITIALIZED; + + private static final String NAME = "ZzzOT"; private static final String BACKUP_SUFFIX = ".jetty6"; private static final String[] xmlFiles = { "jetty.xml", "contexts/base-context.xml", "contexts/cgi-context.xml", "etc/realm.properties", "etc/webdefault.xml" }; - public static void main(String args[]) { - if (args.length != 3 || (!"-d".equals(args[0]))) - throw new IllegalArgumentException("Usage: PluginController -d $PLUGIN [start|stop]"); - if ("start".equals(args[2])) - start(args); - else if ("stop".equals(args[2])) - stop(); - else - throw new IllegalArgumentException("Usage: PluginController -d $PLUGIN [start|stop]"); + /** + * @since 0.12.0 + */ + public ZzzOTController(I2PAppContext ctx, ClientAppManager mgr, String args[]) { + _context = ctx; + _log = ctx.logManager().getLog(ZzzOTController.class); + _mgr = mgr; + _args = args; + _zzzot = new ZzzOT(ctx); + _state = INITIALIZED; } + /** + * No longer supported, as we now need the ClientAppManager for the webapp to find us + */ + public synchronized static void main(String args[]) { + throw new UnsupportedOperationException("Must use ClientApp interface"); + } + + /** + * @return null if not running + */ public static Torrents getTorrents() { - synchronized(_lock) { - if (_zzzot == null) - _zzzot = new ZzzOT(); - } - return _zzzot.getTorrents(); + ClientAppManager mgr = I2PAppContext.getGlobalContext().clientAppManager(); + if (mgr == null) + return null; + ClientApp z = mgr.getRegisteredApp(NAME); + if (z == null) + return null; + ZzzOTController ctrlr = (ZzzOTController) z; + return ctrlr._zzzot.getTorrents(); } - private static void start(String args[]) { - File pluginDir = new File(args[1]); + /** + * @param args ignored + */ + private void start(String args[]) { + //File pluginDir = new File(args[1]); + File pluginDir = new File(_context.getAppDir(), "plugins/zzzot"); if (!pluginDir.exists()) throw new IllegalArgumentException("Plugin directory " + pluginDir.getAbsolutePath() + " does not exist"); @@ -105,11 +137,12 @@ public class ZzzOTController { } startJetty(pluginDir, dest); startI2PTunnel(pluginDir, dest); + _zzzot.start(); // SeedlessAnnouncer.announce(_tunnel); } - private static void startI2PTunnel(File pluginDir, Destination dest) { + private void startI2PTunnel(File pluginDir, Destination dest) { File i2ptunnelConfig = new File(pluginDir, "i2ptunnel.config"); Properties i2ptunnelProps = new Properties(); try { @@ -131,15 +164,15 @@ public class ZzzOTController { _tunnel = tun; } - private static void startJetty(File pluginDir, Destination dest) { + private void startJetty(File pluginDir, Destination dest) { if (_server != null) throw new IllegalArgumentException("Jetty already running!"); migrateJettyXML(pluginDir); - I2PAppContext context = I2PAppContext.getGlobalContext(); - File tmpdir = new File(context.getTempDir().getAbsolutePath(), "/zzzot-work"); + File tmpdir = new File(_context.getTempDir().getAbsolutePath(), "/zzzot-work"); tmpdir.mkdir(); File jettyXml = new File(pluginDir, "jetty.xml"); try { + Resource.setDefaultUseCaches(false); XmlConfiguration xmlc = new XmlConfiguration(jettyXml.toURI().toURL()); Server serv = (Server) xmlc.configure(); //HttpContext[] hcs = serv.getContexts(); @@ -155,14 +188,13 @@ public class ZzzOTController { launchHelp(pluginDir, dest); } - private static void stop() { + private void stop() { stopI2PTunnel(); stopJetty(); - if (_zzzot != null) - _zzzot.stop(); + _zzzot.stop(); } - private static void stopI2PTunnel() { + private void stopI2PTunnel() { if (_tunnel == null) return; try { @@ -174,7 +206,7 @@ public class ZzzOTController { _tunnel = null; } - private static void stopJetty() { + private void stopJetty() { if (_server == null) return; try { @@ -190,7 +222,7 @@ public class ZzzOTController { * Migate the jetty configuration files. * Save old jetty.xml if moving from jetty 5 to jetty 6 */ - private static void migrateJettyXML(File pluginDir) { + private void migrateJettyXML(File pluginDir) { // contexts dir does not exist in Jetty 5 File file = new File(pluginDir, "contexts"); file.mkdir(); @@ -220,7 +252,7 @@ public class ZzzOTController { * @return success * @since Jetty 7 */ - private static boolean backupAndMigrateFile(File toDir, String filename) { + private boolean backupAndMigrateFile(File toDir, String filename) { File to = new File(toDir, filename); boolean rv = backupFile(to); boolean rv2 = migrateJettyFile(toDir, filename); @@ -249,7 +281,7 @@ public class ZzzOTController { /** * Migate a single jetty config file, replacing $PLUGIN as we copy it. */ - private static boolean migrateJettyFile(File pluginDir, String name) { + private boolean migrateJettyFile(File pluginDir, String name) { File templateDir = new File(pluginDir, "templates"); File fileTmpl = new File(templateDir, name); File outFile = new File(pluginDir, name); @@ -271,7 +303,7 @@ public class ZzzOTController { } /** put the directory, base32, and base64 info in the help.html file and launch a browser window to display it */ - private static void launchHelp(File pluginDir, Destination dest) { + private void launchHelp(File pluginDir, Destination dest) { File fileTmpl = new File(pluginDir, "templates/help.html"); File outFile = new File(pluginDir, "eepsite/docroot/help.html"); String b32 = Base32.encode(dest.calculateHash().getData()) + ".b32.i2p"; @@ -298,4 +330,82 @@ public class ZzzOTController { UrlLauncher.main(new String[] { "http://127.0.0.1:7662/help.html" } ); } } + + /////// ClientApp methods + + /** @since 0.12.0 */ + public synchronized void startup() { + if (_mgr != null) { + // this is really ugly, but thru 0.9.16, + // stopping a ClientApp plugin with $PLUGIN in the args fails, + // and it tries to start a second one instead. + // Find the first one and stop it. + ClientApp z = _mgr.getRegisteredApp(NAME); + if (z != null) { + if (VersionComparator.comp(CoreVersion.VERSION, "0.9.17") < 0) { + ZzzOTController ctrlr = (ZzzOTController) z; + ctrlr.shutdown(null); + } else { + _log.error("ZzzOT already running"); + } + changeState(START_FAILED); + return; + } + } + if (_state != STOPPED && _state != INITIALIZED && _state != START_FAILED) { + _log.error("Start while state = " + _state); + return; + } + changeState(STARTING); + try { + start(_args); + changeState(RUNNING); + if (_mgr != null) + _mgr.register(this); + } catch (Exception e) { + changeState(START_FAILED, "Start failed", e); + } + } + + /** @since 0.12.0 */ + public synchronized void shutdown(String[] args) { + if (_state == STOPPED) + return; + changeState(STOPPING); + if (_mgr != null) + _mgr.unregister(this); + stop(); + changeState(STOPPED); + } + + /** @since 0.12.0 */ + public ClientAppState getState() { + return _state; + } + + /** @since 0.12.0 */ + public String getName() { + return NAME; + } + + /** @since 0.12.0 */ + public String getDisplayName() { + return NAME; + } + + /////// end ClientApp methods + + /** @since 0.12.0 */ + private synchronized void changeState(ClientAppState state) { + _state = state; + if (_mgr != null) + _mgr.notify(this, state, null, null); + } + + /** @since 0.12.0 */ + private synchronized void changeState(ClientAppState state, String msg, Exception e) { + _state = state; + if (_mgr != null) + _mgr.notify(this, state, msg, e); + } } diff --git a/src/jsp/announce.jsp b/src/jsp/announce.jsp index cd4a39d..24a75a6 100644 --- a/src/jsp/announce.jsp +++ b/src/jsp/announce.jsp @@ -73,6 +73,11 @@ msg = "no info hash"; } + if (info_hash.length() != 20 && !fail) { + fail = true; + msg = "bad info hash length " + info_hash.length(); + } + if (ip == null && !fail) { fail = true; msg = "no ip (dest)"; @@ -83,11 +88,22 @@ msg = "no peer id"; } + if (peer_id.length() != 20 && !fail) { + fail = true; + msg = "bad peer id length " + peer_id.length(); + } + + Torrents torrents = ZzzOTController.getTorrents(); + if (torrents == null && !fail) { + fail = true; + msg = "tracker is down"; + } + InfoHash ih = null; if (!fail) { try { - ih = new InfoHash(info_hash); - } catch (Exception e) { + ih = torrents.createInfoHash(info_hash); + } catch (IllegalArgumentException e) { fail = true; msg = "bad infohash " + e; } @@ -111,8 +127,8 @@ PID pid = null; if (!fail) { try { - pid = new PID(peer_id); - } catch (Exception e) { + pid = torrents.createPID(peer_id); + } catch (IllegalArgumentException e) { fail = true; msg = "bad peer id " + e; } @@ -162,8 +178,7 @@ } catch (NumberFormatException nfe) {}; } - Torrents torrents = ZzzOTController.getTorrents(); - Map m = new HashMap(); + Map m = new HashMap(8); if (fail) { m.put("failure reason", msg); } else if ("stopped".equals(event)) { diff --git a/src/jsp/index.jsp b/src/jsp/index.jsp index 73e0be0..fcc04a6 100644 --- a/src/jsp/index.jsp +++ b/src/jsp/index.jsp @@ -1,4 +1,4 @@ -<%@page import="net.i2p.zzzot.ZzzOTController" %> +<%@page import="net.i2p.zzzot.ZzzOTController,net.i2p.zzzot.Torrents" %> ZzzOT @@ -6,9 +6,20 @@

zzzot

+<% + Torrents torrents = ZzzOTController.getTorrents(); + if (torrents != null) { +%> -
Torrents:<%=ZzzOTController.getTorrents().size()%> -
Peers:<%=ZzzOTController.getTorrents().countPeers()%> +
Torrents:<%=torrents.size()%> +
Peers:<%=torrents.countPeers()%>
+<% + } else { +%> +ZzzOT is not running +<% + } +%> diff --git a/src/jsp/scrape.jsp b/src/jsp/scrape.jsp index d2872aa..5efd193 100644 --- a/src/jsp/scrape.jsp +++ b/src/jsp/scrape.jsp @@ -27,6 +27,10 @@ */ // so the chars will turn into bytes correctly request.setCharacterEncoding("ISO-8859-1"); + // above doesn't work for the query string + // https://wiki.eclipse.org/Jetty/Howto/International_Characters + // we could also do ((org.eclipse.jetty.server.Request) request).setQueryEncoding("ISO-8859-1") + request.setAttribute("org.eclipse.jetty.server.Request.queryEncoding", "ISO-8859-1"); java.io.OutputStream cout = response.getOutputStream(); response.setCharacterEncoding("ISO-8859-1"); response.setContentType("text/plain"); @@ -46,20 +50,24 @@ boolean all = info_hash == null; + Torrents torrents = ZzzOTController.getTorrents(); + if (torrents == null && !fail) { + fail = true; + msg = "tracker is down"; + } + InfoHash ih = null; if ((!all) && !fail) { try { - ih = new InfoHash(info_hash); + ih = torrents.createInfoHash(info_hash); } catch (Exception e) { fail = true; msg = "bad infohash " + e; } } - Torrents torrents = ZzzOTController.getTorrents(); - // build 3-level dictionary - Map m = new HashMap(); + Map m = new HashMap(4); if (fail) { m.put("failure reason", msg); } else { diff --git a/src/jsp/seedless.jsp b/src/jsp/seedless.jsp index ca0ce85..88b2b24 100644 --- a/src/jsp/seedless.jsp +++ b/src/jsp/seedless.jsp @@ -62,6 +62,10 @@ } else if (req.startsWith("locate dG9ycmVud")) { // locate b64(torrent) // all the peers Torrents torrents = ZzzOTController.getTorrents(); + if (torrents == null) { + response.setStatus(503, "Down"); + return; + } for (InfoHash ihash : torrents.keySet()) { Peers peers = torrents.get(ihash); if (peers == null)