From 9b141bd9dd899ecd6b6c727ee22eac23a537c547 Mon Sep 17 00:00:00 2001
From: zzz
Date: Sat, 6 Feb 2010 18:40:55 +0000
Subject: [PATCH 01/25] * Webapps: Allow additions to a webapp classpath.
This will let us: - Pull jstl.jar and standard.jar out of susidns.war
- Remove 100KB of duplicate classes from i2psnark.war - Add
classpaths for plugins
---
.../i2p/router/web/RouterConsoleRunner.java | 18 ++--
.../i2p/router/web/WebAppConfiguration.java | 91 +++++++++++++++++++
.../src/net/i2p/router/web/WebAppStarter.java | 60 ++++++++++++
3 files changed, 162 insertions(+), 7 deletions(-)
create mode 100644 apps/routerconsole/java/src/net/i2p/router/web/WebAppConfiguration.java
create mode 100644 apps/routerconsole/java/src/net/i2p/router/web/WebAppStarter.java
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java b/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java
index bcd1f3cbd8..31d854aeae 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java
@@ -69,6 +69,8 @@ public class RouterConsoleRunner {
if (!workDirCreated)
System.err.println("ERROR: Unable to create Jetty temporary work directory");
+ // so Jetty can find WebAppConfiguration
+ System.setProperty("jetty.class.path", I2PAppContext.getGlobalContext().getBaseDir() + "/lib/routerconsole.jar");
_server = new Server();
boolean rewrite = false;
Properties props = webAppProperties();
@@ -127,11 +129,9 @@ public class RouterConsoleRunner {
String enabled = props.getProperty(PREFIX + appName + ENABLED);
if (! "false".equals(enabled)) {
String path = new File(dir, fileNames[i]).getCanonicalPath();
- wac = _server.addWebApplication("/"+ appName, path);
tmpdir = new File(workDir, appName + "-" + _listenPort);
- tmpdir.mkdir();
- wac.setTempDirectory(tmpdir);
- initialize(wac);
+ WebAppStarter.addWebApp(I2PAppContext.getGlobalContext(), _server, appName, path, tmpdir);
+
if (enabled == null) {
// do this so configclients.jsp knows about all apps from reading the config
props.setProperty(PREFIX + appName + ENABLED, "true");
@@ -190,7 +190,7 @@ public class RouterConsoleRunner {
st.start();
}
- private void initialize(WebApplicationContext context) {
+ static void initialize(WebApplicationContext context) {
String password = getPassword();
if (password != null) {
HashUserRealm realm = new HashUserRealm("i2prouter");
@@ -205,7 +205,7 @@ public class RouterConsoleRunner {
}
}
- private String getPassword() {
+ static String getPassword() {
List contexts = RouterContext.listContexts();
if (contexts != null) {
for (int i = 0; i < contexts.size(); i++) {
@@ -237,10 +237,14 @@ public class RouterConsoleRunner {
********/
public static Properties webAppProperties() {
+ return webAppProperties(I2PAppContext.getGlobalContext().getConfigDir().getAbsolutePath());
+ }
+
+ public static Properties webAppProperties(String dir) {
Properties rv = new Properties();
// String webappConfigFile = ctx.getProperty(PROP_WEBAPP_CONFIG_FILENAME, DEFAULT_WEBAPP_CONFIG_FILENAME);
String webappConfigFile = DEFAULT_WEBAPP_CONFIG_FILENAME;
- File cfgFile = new File(I2PAppContext.getGlobalContext().getConfigDir(), webappConfigFile);
+ File cfgFile = new File(dir, webappConfigFile);
try {
DataHelper.loadProps(rv, cfgFile);
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/WebAppConfiguration.java b/apps/routerconsole/java/src/net/i2p/router/web/WebAppConfiguration.java
new file mode 100644
index 0000000000..d903d83caf
--- /dev/null
+++ b/apps/routerconsole/java/src/net/i2p/router/web/WebAppConfiguration.java
@@ -0,0 +1,91 @@
+package net.i2p.router.web;
+
+import java.io.File;
+import java.util.Properties;
+import java.util.StringTokenizer;
+
+import net.i2p.I2PAppContext;
+
+import org.mortbay.jetty.servlet.WebApplicationContext;
+
+
+/**
+ * Add to the webapp classpath as specified in webapps.config.
+ * This allows us to reference classes that are not in the classpath
+ * specified in wrapper.config, since old installations have
+ * individual jars and not lib/*.jar specified in wrapper.config.
+ *
+ * A sample line in webapps.config is:
+ * webapps.appname.path=foo.jar,$I2P/lib/bar.jar
+ * Unless $I2P is specified the path will be relative to $I2P/lib for
+ * webapps in the installation and appDir/plugins/appname/lib for plugins.
+ *
+ * Sadly, setting Class-Path in MANIFEST.MF doesn't work for jetty wars.
+ * We could look there ourselves, or look for another properties file in the war,
+ * but let's just do it in webapps.config.
+ *
+ * No, wac.addClassPath() does not work. For more info see:
+ *
+ * http://servlets.com/archive/servlet/ReadMsg?msgId=511113&listName=jetty-support
+ *
+ * @since 0.7.12
+ * @author zzz
+ */
+public class WebAppConfiguration implements WebApplicationContext.Configuration {
+ private WebApplicationContext _wac;
+
+ private static final String CLASSPATH = ".classpath";
+
+ public void setWebApplicationContext(WebApplicationContext context) {
+ _wac = context;
+ }
+
+ public WebApplicationContext getWebApplicationContext() {
+ return _wac;
+ }
+
+ public void configureClassPath() throws Exception {
+ String ctxPath = _wac.getContextPath();
+ //System.err.println("Configure Class Path " + ctxPath);
+ if (ctxPath.equals("/"))
+ return;
+ String appName = ctxPath.substring(1);
+
+ I2PAppContext i2pContext = I2PAppContext.getGlobalContext();
+ File libDir = new File(i2pContext.getBaseDir(), "lib");
+ File pluginLibDir = new File(i2pContext.getAppDir(),
+ PluginUpdateHandler.PLUGIN_DIR + ctxPath + '/' + "lib");
+ File dir = libDir;
+ String cp;
+ if (ctxPath.equals("/susidns")) {
+ // jars moved from the .war to lib/ in 0.7.12
+ cp = "jstl.jar,standard.jar";
+ } else if (ctxPath.equals("/i2psnark")) {
+ // duplicate classes removed from the .war in 0.7.12
+ cp = "i2psnark.jar";
+ } else if (pluginLibDir.exists()) {
+ Properties props = RouterConsoleRunner.webAppProperties(pluginLibDir.getAbsolutePath());
+ cp = props.getProperty(RouterConsoleRunner.PREFIX + appName + CLASSPATH);
+ dir = pluginLibDir;
+ } else {
+ Properties props = RouterConsoleRunner.webAppProperties();
+ cp = props.getProperty(RouterConsoleRunner.PREFIX + appName + CLASSPATH);
+ }
+ if (cp == null)
+ return;
+ StringTokenizer tok = new StringTokenizer(cp, " ,");
+ while (tok.hasMoreTokens()) {
+ String elem = tok.nextToken().trim();
+ String path;
+ if (elem.startsWith("$I2P"))
+ path = i2pContext.getBaseDir().getAbsolutePath() + '/' + elem.substring(4);
+ else
+ path = dir.getAbsolutePath() + '/' + elem;
+ System.err.println("Adding " + path + " to classpath for " + appName);
+ _wac.addClassPath(path);
+ }
+ }
+
+ public void configureDefaults() {}
+ public void configureWebApp() {}
+}
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/WebAppStarter.java b/apps/routerconsole/java/src/net/i2p/router/web/WebAppStarter.java
new file mode 100644
index 0000000000..dafc1c10a5
--- /dev/null
+++ b/apps/routerconsole/java/src/net/i2p/router/web/WebAppStarter.java
@@ -0,0 +1,60 @@
+package net.i2p.router.web;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Properties;
+import java.util.StringTokenizer;
+
+import net.i2p.I2PAppContext;
+
+import org.mortbay.jetty.Server;
+import org.mortbay.jetty.servlet.WebApplicationContext;
+
+
+/**
+ * Start a webappapp classpath as specified in webapps.config.
+ *
+ * Sadly, setting Class-Path in MANIFEST.MF doesn't work for jetty wars.
+ * We could look there ourselves, or look for another properties file in the war,
+ * but let's just do it in webapps.config.
+ *
+ * No, wac.addClassPath() does not work.
+ *
+ * http://servlets.com/archive/servlet/ReadMsg?msgId=511113&listName=jetty-support
+ *
+ * @since 0.7.12
+ * @author zzz
+ */
+public class WebAppStarter {
+
+ /**
+ * adds and starts
+ */
+ static void startWebApp(I2PAppContext ctx, Server server, String appName, String warPath) throws Exception {
+ File tmpdir = new File(ctx.getTempDir(), "jetty-work-" + appName + ctx.random().nextInt());
+ WebApplicationContext wac = addWebApp(ctx, server, appName, warPath, tmpdir);
+ wac.start();
+ }
+
+ /**
+ * add but don't start
+ */
+ static WebApplicationContext addWebApp(I2PAppContext ctx, Server server, String appName, String warPath, File tmpdir) throws IOException {
+
+ WebApplicationContext wac = server.addWebApplication("/"+ appName, warPath);
+ tmpdir.mkdir();
+ wac.setTempDirectory(tmpdir);
+
+ // this does the passwords...
+ RouterConsoleRunner.initialize(wac);
+
+ // see WebAppConfiguration for info
+ String[] classNames = server.getWebApplicationConfigurationClassNames();
+ String[] newClassNames = new String[classNames.length + 1];
+ for (int j = 0; j < classNames.length; j++)
+ newClassNames[j] = classNames[j];
+ newClassNames[classNames.length] = WebAppConfiguration.class.getName();
+ wac.setConfigurationClassNames(newClassNames);
+ return wac;
+ }
+}
From 7a59d15e9c2da1b6c7d7a3210bc0d1332a881110 Mon Sep 17 00:00:00 2001
From: zzz
Date: Sat, 6 Feb 2010 18:44:29 +0000
Subject: [PATCH 02/25] - Pull jstl.jar and standard.jar out of
susidns.war (-300KB someday) - Remove duplicate classes from
i2psnark.war (100KB)
---
apps/i2psnark/java/build.xml | 3 ++-
apps/susidns/src/build.xml | 2 ++
build.xml | 5 +++++
3 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/apps/i2psnark/java/build.xml b/apps/i2psnark/java/build.xml
index 8323d57f65..4d51cb7426 100644
--- a/apps/i2psnark/java/build.xml
+++ b/apps/i2psnark/java/build.xml
@@ -53,7 +53,8 @@
-->
-
+
+
diff --git a/apps/susidns/src/build.xml b/apps/susidns/src/build.xml
index b0f7e96cf3..55b662a9d3 100644
--- a/apps/susidns/src/build.xml
+++ b/apps/susidns/src/build.xml
@@ -66,7 +66,9 @@
+
diff --git a/build.xml b/build.xml
index d9f833f9e0..c6e6537d13 100644
--- a/build.xml
+++ b/build.xml
@@ -325,6 +325,8 @@
+
+
@@ -463,6 +465,9 @@
+
+
+
From f7780b6745a66807a6611fa5ed4f41b353298fb1 Mon Sep 17 00:00:00 2001
From: zzz
Date: Sat, 6 Feb 2010 18:47:08 +0000
Subject: [PATCH 03/25] * TrustedUpdate: - Allow method to check if
we know about a key - Add method to extract without verifying
---
.../src/net/i2p/crypto/TrustedUpdate.java | 32 +++++++++++++++++++
1 file changed, 32 insertions(+)
diff --git a/core/java/src/net/i2p/crypto/TrustedUpdate.java b/core/java/src/net/i2p/crypto/TrustedUpdate.java
index 3d5aa0c1e1..133a180333 100644
--- a/core/java/src/net/i2p/crypto/TrustedUpdate.java
+++ b/core/java/src/net/i2p/crypto/TrustedUpdate.java
@@ -178,6 +178,22 @@ D8usM7Dxp5yrDrCYZ5AIijc=
return true;
}
+ /**
+ * Do we know about the following key?
+ * @since 0.7.12
+ */
+ public boolean haveKey(String key) {
+ if (key.length() != KEYSIZE_B64_BYTES)
+ return false;
+ SigningPublicKey signingPublicKey = new SigningPublicKey();
+ try {
+ signingPublicKey.fromBase64(key);
+ } catch (DataFormatException dfe) {
+ return false;
+ }
+ return _trustedKeys.containsKey(signingPublicKey);
+ }
+
/**
* Parses command line arguments when this class is used from the command
* line.
@@ -410,6 +426,22 @@ D8usM7Dxp5yrDrCYZ5AIijc=
if (!verify(signedFile))
return "Unknown signing key or corrupt file";
+ return migrateFile(signedFile, outputFile);
+ }
+
+ /**
+ * Extract the file. Skips and ignores the signature and version. No verification.
+ *
+ * @param signedFile A signed update file.
+ * @param outputFile The file to write the verified data to.
+ *
+ * @return null
if the
+ * data was moved, and an error String
otherwise.
+ */
+ public String migrateFile(File signedFile, File outputFile) {
+ if (!signedFile.exists())
+ return "File not found: " + signedFile.getAbsolutePath();
+
FileInputStream fileInputStream = null;
FileOutputStream fileOutputStream = null;
From 505d5f5cae9376d64af98c03d3fb32b090383020 Mon Sep 17 00:00:00 2001
From: zzz
Date: Sat, 6 Feb 2010 20:25:13 +0000
Subject: [PATCH 04/25] * Plugins: New plugin downloader/installer *
configclients.jsp: Use new WebAppStarter so webapps that are started
later also get the temp dir, password, and classpath configuration just
like if they were started at the beginning * configupdate.jsp: Delay
after checking for update so the summary bar will have buttons.
---
.../i2p/router/web/ConfigClientsHandler.java | 30 +-
.../i2p/router/web/ConfigUpdateHandler.java | 4 +
.../i2p/router/web/PluginUpdateHandler.java | 331 ++++++++++++++++++
.../i2p/router/web/SummaryBarRenderer.java | 2 +-
.../src/net/i2p/router/web/UpdateHandler.java | 4 +-
apps/routerconsole/jsp/configclients.jsp | 6 +
6 files changed, 372 insertions(+), 5 deletions(-)
create mode 100644 apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java
index a1204f3933..cf81ee0227 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java
@@ -37,6 +37,10 @@ public class ConfigClientsHandler extends FormHandler {
saveWebAppChanges();
return;
}
+ if (_action.equals(_("Install Plugin"))) {
+ installPlugin();
+ return;
+ }
// value
if (_action.startsWith("Start ")) {
String app = _action.substring(6);
@@ -189,8 +193,7 @@ public class ConfigClientsHandler extends FormHandler {
try {
File path = new File(_context.getBaseDir(), "webapps");
path = new File(path, app + ".war");
- s.addWebApplication("/"+ app, path.getAbsolutePath()).start();
- // no passwords... initialize(wac);
+ WebAppStarter.startWebApp(_context, s, app, path.getAbsolutePath());
addFormNotice(_("WebApp") + " " + _(app) + " " + _("started") + '.');
} catch (Exception ioe) {
addFormError(_("Failed to start") + ' ' + _(app) + " " + ioe + '.');
@@ -201,4 +204,27 @@ public class ConfigClientsHandler extends FormHandler {
}
addFormError(_("Failed to find server."));
}
+
+ private void installPlugin() {
+ String url = getString("pluginURL");
+ if (url == null || url.length() <= 0) {
+ addFormError(_("No plugin URL specified."));
+ return;
+ }
+ if ("true".equals(System.getProperty(UpdateHandler.PROP_UPDATE_IN_PROGRESS))) {
+ addFormError(_("Plugin or update download already in progress."));
+ return;
+ }
+ PluginUpdateHandler puh = PluginUpdateHandler.getInstance(_context);
+ if (puh.isRunning()) {
+ addFormError(_("Plugin or update download already in progress."));
+ return;
+ }
+ puh.update(url);
+ addFormNotice(_("Downloading plugin from {0}", url));
+ // So that update() will post a status to the summary bar before we reload
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException ie) {}
+ }
}
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHandler.java
index 62d60358d9..d9f23d8c9a 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHandler.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHandler.java
@@ -65,6 +65,10 @@ public class ConfigUpdateHandler extends FormHandler {
addFormNotice(_("Update available, attempting to download now"));
else
addFormNotice(_("Update available, click button on left to download"));
+ // So that update() will post a status to the summary bar before we reload
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException ie) {}
} else
addFormNotice(_("No update available"));
return;
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java
new file mode 100644
index 0000000000..8045c6e801
--- /dev/null
+++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java
@@ -0,0 +1,331 @@
+package net.i2p.router.web;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Properties;
+
+import net.i2p.CoreVersion;
+import net.i2p.I2PAppContext;
+import net.i2p.crypto.TrustedUpdate;
+import net.i2p.data.DataHelper;
+import net.i2p.router.Router;
+import net.i2p.router.RouterContext;
+import net.i2p.util.EepGet;
+import net.i2p.util.FileUtil;
+import net.i2p.util.I2PAppThread;
+import net.i2p.util.Log;
+import net.i2p.util.OrderedProperties;
+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 (which is ignored), 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 install.properties file at the top level.
+ * The properties 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 PluginUpdateHandler extends UpdateHandler {
+ private static PluginUpdateRunner _pluginUpdateRunner;
+ private String _xpi2pURL;
+ private String _appStatus;
+ private static final String XPI2P = "app.xpi2p";
+ private static final String ZIP = XPI2P + ".zip";
+ public static final String PLUGIN_DIR = "plugins";
+
+ private static PluginUpdateHandler _instance;
+ public static final synchronized PluginUpdateHandler getInstance(RouterContext ctx) {
+ if (_instance != null)
+ return _instance;
+ _instance = new PluginUpdateHandler(ctx);
+ return _instance;
+ }
+
+ private PluginUpdateHandler(RouterContext ctx) {
+ super(ctx);
+ _appStatus = "";
+ }
+
+ public void update(String xpi2pURL) {
+ // 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) {
+ if (_pluginUpdateRunner == null)
+ _pluginUpdateRunner = new PluginUpdateRunner(_xpi2pURL);
+ if (_pluginUpdateRunner.isRunning())
+ return;
+ _xpi2pURL = xpi2pURL;
+ _updateFile = (new File(_context.getTempDir(), "tmp" + _context.random().nextInt() + XPI2P)).getAbsolutePath();
+ System.setProperty(PROP_UPDATE_IN_PROGRESS, "true");
+ I2PAppThread update = new I2PAppThread(_pluginUpdateRunner, "AppDownload");
+ update.start();
+ }
+ }
+
+ public String getAppStatus() {
+ return _appStatus;
+ }
+
+ public boolean isRunning() {
+ return _pluginUpdateRunner != null && _pluginUpdateRunner.isRunning();
+ }
+
+ @Override
+ public boolean isDone() {
+ // FIXME
+ return false;
+ }
+
+ public class PluginUpdateRunner extends UpdateRunner implements Runnable, EepGet.StatusListener {
+ String _updateURL;
+
+ public PluginUpdateRunner(String url) {
+ super();
+ _updateURL = url;
+ }
+
+ @Override
+ protected void update() {
+ updateStatus("" + _("Downloading plugin from {0}", _xpi2pURL) + "");
+ // 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 {
+ if (shouldProxy)
+ // 10 retries!!
+ _get = new EepGet(_context, proxyHost, proxyPort, 10, _updateFile, _xpi2pURL, false);
+ else
+ _get = new EepGet(_context, 1, _updateFile, _xpi2pURL, false);
+ _get.addStatusListener(PluginUpdateRunner.this);
+ _get.fetch();
+ } catch (Throwable t) {
+ _log.error("Error downloading plugin", t);
+ }
+ }
+
+ @Override
+ public void bytesTransferred(long alreadyTransferred, int currentWrite, long bytesTransferred, long bytesRemaining, String url) {
+ StringBuilder buf = new StringBuilder(64);
+ buf.append("").append(_("Downloading plugin")).append(' ');
+ double pct = ((double)alreadyTransferred + (double)currentWrite) /
+ ((double)alreadyTransferred + (double)currentWrite + (double)bytesRemaining);
+ synchronized (_pct) {
+ buf.append(_pct.format(pct));
+ }
+ buf.append(": ");
+ buf.append(_("{0}B transferred", DataHelper.formatSize(currentWrite + alreadyTransferred)));
+ updateStatus(buf.toString());
+ }
+
+ @Override
+ public void transferComplete(long alreadyTransferred, long bytesTransferred, long bytesRemaining, String url, String outputFile, boolean notModified) {
+ updateStatus("" + _("Plugin downloaded") + "");
+ File f = new File(_updateFile);
+ File appDir = new File(_context.getAppDir(), PLUGIN_DIR);
+ if ((!appDir.exists()) && (!appDir.mkdir())) {
+ f.delete();
+ updateStatus("" + _("Cannot create plugin directory {0}", appDir.getAbsolutePath()) + "");
+ return;
+ }
+
+ TrustedUpdate up = new TrustedUpdate(_context);
+ File to = new File(_context.getTempDir(), "tmp" + _context.random().nextInt() + ZIP);
+ // extract to a zip file whether the sig is good or not, so we can get the properties file
+ String err = up.migrateFile(f, to);
+ if (err != null) {
+ updateStatus("" + err + ' ' + _("from {0}", url) + " ");
+ f.delete();
+ to.delete();
+ return;
+ }
+ File tempDir = new File(_context.getTempDir(), "tmp" + _context.random().nextInt() + "-unzip");
+ if (!FileUtil.extractZip(to, tempDir)) {
+ f.delete();
+ to.delete();
+ FileUtil.rmdir(tempDir, false);
+ updateStatus("" + _("Plugin from {0} is corrupt", url) + "");
+ return;
+ }
+ File installProps = new File(tempDir, "install.properties");
+ Properties props = new OrderedProperties();
+ try {
+ DataHelper.loadProps(props, installProps);
+ } catch (IOException ioe) {
+ f.delete();
+ to.delete();
+ FileUtil.rmdir(tempDir, false);
+ updateStatus("" + _("Plugin from {0} does not contain the required configuration file", url) + "");
+ return;
+ }
+ // we don't need this anymore, we will unzip again
+ FileUtil.rmdir(tempDir, false);
+
+ // ok, now we check sigs and deal with a bad sig
+ String pubkey = props.getProperty("key");
+ String keyName = props.getProperty("keyName");
+ if (pubkey == null || keyName == null || pubkey.length() != 172 || keyName.length() <= 0) {
+ f.delete();
+ to.delete();
+ //updateStatus("" + "Plugin contains an invalid key" + ' ' + pubkey + ' ' + keyName + "");
+ updateStatus("" + _("Plugin from {0} contains an invalid key", url) + "");
+ return;
+ }
+
+ if (up.haveKey(pubkey)) {
+ // the key is already in the TrustedUpdate keyring
+ if (!up.verify(f)) {
+ f.delete();
+ to.delete();
+ updateStatus("" + _("Plugin signature verification of {0} failed", url) + "");
+ return;
+ }
+ } else {
+ // add to keyring...
+ if(!up.addKey(pubkey, keyName)) {
+ // bad or duplicate key
+ f.delete();
+ to.delete();
+ updateStatus("" + _("Plugin signature verification of {0} failed", url) + "");
+ return;
+ }
+ // ...and try the verify again
+ if (!up.verify(f)) {
+ f.delete();
+ to.delete();
+ updateStatus("" + _("Plugin signature verification of {0} failed", url) + "");
+ return;
+ }
+ }
+ f.delete();
+
+ String appName = props.getProperty("name");
+ String version = props.getProperty("version");
+ if (appName == null || version == null || appName.length() <= 0 || version.length() <= 0 ||
+ appName.startsWith(".") || appName.indexOf("/") > 0 || appName.indexOf("\\") > 0) {
+ to.delete();
+ updateStatus("" + _("Plugin from {0} has invalid name or version", url) + "");
+ return;
+ }
+
+ String minVersion = props.getProperty("min-i2p-version");
+ if (minVersion != null &&
+ (new VersionComparator()).compare(CoreVersion.VERSION, minVersion) < 0) {
+ to.delete();
+ updateStatus("" + _("This plugin requires I2P version {0} or higher", minVersion) + "");
+ return;
+ }
+
+ minVersion = props.getProperty("min-java-version");
+ if (minVersion != null &&
+ (new VersionComparator()).compare(System.getProperty("java.version"), minVersion) < 0) {
+ to.delete();
+ updateStatus("" + _("This plugin requires Java version {0} or higher", minVersion) + "");
+ return;
+ }
+
+ boolean isUpdate = Boolean.valueOf(props.getProperty("update")).booleanValue();
+ File destDir = new File(appDir, appName);
+ if (destDir.exists()) {
+ if (!isUpdate) {
+ to.delete();
+ updateStatus("" + _("Downloaded plugin is not for upgrading but the plugin is already installed", url) + "");
+ return;
+ }
+
+ // compare previous version
+ File oldPropFile = new File(destDir, "install.properties");
+ Properties oldProps = new OrderedProperties();
+ try {
+ DataHelper.loadProps(oldProps, oldPropFile);
+ } catch (IOException ioe) {
+ to.delete();
+ FileUtil.rmdir(tempDir, false);
+ updateStatus("" + _("Installed plugin does not contain the required configuration file", url) + "");
+ return;
+ }
+ String oldPubkey = oldProps.getProperty("key");
+ String oldKeyName = oldProps.getProperty("keyName");
+ String oldAppName = props.getProperty("name");
+ if ((!pubkey.equals(oldPubkey)) || (!keyName.equals(oldKeyName)) || (!appName.equals(oldAppName))) {
+ to.delete();
+ updateStatus("" + _("Signature of downloaded plugin does not match installed plugin") + "");
+ return;
+ }
+ String oldVersion = oldProps.getProperty("version");
+ if (oldVersion == null ||
+ (new VersionComparator()).compare(oldVersion, version) >= 0) {
+ to.delete();
+ updateStatus("" + _("New plugin version {0} is not newer than installed plugin", version) + "");
+ return;
+ }
+ minVersion = props.getProperty("min-installed-version");
+ if (minVersion != null &&
+ (new VersionComparator()).compare(minVersion, oldVersion) > 0) {
+ to.delete();
+ updateStatus("" + _("Plugin update requires installed version {0} or higher", minVersion) + "");
+ return;
+ }
+ String maxVersion = props.getProperty("max-installed-version");
+ if (maxVersion != null &&
+ (new VersionComparator()).compare(maxVersion, oldVersion) < 0) {
+ to.delete();
+ updateStatus("" + _("Plugin update requires installed version {0} or lower", maxVersion) + "");
+ return;
+ }
+
+ // check if it is running now and stop it?
+
+ } else {
+ if (isUpdate) {
+ to.delete();
+ updateStatus("" + _("Plugin is for upgrades only, but the plugin is not installed", url) + "");
+ return;
+ }
+ if (!destDir.mkdir()) {
+ to.delete();
+ updateStatus("" + _("Cannot create plugin directory {0}", destDir.getAbsolutePath()) + "");
+ return;
+ }
+ }
+
+ // Finally, extract the zip to the plugin directory
+ if (!FileUtil.extractZip(to, destDir)) {
+ to.delete();
+ updateStatus("" + _("Unzip of plugin in plugin directory {0} failed", destDir.getAbsolutePath()) + "");
+ return;
+ }
+
+ to.delete();
+ updateStatus("" + _("Plugin successfully installed in {0}", destDir.getAbsolutePath()) + "");
+
+ // start everything
+ }
+
+ @Override
+ public void transferFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt) {
+ File f = new File(_updateFile);
+ f.delete();
+ updateStatus("" + _("Plugin download from {0} failed", url) + "");
+ }
+ }
+
+ @Override
+ protected void updateStatus(String s) {
+ super.updateStatus(s);
+ _appStatus = s;
+ }
+}
+
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/SummaryBarRenderer.java b/apps/routerconsole/java/src/net/i2p/router/web/SummaryBarRenderer.java
index af3323571d..7dd28c0efa 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/SummaryBarRenderer.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/SummaryBarRenderer.java
@@ -184,7 +184,7 @@ public class SummaryBarRenderer {
if (_helper.updateAvailable() || _helper.unsignedUpdateAvailable()) {
// display all the time so we display the final failure message
buf.append(UpdateHandler.getStatus());
- if ("true".equals(System.getProperty("net.i2p.router.web.UpdateHandler.updateInProgress"))) {
+ if ("true".equals(System.getProperty(UpdateHandler.PROP_UPDATE_IN_PROGRESS))) {
// nothing
} else if(
// isDone() is always false for now, see UpdateHandler
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/UpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/UpdateHandler.java
index 050f0f975a..1f1d3de264 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/UpdateHandler.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/UpdateHandler.java
@@ -37,7 +37,7 @@ public class UpdateHandler {
private String _nonce;
protected static final String SIGNED_UPDATE_FILE = "i2pupdate.sud";
- protected static final String PROP_UPDATE_IN_PROGRESS = "net.i2p.router.web.UpdateHandler.updateInProgress";
+ static final String PROP_UPDATE_IN_PROGRESS = "net.i2p.router.web.UpdateHandler.updateInProgress";
protected static final String PROP_LAST_UPDATE_TIME = "router.updateLastDownloaded";
public UpdateHandler() {
@@ -124,7 +124,7 @@ public class UpdateHandler {
protected boolean _isRunning;
protected boolean done;
protected EepGet _get;
- private final DecimalFormat _pct = new DecimalFormat("0.0%");
+ protected final DecimalFormat _pct = new DecimalFormat("0.0%");
public UpdateRunner() {
_isRunning = false;
diff --git a/apps/routerconsole/jsp/configclients.jsp b/apps/routerconsole/jsp/configclients.jsp
index 2af8ab782b..daf3bcf87b 100644
--- a/apps/routerconsole/jsp/configclients.jsp
+++ b/apps/routerconsole/jsp/configclients.jsp
@@ -54,4 +54,10 @@ button span.hide{
<%=intl._("All changes require restart to take effect.")%>
" />
+
<%=intl._("Plugin Installation")%>
+ <%=intl._("To install a plugin, enter the URL to download the plugin from:")%>
+