diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateChecker.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateChecker.java
new file mode 100644
index 0000000000000000000000000000000000000000..e19e23d91f1b9cfc3f068a384f2eebedb2af1cb8
--- /dev/null
+++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateChecker.java
@@ -0,0 +1,139 @@
+package net.i2p.router.web;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.util.Properties;
+
+import net.i2p.I2PAppContext;
+import net.i2p.crypto.TrustedUpdate;
+import net.i2p.data.DataHelper;
+import net.i2p.router.RouterContext;
+import net.i2p.util.EepGet;
+import net.i2p.util.I2PAppThread;
+import net.i2p.util.Log;
+import net.i2p.util.PartialEepGet;
+import net.i2p.util.VersionComparator;
+
+/**
+ * Download and install a plugin.
+ * A plugin is a standard .sud file with a 40-byte signature,
+ * a 16-byte version, and a .zip file.
+ * Unlike for router updates, we need not have the public key
+ * for the signature in advance.
+ *
+ * The zip file must have a standard directory layout, with
+ * a plugin.config file at the top level.
+ * The config file contains properties for the package name, version,
+ * signing public key, and other settings.
+ * The zip file will typically contain a webapps/ or lib/ dir,
+ * and a webapps.config and/or clients.config file.
+ *
+ * @since 0.7.12
+ * @author zzz
+ */
+public class PluginUpdateChecker extends UpdateHandler {
+    private static PluginUpdateCheckerRunner _pluginUpdateCheckerRunner;
+    private String _appName;
+    private String _oldVersion;
+    private String _xpi2pURL;
+
+    private static PluginUpdateChecker _instance;
+    public static final synchronized PluginUpdateChecker getInstance(RouterContext ctx) { 
+        if (_instance != null)
+            return _instance;
+        _instance = new PluginUpdateChecker(ctx);
+        return _instance;
+    }
+
+    private PluginUpdateChecker(RouterContext ctx) {
+        super(ctx);
+    }
+    
+    public void update(String appName) {
+        // don't block waiting for the other one to finish
+        if ("true".equals(System.getProperty(PROP_UPDATE_IN_PROGRESS))) {
+            _log.error("Update already running");
+            return;
+        }
+        synchronized (UpdateHandler.class) {
+            Properties props = PluginStarter.pluginProperties(_context, appName);
+            String oldVersion = props.getProperty("version");
+            String xpi2pURL = props.getProperty("updateURL");
+            if (oldVersion == null || xpi2pURL == null) {
+                updateStatus("<b>" + _("Cannot update, plugin {0} is not installed", appName) + "</b>");
+                return;
+            }
+
+            if (_pluginUpdateCheckerRunner == null)
+                _pluginUpdateCheckerRunner = new PluginUpdateCheckerRunner(xpi2pURL);
+            if (_pluginUpdateCheckerRunner.isRunning())
+                return;
+            _appName = appName;
+            _oldVersion = oldVersion;
+            System.setProperty(PROP_UPDATE_IN_PROGRESS, "true");
+            I2PAppThread update = new I2PAppThread(_pluginUpdateCheckerRunner, "AppChecker");
+            update.start();
+        }
+    }
+    
+    public boolean isRunning() {
+        return _pluginUpdateCheckerRunner != null && _pluginUpdateCheckerRunner.isRunning();
+    }
+    
+    @Override
+    public boolean isDone() {
+        // FIXME
+        return false;
+    }
+    
+    public class PluginUpdateCheckerRunner extends UpdateRunner implements Runnable, EepGet.StatusListener {
+        String _updateURL;
+        ByteArrayOutputStream _baos;
+
+        public PluginUpdateCheckerRunner(String url) { 
+            super();
+            _updateURL = url;
+            _baos = new ByteArrayOutputStream(TrustedUpdate.HEADER_BYTES);
+        }
+
+        @Override
+        protected void update() {
+            updateStatus("<b>" + _("Checking plugin {0} for updates", _appName) + "</b>");
+            // use the same settings as for updater
+            boolean shouldProxy = Boolean.valueOf(_context.getProperty(ConfigUpdateHandler.PROP_SHOULD_PROXY, ConfigUpdateHandler.DEFAULT_SHOULD_PROXY)).booleanValue();
+            String proxyHost = _context.getProperty(ConfigUpdateHandler.PROP_PROXY_HOST, ConfigUpdateHandler.DEFAULT_PROXY_HOST);
+            int proxyPort = _context.getProperty(ConfigUpdateHandler.PROP_PROXY_PORT, ConfigUpdateHandler.DEFAULT_PROXY_PORT_INT);
+            try {
+                _get = new PartialEepGet(_context, proxyHost, proxyPort, _baos, _xpi2pURL, TrustedUpdate.HEADER_BYTES);
+                _get.addStatusListener(PluginUpdateCheckerRunner.this);
+                _get.fetch();
+            } catch (Throwable t) {
+                _log.error("Error checking update for plugin", t);
+            }
+        }
+        
+        @Override
+        public void bytesTransferred(long alreadyTransferred, int currentWrite, long bytesTransferred, long bytesRemaining, String url) {
+        }
+
+        @Override
+        public void transferComplete(long alreadyTransferred, long bytesTransferred, long bytesRemaining, String url, String outputFile, boolean notModified) {
+            String newVersion = TrustedUpdate.getVersionString(new ByteArrayInputStream(_baos.toByteArray()));
+            boolean newer = (new VersionComparator()).compare(newVersion, _oldVersion) > 0;
+            if (newer)
+                updateStatus("<b>" + _("New plugin version {0} is available", newVersion) + "</b>");
+            else
+                updateStatus("<b>" + _("No new version is available for plugin {0}", _appName) + "</b>");
+        }
+
+        @Override
+        public void transferFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt) {
+            File f = new File(_updateFile);
+            f.delete();
+            updateStatus("<b>" + _("Update check failed for plugin {0}", _appName) + "</b>");
+        }
+    }
+}
+    
diff --git a/core/java/src/net/i2p/crypto/TrustedUpdate.java b/core/java/src/net/i2p/crypto/TrustedUpdate.java
index 133a18033365d45ed8aa177ad22a0ee5745ef7a7..c567fcb3100445b0acb4cefd0bdbe05e2fc2aae2 100644
--- a/core/java/src/net/i2p/crypto/TrustedUpdate.java
+++ b/core/java/src/net/i2p/crypto/TrustedUpdate.java
@@ -4,6 +4,7 @@ import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
+import java.io.InputStream;
 import java.io.IOException;
 import java.io.SequenceInputStream;
 import java.io.UnsupportedEncodingException;
@@ -104,7 +105,7 @@ D8usM7Dxp5yrDrCYZ5AIijc=
 */
 
     private static final int    VERSION_BYTES       = 16;
-    private static final int    HEADER_BYTES        = Signature.SIGNATURE_BYTES + VERSION_BYTES;
+    public static final int    HEADER_BYTES        = Signature.SIGNATURE_BYTES + VERSION_BYTES;
     private static final String PROP_TRUSTED_KEYS   = "router.trustedUpdateKeys";
 
     private static I2PAppContext _context;
@@ -274,7 +275,7 @@ D8usM7Dxp5yrDrCYZ5AIijc=
     }
 
     private static final void showVersionCLI(String signedFile) {
-        String versionString = new TrustedUpdate().getVersionString(new File(signedFile));
+        String versionString = getVersionString(new File(signedFile));
 
         if (versionString.equals(""))
             System.out.println("No version string found in file '" + signedFile + "'");
@@ -347,7 +348,7 @@ D8usM7Dxp5yrDrCYZ5AIijc=
      * @return The version string read, or an empty string if no version string
      *         is present.
      */
-    public String getVersionString(File signedFile) {
+    public static String getVersionString(File signedFile) {
         FileInputStream fileInputStream = null;
 
         try {
@@ -380,6 +381,45 @@ D8usM7Dxp5yrDrCYZ5AIijc=
                 }
         }
     }
+    
+    /**
+     * Reads the version string from an input stream
+     * 
+     * @param inputStream containing at least 56 bytes
+     * 
+     * @return The version string read, or an empty string if no version string
+     *         is present.
+     */
+    public static String getVersionString(InputStream inputStream) {
+        try {
+            long skipped = inputStream.skip(Signature.SIGNATURE_BYTES);
+            if (skipped != Signature.SIGNATURE_BYTES)
+                return "";
+            byte[] data = new byte[VERSION_BYTES];
+            int bytesRead = DataHelper.read(inputStream, data);
+
+            if (bytesRead != VERSION_BYTES) {
+                return "";
+            }
+
+            for (int i = 0; i < VERSION_BYTES; i++) 
+                if (data[i] == 0x00) {
+                    return new String(data, 0, i, "UTF-8");
+                }
+
+            return new String(data, "UTF-8");
+        } catch (UnsupportedEncodingException uee) {
+            throw new RuntimeException("wtf, your JVM doesnt support utf-8? " + uee.getMessage());
+        } catch (IOException ioe) {
+            return "";
+        } finally {
+            if (inputStream != null)
+                try {
+                    inputStream.close();
+                } catch (IOException ioe) {
+                }
+        }
+    }
 
     /** version in the .sud file, valid only after calling migrateVerified() */
     public String newVersion() {
diff --git a/core/java/src/net/i2p/util/PartialEepGet.java b/core/java/src/net/i2p/util/PartialEepGet.java
new file mode 100644
index 0000000000000000000000000000000000000000..5b18bc7619ffbdc4ebe2e749243d5f9186735e04
--- /dev/null
+++ b/core/java/src/net/i2p/util/PartialEepGet.java
@@ -0,0 +1,130 @@
+package net.i2p.util;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URL;
+
+import net.i2p.I2PAppContext;
+
+/**
+ * Fetch exactly the first 'size' bytes into a stream
+ * Anything less or more will throw an IOException
+ * No retries, no min and max size options, no timeout option
+ * Useful for checking .sud versions
+ *
+ * @since 0.7.12
+ * @author zzz
+ */
+public class PartialEepGet extends EepGet {
+    long _fetchSize;
+
+    /** @param size fetch exactly this many bytes */
+    public PartialEepGet(I2PAppContext ctx, String proxyHost, int proxyPort,
+                         OutputStream outputStream,  String url, long size) {
+        // we're using this constructor:
+        // public EepGet(I2PAppContext ctx, boolean shouldProxy, String proxyHost, int proxyPort, int numRetries, long minSize, long maxSize, String outputFile, OutputStream outputStream, String url, boolean allowCaching, String etag, String postData) {
+        super(ctx, true, proxyHost, proxyPort, 0, size, size, null, outputStream, url, true, null, null);
+        _fetchSize = size;
+    }
+   
+    /**
+     * PartialEepGet [-p 127.0.0.1:4444] [-l #bytes] url
+     *
+     */ 
+    public static void main(String args[]) {
+        String proxyHost = "127.0.0.1";
+        int proxyPort = 4444;
+        // 40 sig + 16 version for .suds
+        long size = 56;
+        String url = null;
+        try {
+            for (int i = 0; i < args.length; i++) {
+                if (args[i].equals("-p")) {
+                    proxyHost = args[i+1].substring(0, args[i+1].indexOf(':'));
+                    String port = args[i+1].substring(args[i+1].indexOf(':')+1);
+                    proxyPort = Integer.parseInt(port);
+                    i++;
+                } else if (args[i].equals("-l")) {
+                    size = Long.parseLong(args[i+1]);
+                    i++;
+                } else if (args[i].startsWith("-")) {
+                    usage();
+                    return;
+                } else {
+                    url = args[i];
+                }
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            usage();
+            return;
+        }
+        
+        if (url == null) {
+            usage();
+            return;
+        }
+
+        String saveAs = suggestName(url);
+        OutputStream out;
+        try {
+            // resume from a previous eepget won't work right doing it this way
+            out = new FileOutputStream(saveAs);
+        } catch (IOException ioe) {
+            System.err.println("Failed to create output file " + saveAs);
+            return;
+        }
+
+        EepGet get = new PartialEepGet(I2PAppContext.getGlobalContext(), proxyHost, proxyPort, out, url, size);
+        get.addStatusListener(get.new CLIStatusListener(1024, 40));
+        if (get.fetch(45*1000, -1, 60*1000)) {
+            System.err.println("Last-Modified: " + get.getLastModified());
+            System.err.println("Etag: " + get.getETag());
+        } else {
+            System.err.println("Failed " + url);
+        }
+    }
+    
+    private static void usage() {
+        System.err.println("PartialEepGet [-p 127.0.0.1:4444] [-l #bytes] url");
+    }
+    
+    @Override
+    protected String getRequest() throws IOException {
+        StringBuilder buf = new StringBuilder(2048);
+        URL url = new URL(_actualURL);
+        String proto = url.getProtocol();
+        String host = url.getHost();
+        int port = url.getPort();
+        String path = url.getPath();
+        String query = url.getQuery();
+        if (query != null)
+            path = path + '?' + query;
+        if (!path.startsWith("/"))
+	    path = "/" + path;
+        if ( (port == 80) || (port == 443) || (port <= 0) ) path = proto + "://" + host + path;
+        else path = proto + "://" + host + ":" + port + path;
+        if (_log.shouldLog(Log.DEBUG)) _log.debug("Requesting " + path);
+        buf.append("GET ").append(_actualURL).append(" HTTP/1.1\r\n");
+        buf.append("Host: ").append(url.getHost()).append("\r\n");
+        buf.append("Range: bytes=");
+        buf.append(_alreadyTransferred);
+        buf.append('-');
+        buf.append(_fetchSize - 1);
+        buf.append("\r\n");
+
+        if (_shouldProxy)
+            buf.append("X-Accept-Encoding: x-i2p-gzip;q=1.0, identity;q=0.5, deflate;q=0, gzip;q=0, *;q=0\r\n");
+        buf.append("Cache-control: no-cache\r\n" +
+                   "Pragma: no-cache\r\n");
+        // This will be replaced if we are going through I2PTunnelHTTPClient
+        buf.append("User-Agent: " + USER_AGENT + "\r\n" +
+                   "Accept-Encoding: \r\n" +
+                   "Connection: close\r\n\r\n");
+
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Request: [" + buf.toString() + "]");
+        return buf.toString();
+    }
+}