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:")%> +

+ +


+ " />
From 040f3e016e4d94b2b4c821a85dfbc59577769005 Mon Sep 17 00:00:00 2001 From: zzz Date: Sat, 6 Feb 2010 20:48:14 +0000 Subject: [PATCH 05/25] move jrobin from routerconsole.jar to its own jar --- apps/routerconsole/java/build.xml | 4 +++- build.xml | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/routerconsole/java/build.xml b/apps/routerconsole/java/build.xml index d8d85e21dd..95568170dc 100644 --- a/apps/routerconsole/java/build.xml +++ b/apps/routerconsole/java/build.xml @@ -64,13 +64,15 @@ - + + diff --git a/build.xml b/build.xml index c6e6537d13..f56f141785 100644 --- a/build.xml +++ b/build.xml @@ -314,6 +314,8 @@ + + @@ -487,6 +489,8 @@ + + From 58adccfd4afad7ce2e3c5954914610304e7dc965 Mon Sep 17 00:00:00 2001 From: zzz Date: Sun, 7 Feb 2010 13:32:49 +0000 Subject: [PATCH 06/25] start of a plugin starter --- .../i2p/router/web/ConfigClientsHandler.java | 20 +-- .../i2p/router/web/ConfigClientsHelper.java | 19 ++ .../src/net/i2p/router/web/PluginStarter.java | 169 ++++++++++++++++++ .../i2p/router/web/PluginUpdateHandler.java | 36 ++-- .../i2p/router/web/RouterConsoleRunner.java | 21 ++- apps/routerconsole/jsp/configclients.jsp | 6 + .../src/net/i2p/router/RouterContext.java | 4 +- .../i2p/router/startup/ClientAppConfig.java | 21 +++ .../i2p/router/startup/LoadClientAppsJob.java | 8 +- 9 files changed, 268 insertions(+), 36 deletions(-) create mode 100644 apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.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 cf81ee0227..6d0a4faa9f 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java @@ -1,7 +1,6 @@ package net.i2p.router.web; import java.io.File; -import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -13,7 +12,6 @@ import net.i2p.router.startup.ClientAppConfig; import net.i2p.router.startup.LoadClientAppsJob; import net.i2p.util.Log; -import org.mortbay.http.HttpListener; import org.mortbay.jetty.Server; /** @@ -180,16 +178,14 @@ public class ConfigClientsHandler extends FormHandler { addFormNotice(_("WebApp configuration saved successfully - restart required to take effect.")); } - // Big hack for the moment, not using properties for directory and port - // Go through all the Jetty servers, find the one serving port 7657, - // requested and add the .war to that one + /** + * Big hack for the moment, not using properties for directory and port + * Go through all the Jetty servers, find the one serving port 7657, + * requested and add the .war to that one + */ private void startWebApp(String app) { - Collection c = Server.getHttpServers(); - for (int i = 0; i < c.size(); i++) { - Server s = (Server) c.toArray()[i]; - HttpListener[] hl = s.getListeners(); - for (int j = 0; j < hl.length; j++) { - if (hl[j].getPort() == 7657) { + Server s = PluginStarter.getConsoleServer(); + if (s != null) { try { File path = new File(_context.getBaseDir(), "webapps"); path = new File(path, app + ".war"); @@ -199,8 +195,6 @@ public class ConfigClientsHandler extends FormHandler { addFormError(_("Failed to start") + ' ' + _(app) + " " + ioe + '.'); } return; - } - } } addFormError(_("Failed to find server.")); } diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java index acef26f721..3633a53356 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java @@ -68,6 +68,25 @@ public class ConfigClientsHelper extends HelperBase { return buf.toString(); } + public String getForm3() { + StringBuilder buf = new StringBuilder(1024); + buf.append("\n"); + buf.append("\n"); + Properties props = PluginStarter.pluginProperties(); + Set keys = new TreeSet(props.keySet()); + for (Iterator iter = keys.iterator(); iter.hasNext(); ) { + String name = iter.next(); + if (name.startsWith(PluginStarter.PREFIX) && name.endsWith(PluginStarter.ENABLED)) { + String app = name.substring(PluginStarter.PREFIX.length(), name.lastIndexOf(PluginStarter.ENABLED)); + String val = props.getProperty(name); + renderForm(buf, app, app, !"addressbook".equals(app), + "true".equals(val), false, app, false, false); + } + } + buf.append("
" + _("Plugin") + "" + _("Run at Startup?") + "" + _("Start Now") + "" + _("Description") + "
\n"); + return buf.toString(); + } + /** ro trumps edit and showEditButton */ private void renderForm(StringBuilder buf, String index, String name, boolean urlify, boolean enabled, boolean ro, String desc, boolean edit, boolean showEditButton) { diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java new file mode 100644 index 0000000000..006192d81c --- /dev/null +++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java @@ -0,0 +1,169 @@ +package net.i2p.router.web; + +import java.io.File; +import java.io.IOException; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Properties; +import java.util.StringTokenizer; + +import net.i2p.I2PAppContext; +import net.i2p.data.DataHelper; +import net.i2p.router.RouterContext; +import net.i2p.router.startup.ClientAppConfig; +import net.i2p.router.startup.LoadClientAppsJob; +import net.i2p.util.Log; + +import org.mortbay.http.HttpListener; +import org.mortbay.jetty.Server; + + +/** + * Start plugins that are already installed + * + * @since 0.7.12 + * @author zzz + */ +public class PluginStarter implements Runnable { + private RouterContext _context; + static final String PREFIX = "plugin."; + static final String ENABLED = ".startOnLoad"; + + public PluginStarter(RouterContext ctx) { + _context = ctx; + } + + public void run() { + startPlugins(_context); + } + + static void startPlugins(RouterContext ctx) { + Properties props = pluginProperties(); + for (Iterator iter = props.keySet().iterator(); iter.hasNext(); ) { + String name = (String)iter.next(); + if (name.startsWith(PluginStarter.PREFIX) && name.endsWith(PluginStarter.ENABLED)) { + if (Boolean.valueOf(props.getProperty(name)).booleanValue()) { + String app = name.substring(PluginStarter.PREFIX.length(), name.lastIndexOf(PluginStarter.ENABLED)); + try { + if (!startPlugin(ctx, app)) + System.err.println("Failed to start plugin: " + app); + } catch (Exception e) { + System.err.println("Failed to start plugin: " + app + ' ' + e); + } + } + } + } + } + + /** @return true on success */ + static boolean startPlugin(RouterContext ctx, String appName) throws Exception { + File pluginDir = new File(ctx.getAppDir(), PluginUpdateHandler.PLUGIN_DIR + '/' + appName); + if ((!pluginDir.exists()) || (!pluginDir.isDirectory())) { + System.err.println("Cannot start nonexistent plugin: " + appName); + return false; + } + + // load and start things in clients.config + File clientConfig = new File(pluginDir, "clients.config"); + if (clientConfig.exists()) { + Properties props = new Properties(); + DataHelper.loadProps(props, clientConfig); + List clients = ClientAppConfig.getClientApps(clientConfig); + runClientApps(ctx, pluginDir, clients); + } + + // start console webapps in console/webapps + Server server = getConsoleServer(); + if (server != null) { + File consoleDir = new File(pluginDir, "console"); + Properties props = RouterConsoleRunner.webAppProperties(consoleDir.getAbsolutePath()); + File webappDir = new File(pluginDir, "webapps"); + String fileNames[] = webappDir.list(RouterConsoleRunner.WarFilenameFilter.instance()); + if (fileNames != null) { + for (int i = 0; i < fileNames.length; i++) { + try { + String warName = fileNames[i].substring(0, fileNames[i].lastIndexOf(".war")); + // check for duplicates in $I2P ? + String enabled = props.getProperty(PREFIX + warName + ENABLED); + if (! "false".equals(enabled)) { + String path = new File(webappDir, fileNames[i]).getCanonicalPath(); + WebAppStarter.startWebApp(ctx, server, warName, path); + } + } catch (IOException ioe) { + System.err.println("Error resolving '" + fileNames[i] + "' in '" + webappDir); + } + } + } + } + + // add translation jars in console/locale + + // add themes in console/themes + + // add summary bar link + + return true; + } + + /** this auto-adds a propery for every dir in the plugin directory */ + public static Properties pluginProperties() { + File dir = I2PAppContext.getGlobalContext().getConfigDir(); + Properties rv = new Properties(); + File cfgFile = new File(dir, "plugins.config"); + + try { + DataHelper.loadProps(rv, cfgFile); + } catch (IOException ioe) {} + + File pluginDir = new File(I2PAppContext.getGlobalContext().getAppDir(), PluginUpdateHandler.PLUGIN_DIR); + File[] files = pluginDir.listFiles(); + if (files == null) + return rv; + for (int i = 0; i < files.length; i++) { + String name = files[i].getName(); + String prop = PREFIX + name + ENABLED; + if (files[i].isDirectory() && rv.getProperty(prop) == null) + rv.setProperty(prop, "true"); + } + return rv; + } + + /** see comments in ConfigClientsHandler */ + static Server getConsoleServer() { + Collection c = Server.getHttpServers(); + for (int i = 0; i < c.size(); i++) { + Server s = (Server) c.toArray()[i]; + HttpListener[] hl = s.getListeners(); + for (int j = 0; j < hl.length; j++) { + if (hl[j].getPort() == 7657) + return s; + } + } + return null; + } + + private static void runClientApps(RouterContext ctx, File pluginDir, List apps) { + Log log = ctx.logManager().getLog(PluginStarter.class); + for(ClientAppConfig app : apps) { + if (app.disabled) + continue; + String argVal[] = LoadClientAppsJob.parseArgs(app.args); + // do this after parsing so we don't need to worry about quoting + for (int i = 0; i < argVal.length; i++) { + if (argVal[i].indexOf("$") >= 0) { + argVal[i] = argVal[i].replace("$I2P", ctx.getBaseDir().getAbsolutePath()); + argVal[i] = argVal[i].replace("$CONFIG", ctx.getConfigDir().getAbsolutePath()); + argVal[i] = argVal[i].replace("$PLUGIN", pluginDir.getAbsolutePath()); + } + } + if (app.delay == 0) { + // run this guy now + LoadClientAppsJob.runClient(app.className, app.clientName, argVal, log); + } else { + // wait before firing it up + ctx.jobQueue().addJob(new LoadClientAppsJob.DelayedRunClient(ctx, app.className, app.clientName, argVal, app.delay)); + } + } + } +} diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java index 8045c6e801..814fbff86a 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java @@ -20,13 +20,13 @@ 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. + * 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 install.properties file at the top level. - * The properties file contains properties for the package name, version, + * 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. @@ -159,7 +159,7 @@ public class PluginUpdateHandler extends UpdateHandler { updateStatus("" + _("Plugin from {0} is corrupt", url) + ""); return; } - File installProps = new File(tempDir, "install.properties"); + File installProps = new File(tempDir, "plugin.config"); Properties props = new OrderedProperties(); try { DataHelper.loadProps(props, installProps); @@ -220,6 +220,8 @@ public class PluginUpdateHandler extends UpdateHandler { return; } + // todo compare sud version with property version + String minVersion = props.getProperty("min-i2p-version"); if (minVersion != null && (new VersionComparator()).compare(CoreVersion.VERSION, minVersion) < 0) { @@ -236,17 +238,16 @@ public class PluginUpdateHandler extends UpdateHandler { return; } - boolean isUpdate = Boolean.valueOf(props.getProperty("update")).booleanValue(); File destDir = new File(appDir, appName); if (destDir.exists()) { - if (!isUpdate) { + if (Boolean.valueOf(props.getProperty("install-only")).booleanValue()) { 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"); + File oldPropFile = new File(destDir, "plugin.config"); Properties oldProps = new OrderedProperties(); try { DataHelper.loadProps(oldProps, oldPropFile); @@ -289,7 +290,7 @@ public class PluginUpdateHandler extends UpdateHandler { // check if it is running now and stop it? } else { - if (isUpdate) { + if (Boolean.valueOf(props.getProperty("update-only")).booleanValue()) { to.delete(); updateStatus("" + _("Plugin is for upgrades only, but the plugin is not installed", url) + ""); return; @@ -309,9 +310,22 @@ public class PluginUpdateHandler extends UpdateHandler { } to.delete(); - updateStatus("" + _("Plugin successfully installed in {0}", destDir.getAbsolutePath()) + ""); - - // start everything + if (Boolean.valueOf(props.getProperty("dont-start-at-install")).booleanValue()) { + if (Boolean.valueOf(props.getProperty("router-restart-required")).booleanValue()) + updateStatus("" + _("Plugin {0} successfully installed, router restart required", appName) + ""); + else + updateStatus("" + _("Plugin {0} successfully installed", appName) + ""); + } else { + // start everything + try { + if (PluginStarter.startPlugin(_context, appName)) + updateStatus("" + _("Plugin {0} started", appName) + ""); + else + updateStatus("" + _("Failed to start plugin {0}, check logs", appName) + ""); + } catch (Exception e) { + updateStatus("" + _("Failed to start plugin {0}:", appName) + ' ' + e + ""); + } + } } @Override 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 31d854aeae..07d3c2ed75 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java @@ -181,13 +181,17 @@ public class RouterConsoleRunner { } NewsFetcher fetcher = NewsFetcher.getInstance(I2PAppContext.getGlobalContext()); - Thread t = new I2PAppThread(fetcher, "NewsFetcher"); - t.setDaemon(true); + Thread t = new I2PAppThread(fetcher, "NewsFetcher", true); t.start(); - Thread st = new I2PAppThread(new StatSummarizer(), "StatSummarizer"); - st.setDaemon(true); - st.start(); + t = new I2PAppThread(new StatSummarizer(), "StatSummarizer", true); + t.start(); + + List contexts = RouterContext.listContexts(); + if (contexts != null) { + t = new I2PAppThread(new PluginStarter(contexts.get(0)), "PluginStarter", true); + t.start(); + } } static void initialize(WebApplicationContext context) { @@ -206,10 +210,10 @@ public class RouterConsoleRunner { } static String getPassword() { - List contexts = RouterContext.listContexts(); + List contexts = RouterContext.listContexts(); if (contexts != null) { for (int i = 0; i < contexts.size(); i++) { - RouterContext ctx = (RouterContext)contexts.get(i); + RouterContext ctx = contexts.get(i); String password = ctx.getProperty("consolePassword"); if (password != null) { password = password.trim(); @@ -267,11 +271,12 @@ public class RouterConsoleRunner { } } - private static class WarFilenameFilter implements FilenameFilter { + static class WarFilenameFilter implements FilenameFilter { private static final WarFilenameFilter _filter = new WarFilenameFilter(); public static WarFilenameFilter instance() { return _filter; } public boolean accept(File dir, String name) { return (name != null) && (name.endsWith(".war") && !name.equals(ROUTERCONSOLE + ".war")); } } + } diff --git a/apps/routerconsole/jsp/configclients.jsp b/apps/routerconsole/jsp/configclients.jsp index daf3bcf87b..0fcb2e2e97 100644 --- a/apps/routerconsole/jsp/configclients.jsp +++ b/apps/routerconsole/jsp/configclients.jsp @@ -54,6 +54,12 @@ button span.hide{ <%=intl._("All changes require restart to take effect.")%>


" /> +

<%=intl._("Plugin Configuration")%>

+ <%=intl._("The plugins listed below are started by the webConsole client and run in the same JVM as the router. They are usually web applications accessible through the router console.")%> +

+ +


+ " />

<%=intl._("Plugin Installation")%>

<%=intl._("To install a plugin, enter the URL to download the plugin from:")%>

diff --git a/router/java/src/net/i2p/router/RouterContext.java b/router/java/src/net/i2p/router/RouterContext.java index dfa9d7c210..b4fa753812 100644 --- a/router/java/src/net/i2p/router/RouterContext.java +++ b/router/java/src/net/i2p/router/RouterContext.java @@ -62,7 +62,7 @@ public class RouterContext extends I2PAppContext { private Calculator _capacityCalc; - private static List _contexts = new ArrayList(1); + private static List _contexts = new ArrayList(1); public RouterContext(Router router) { this(router, null); } public RouterContext(Router router, Properties envProps) { @@ -148,7 +148,7 @@ public class RouterContext extends I2PAppContext { * context is created or a router is shut down. * */ - public static List listContexts() { return _contexts; } + public static List listContexts() { return _contexts; } /** what router is this context working for? */ public Router router() { return _router; } diff --git a/router/java/src/net/i2p/router/startup/ClientAppConfig.java b/router/java/src/net/i2p/router/startup/ClientAppConfig.java index 54342a8951..b08e7577d4 100644 --- a/router/java/src/net/i2p/router/startup/ClientAppConfig.java +++ b/router/java/src/net/i2p/router/startup/ClientAppConfig.java @@ -4,6 +4,7 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Properties; @@ -72,6 +73,26 @@ public class ClientAppConfig { */ public static List getClientApps(RouterContext ctx) { Properties clientApps = getClientAppProps(ctx); + return getClientApps(clientApps); + } + + /* + * Go through the properties, and return a List of ClientAppConfig structures + */ + public static List getClientApps(File cfgFile) { + Properties clientApps = new Properties(); + try { + DataHelper.loadProps(clientApps, cfgFile); + } catch (IOException ioe) { + return Collections.EMPTY_LIST; + } + return getClientApps(clientApps); + } + + /* + * Go through the properties, and return a List of ClientAppConfig structures + */ + private static List getClientApps(Properties clientApps) { List rv = new ArrayList(8); int i = 0; while (true) { diff --git a/router/java/src/net/i2p/router/startup/LoadClientAppsJob.java b/router/java/src/net/i2p/router/startup/LoadClientAppsJob.java index 0f86f5b61b..663c56025b 100644 --- a/router/java/src/net/i2p/router/startup/LoadClientAppsJob.java +++ b/router/java/src/net/i2p/router/startup/LoadClientAppsJob.java @@ -48,16 +48,20 @@ public class LoadClientAppsJob extends JobImpl { } } } - private class DelayedRunClient extends JobImpl { + + public static class DelayedRunClient extends JobImpl { private String _className; private String _clientName; private String _args[]; + private Log _log; + public DelayedRunClient(RouterContext enclosingContext, String className, String clientName, String args[], long delay) { super(enclosingContext); _className = className; _clientName = clientName; _args = args; - getTiming().setStartAfter(LoadClientAppsJob.this.getContext().clock().now() + delay); + _log = enclosingContext.logManager().getLog(LoadClientAppsJob.class); + getTiming().setStartAfter(getContext().clock().now() + delay); } public String getName() { return "Delayed client job"; } public void runJob() { From e9f1da85e4fc3e17bab7205001feef6ad5049116 Mon Sep 17 00:00:00 2001 From: zzz Date: Sun, 7 Feb 2010 17:13:44 +0000 Subject: [PATCH 07/25] classpath for plugins --- .../src/net/i2p/router/web/PluginStarter.java | 56 +++++++++++++++++-- .../i2p/router/startup/ClientAppConfig.java | 13 ++++- 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java index 006192d81c..03f567f134 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java @@ -2,6 +2,9 @@ package net.i2p.router.web; import java.io.File; import java.io.IOException; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; import java.util.Collection; import java.util.Iterator; import java.util.List; @@ -39,6 +42,7 @@ public class PluginStarter implements Runnable { } static void startPlugins(RouterContext ctx) { + Log log = ctx.logManager().getLog(PluginStarter.class); Properties props = pluginProperties(); for (Iterator iter = props.keySet().iterator(); iter.hasNext(); ) { String name = (String)iter.next(); @@ -47,9 +51,9 @@ public class PluginStarter implements Runnable { String app = name.substring(PluginStarter.PREFIX.length(), name.lastIndexOf(PluginStarter.ENABLED)); try { if (!startPlugin(ctx, app)) - System.err.println("Failed to start plugin: " + app); + log.error("Failed to start plugin: " + app); } catch (Exception e) { - System.err.println("Failed to start plugin: " + app + ' ' + e); + log.error("Failed to start plugin: " + app, e); } } } @@ -58,9 +62,10 @@ public class PluginStarter implements Runnable { /** @return true on success */ static boolean startPlugin(RouterContext ctx, String appName) throws Exception { + Log log = ctx.logManager().getLog(PluginStarter.class); File pluginDir = new File(ctx.getAppDir(), PluginUpdateHandler.PLUGIN_DIR + '/' + appName); if ((!pluginDir.exists()) || (!pluginDir.isDirectory())) { - System.err.println("Cannot start nonexistent plugin: " + appName); + log.error("Cannot start nonexistent plugin: " + appName); return false; } @@ -91,7 +96,7 @@ public class PluginStarter implements Runnable { WebAppStarter.startWebApp(ctx, server, warName, path); } } catch (IOException ioe) { - System.err.println("Error resolving '" + fileNames[i] + "' in '" + webappDir); + log.error("Error resolving '" + fileNames[i] + "' in '" + webappDir, ioe); } } } @@ -157,6 +162,15 @@ public class PluginStarter implements Runnable { argVal[i] = argVal[i].replace("$PLUGIN", pluginDir.getAbsolutePath()); } } + if (app.classpath != null) { + String cp = new String(app.classpath); + if (cp.indexOf("$") >= 0) { + cp = cp.replace("$I2P", ctx.getBaseDir().getAbsolutePath()); + cp = cp.replace("$CONFIG", ctx.getConfigDir().getAbsolutePath()); + cp = cp.replace("$PLUGIN", pluginDir.getAbsolutePath()); + } + addToClasspath(cp, app.clientName, log); + } if (app.delay == 0) { // run this guy now LoadClientAppsJob.runClient(app.className, app.clientName, argVal, log); @@ -166,4 +180,38 @@ public class PluginStarter implements Runnable { } } } + + /** + * Perhaps there's an easy way to use Thread.setContextClassLoader() + * but I don't see how to make it magically get used for everything. + * So add this to the whole JVM's classpath. + */ + private static void addToClasspath(String classpath, String clientName, Log log) { + StringTokenizer tok = new StringTokenizer(classpath, ","); + while (tok.hasMoreTokens()) { + String elem = tok.nextToken().trim(); + File f = new File(elem); + if (!f.isAbsolute()) { + log.error("Plugin client " + clientName + " classpath element is not absolute: " + f); + continue; + } + try { + log.error("INFO: Adding plugin classpath: " + f); + addPath(f.toURI().toURL()); + } catch (Exception e) { + log.error("Plugin client " + clientName + " bad classpath element: " + f, e); + } + } + } + + /** + * http://jimlife.wordpress.com/2007/12/19/java-adding-new-classpath-at-runtime/ + */ + public static void addPath(URL u) throws Exception { + URLClassLoader urlClassLoader = (URLClassLoader) ClassLoader.getSystemClassLoader(); + Class urlClass = URLClassLoader.class; + Method method = urlClass.getDeclaredMethod("addURL", new Class[]{URL.class}); + method.setAccessible(true); + method.invoke(urlClassLoader, new Object[]{u}); + } } diff --git a/router/java/src/net/i2p/router/startup/ClientAppConfig.java b/router/java/src/net/i2p/router/startup/ClientAppConfig.java index b08e7577d4..7afd0fc992 100644 --- a/router/java/src/net/i2p/router/startup/ClientAppConfig.java +++ b/router/java/src/net/i2p/router/startup/ClientAppConfig.java @@ -33,6 +33,9 @@ public class ClientAppConfig { public String args; public long delay; public boolean disabled; + /** @since 0.7.12 */ + public String classpath; + public ClientAppConfig(String cl, String client, String a, long d, boolean dis) { className = cl; clientName = client; @@ -41,6 +44,12 @@ public class ClientAppConfig { disabled = dis; } + /** @since 0.7.12 */ + public ClientAppConfig(String cl, String client, String a, long d, boolean dis, String cp) { + this(cl, client, a, d, dis); + classpath = cp; + } + public static File configFile(I2PAppContext ctx) { String clientConfigFile = ctx.getProperty(PROP_CLIENT_CONFIG_FILENAME, DEFAULT_CLIENT_CONFIG_FILENAME); File cfgFile = new File(clientConfigFile); @@ -104,6 +113,7 @@ public class ClientAppConfig { String delayStr = clientApps.getProperty(PREFIX + i + ".delay"); String onBoot = clientApps.getProperty(PREFIX + i + ".onBoot"); String disabled = clientApps.getProperty(PREFIX + i + ".startOnLoad"); + String classpath = clientApps.getProperty(PREFIX + i + ".classpath"); i++; boolean dis = disabled != null && "false".equals(disabled); @@ -115,11 +125,12 @@ public class ClientAppConfig { if (delayStr != null && !onStartup) try { delay = 1000*Integer.parseInt(delayStr); } catch (NumberFormatException nfe) {} - rv.add(new ClientAppConfig(className, clientName, args, delay, dis)); + rv.add(new ClientAppConfig(className, clientName, args, delay, dis, classpath)); } return rv; } + /** classpath not supported */ public static void writeClientAppConfig(RouterContext ctx, List apps) { File cfgFile = configFile(ctx); FileOutputStream fos = null; From 3c8355790ffc9fde0cb70c5800868bf902daea53 Mon Sep 17 00:00:00 2001 From: zzz Date: Sun, 7 Feb 2010 18:18:31 +0000 Subject: [PATCH 08/25] load translation bundles in plugins --- .../src/net/i2p/router/web/PluginStarter.java | 26 ++++++++++++++++++- core/java/src/net/i2p/util/Translate.java | 9 +++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java index 03f567f134..3d11191a97 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java @@ -17,6 +17,7 @@ import net.i2p.router.RouterContext; import net.i2p.router.startup.ClientAppConfig; import net.i2p.router.startup.LoadClientAppsJob; import net.i2p.util.Log; +import net.i2p.util.Translate; import org.mortbay.http.HttpListener; import org.mortbay.jetty.Server; @@ -103,6 +104,29 @@ public class PluginStarter implements Runnable { } // add translation jars in console/locale + // These will not override existing resource bundles since we are adding them + // later in the classpath. + File localeDir = new File(pluginDir, "console/locale"); + if (localeDir.exists() && localeDir.isDirectory()) { + File[] files = localeDir.listFiles(); + if (files != null) { + boolean added = false; + for (int i = 0; i < files.length; i++) { + File f = files[i]; + if (f.getName().endsWith(".jar")) { + try { + addPath(f.toURI().toURL()); + log.error("INFO: Adding translation plugin to classpath: " + f); + added = true; + } catch (Exception e) { + log.error("Plugin " + appName + " bad classpath element: " + f, e); + } + } + } + if (added) + Translate.clearCache(); + } + } // add themes in console/themes @@ -196,8 +220,8 @@ public class PluginStarter implements Runnable { continue; } try { - log.error("INFO: Adding plugin classpath: " + f); addPath(f.toURI().toURL()); + log.error("INFO: Adding plugin to classpath: " + f); } catch (Exception e) { log.error("Plugin client " + clientName + " bad classpath element: " + f, e); } diff --git a/core/java/src/net/i2p/util/Translate.java b/core/java/src/net/i2p/util/Translate.java index c9072b62f5..799b89c00c 100644 --- a/core/java/src/net/i2p/util/Translate.java +++ b/core/java/src/net/i2p/util/Translate.java @@ -126,4 +126,13 @@ public abstract class Translate { } return rv; } + + /** + * Clear the cache. + * Call this after adding new bundles to the classpath. + * @since 0.7.12 + */ + public static void clearCache() { + _missing.clear(); + } } From 9012baf51eabd72760fb88239eea97f05754d68d Mon Sep 17 00:00:00 2001 From: zzz Date: Sun, 7 Feb 2010 19:01:06 +0000 Subject: [PATCH 09/25] plugin links --- .../src/net/i2p/router/web/NavHelper.java | 25 +++++++++++-------- .../src/net/i2p/router/web/PluginStarter.java | 9 +++++++ .../i2p/router/web/SummaryBarRenderer.java | 6 ++++- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/apps/routerconsole/java/src/net/i2p/router/web/NavHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/NavHelper.java index cabb68f156..37ca94cd66 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/NavHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/NavHelper.java @@ -1,14 +1,13 @@ package net.i2p.router.web; -import java.util.HashMap; import java.util.Iterator; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import net.i2p.I2PAppContext; -public class NavHelper extends HelperBase { - private static Map _apps = new HashMap(); - - public NavHelper() {} +public class NavHelper { + private static Map _apps = new ConcurrentHashMap(); /** * To register a new client application so that it shows up on the router @@ -25,13 +24,17 @@ public class NavHelper extends HelperBase { _apps.remove(name); } - public String getClientAppLinks() { + /** + * Fixme, this translates with the router console bundle, not + * the plugin bundle + */ + public static String getClientAppLinks(I2PAppContext ctx) { StringBuilder buf = new StringBuilder(1024); - for (Iterator iter = _apps.keySet().iterator(); iter.hasNext(); ) { - String name = (String)iter.next(); - String path = (String)_apps.get(name); - buf.append(""); - buf.append(name).append(" |"); + for (Iterator iter = _apps.keySet().iterator(); iter.hasNext(); ) { + String name = iter.next(); + String path = _apps.get(name); + buf.append(" "); + buf.append(Messages.getString(name, ctx)).append(""); } return buf.toString(); } diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java index 3d11191a97..99562789ab 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java @@ -131,6 +131,15 @@ public class PluginStarter implements Runnable { // add themes in console/themes // add summary bar link + File pluginConfig = new File(pluginDir, "plugin.config"); + if (pluginConfig.exists()) { + Properties props = new Properties(); + DataHelper.loadProps(props, pluginConfig); + String name = props.getProperty("consoleLinkName"); + String url = props.getProperty("consoleLinkURL"); + if (name != null && url != null && name.length() > 0 && url.length() > 0) + NavHelper.registerApp(name, url); + } return true; } 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 7dd28c0efa..153ef06301 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/SummaryBarRenderer.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/SummaryBarRenderer.java @@ -70,7 +70,11 @@ public class SummaryBarRenderer { .append(_("Anonymous resident webserver")) .append("\">") .append(_("Webserver")) - .append("\n" + + .append("") + + .append(NavHelper.getClientAppLinks(_context)) + + .append("\n" + "


Date: Mon, 8 Feb 2010 16:15:23 +0000 Subject: [PATCH 10/25] plugin description on configclients --- .../i2p/router/web/ConfigClientsHelper.java | 54 ++++++++++++++++++- .../src/net/i2p/router/web/NavHelper.java | 5 +- .../src/net/i2p/router/web/PluginStarter.java | 41 ++++++++++---- .../i2p/router/web/PluginUpdateHandler.java | 6 ++- 4 files changed, 90 insertions(+), 16 deletions(-) diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java index 3633a53356..95cd1086f9 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java @@ -1,5 +1,7 @@ package net.i2p.router.web; +import java.text.SimpleDateFormat; +import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Properties; @@ -79,8 +81,56 @@ public class ConfigClientsHelper extends HelperBase { if (name.startsWith(PluginStarter.PREFIX) && name.endsWith(PluginStarter.ENABLED)) { String app = name.substring(PluginStarter.PREFIX.length(), name.lastIndexOf(PluginStarter.ENABLED)); String val = props.getProperty(name); - renderForm(buf, app, app, !"addressbook".equals(app), - "true".equals(val), false, app, false, false); + Properties appProps = PluginStarter.pluginProperties(_context, app); + StringBuilder desc = new StringBuilder(256); + desc.append("") + .append("
").append(_("Version")).append("").append(appProps.getProperty("version")) + .append("
") + .append(_("Signed by")).append("").append(appProps.getProperty("keyName")); + String s = appProps.getProperty("date"); + if (s != null) { + long ms = 0; + try { + ms = Long.parseLong(s); + } catch (NumberFormatException nfe) {} + if (ms > 0) { + String date = (new SimpleDateFormat("yyyy-MM-dd HH:mm")).format(new Date(ms)); + desc.append("
") + .append(_("Date")).append("").append(date); + } + } + s = appProps.getProperty("author"); + if (s != null) { + // fixme translate info using bundle specified in appProps + desc.append("
") + .append(_("Author")).append("").append(s); + } + s = appProps.getProperty("description_" + Messages.getLanguage(_context)); + if (s == null) + s = appProps.getProperty("description"); + if (s != null) { + // fixme translate info using bundle specified in appProps + desc.append("
") + .append(_("Description")).append("").append(s); + } + s = appProps.getProperty("license"); + if (s != null) { + desc.append("
") + .append(_("License")).append("").append(s); + } + s = appProps.getProperty("websiteURL"); + if (s != null) { + desc.append("
") + .append("").append(_("Website")).append(" "); + } + s = appProps.getProperty("updateURL"); + if (s != null) { + desc.append("
") + .append("").append(_("Update link")).append(" "); + } + desc.append("
"); + renderForm(buf, app, app, false, + "true".equals(val), false, desc.toString(), false, false); } } buf.append("\n"); diff --git a/apps/routerconsole/java/src/net/i2p/router/web/NavHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/NavHelper.java index 37ca94cd66..488275a23b 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/NavHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/NavHelper.java @@ -25,8 +25,7 @@ public class NavHelper { } /** - * Fixme, this translates with the router console bundle, not - * the plugin bundle + * Translated string is loaded by PluginStarter */ public static String getClientAppLinks(I2PAppContext ctx) { StringBuilder buf = new StringBuilder(1024); @@ -34,7 +33,7 @@ public class NavHelper { String name = iter.next(); String path = _apps.get(name); buf.append("
"); - buf.append(Messages.getString(name, ctx)).append(""); + buf.append(name).append(""); } return buf.toString(); } diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java index 99562789ab..df777712c2 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java @@ -131,20 +131,31 @@ public class PluginStarter implements Runnable { // add themes in console/themes // add summary bar link - File pluginConfig = new File(pluginDir, "plugin.config"); - if (pluginConfig.exists()) { - Properties props = new Properties(); - DataHelper.loadProps(props, pluginConfig); - String name = props.getProperty("consoleLinkName"); - String url = props.getProperty("consoleLinkURL"); - if (name != null && url != null && name.length() > 0 && url.length() > 0) - NavHelper.registerApp(name, url); - } + Properties props = pluginProperties(ctx, appName); + String name = props.getProperty("consoleLinkName_" + Messages.getLanguage(ctx)); + if (name == null) + name = props.getProperty("consoleLinkName"); + String url = props.getProperty("consoleLinkURL"); + if (name != null && url != null && name.length() > 0 && url.length() > 0) + NavHelper.registerApp(name, url); return true; } - /** this auto-adds a propery for every dir in the plugin directory */ + /** plugin.config */ + public static Properties pluginProperties(I2PAppContext ctx, String appName) { + File cfgFile = new File(ctx.getAppDir(), PluginUpdateHandler.PLUGIN_DIR + '/' + appName + '/' + "plugin.config"); + Properties rv = new Properties(); + try { + DataHelper.loadProps(rv, cfgFile); + } catch (IOException ioe) {} + return rv; + } + + /** + * plugins.config + * this auto-adds a propery for every dir in the plugin directory + */ public static Properties pluginProperties() { File dir = I2PAppContext.getGlobalContext().getConfigDir(); Properties rv = new Properties(); @@ -167,6 +178,16 @@ public class PluginStarter implements Runnable { return rv; } + /** + * plugins.config + */ + public static void storePluginProperties(Properties props) { + File cfgFile = new File(I2PAppContext.getGlobalContext().getConfigDir(), "plugins.config"); + try { + DataHelper.storeProps(props, cfgFile); + } catch (IOException ioe) {} + } + /** see comments in ConfigClientsHandler */ static Server getConsoleServer() { Collection c = Server.getHttpServers(); diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java index 814fbff86a..7eb343a685 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java @@ -313,8 +313,12 @@ public class PluginUpdateHandler extends UpdateHandler { if (Boolean.valueOf(props.getProperty("dont-start-at-install")).booleanValue()) { if (Boolean.valueOf(props.getProperty("router-restart-required")).booleanValue()) updateStatus("" + _("Plugin {0} successfully installed, router restart required", appName) + ""); - else + else { updateStatus("" + _("Plugin {0} successfully installed", appName) + ""); + Properties pluginProps = PluginStarter.pluginProperties(); + pluginProps.setProperty(PluginStarter.PREFIX + appName + PluginStarter.ENABLED, "false"); + PluginStarter.storePluginProperties(pluginProps); + } } else { // start everything try { From 66375e25c6890f0abee86b8980b82916f11d1241 Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 8 Feb 2010 18:45:53 +0000 Subject: [PATCH 11/25] start of a plugin version checker --- .../i2p/router/web/PluginUpdateChecker.java | 139 ++++++++++++++++++ .../src/net/i2p/crypto/TrustedUpdate.java | 46 +++++- core/java/src/net/i2p/util/PartialEepGet.java | 130 ++++++++++++++++ 3 files changed, 312 insertions(+), 3 deletions(-) create mode 100644 apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateChecker.java create mode 100644 core/java/src/net/i2p/util/PartialEepGet.java 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 0000000000..e19e23d91f --- /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("" + _("Cannot update, plugin {0} is not installed", appName) + ""); + 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("" + _("Checking plugin {0} for updates", _appName) + ""); + // 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("" + _("New plugin version {0} is available", newVersion) + ""); + else + updateStatus("" + _("No new version is available for plugin {0}", _appName) + ""); + } + + @Override + public void transferFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt) { + File f = new File(_updateFile); + f.delete(); + updateStatus("" + _("Update check failed for plugin {0}", _appName) + ""); + } + } +} + diff --git a/core/java/src/net/i2p/crypto/TrustedUpdate.java b/core/java/src/net/i2p/crypto/TrustedUpdate.java index 133a180333..c567fcb310 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 0000000000..5b18bc7619 --- /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(); + } +} From b7a0aeea3424ec8dc3b4830f3bfdbc990d1fa6d5 Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 8 Feb 2010 19:04:46 +0000 Subject: [PATCH 12/25] plugin buttons --- .../i2p/router/web/ConfigClientsHelper.java | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java index 95cd1086f9..1779d613b2 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java @@ -42,11 +42,11 @@ public class ConfigClientsHelper extends HelperBase { ClientAppConfig ca = clients.get(cur); renderForm(buf, ""+cur, ca.clientName, false, !ca.disabled, "webConsole".equals(ca.clientName) || "Web console".equals(ca.clientName), - ca.className + ((ca.args != null) ? " " + ca.args : ""), (""+cur).equals(_edit), true); + ca.className + ((ca.args != null) ? " " + ca.args : ""), (""+cur).equals(_edit), true, false); } if ("new".equals(_edit)) - renderForm(buf, "" + clients.size(), "", false, false, false, "", true, false); + renderForm(buf, "" + clients.size(), "", false, false, false, "", true, false, false); buf.append("\n"); return buf.toString(); } @@ -63,7 +63,7 @@ public class ConfigClientsHelper extends HelperBase { String app = name.substring(RouterConsoleRunner.PREFIX.length(), name.lastIndexOf(RouterConsoleRunner.ENABLED)); String val = props.getProperty(name); renderForm(buf, app, app, !"addressbook".equals(app), - "true".equals(val), RouterConsoleRunner.ROUTERCONSOLE.equals(app), app + ".war", false, false); + "true".equals(val), RouterConsoleRunner.ROUTERCONSOLE.equals(app), app + ".war", false, false, false); } } buf.append("\n"); @@ -101,7 +101,6 @@ public class ConfigClientsHelper extends HelperBase { } s = appProps.getProperty("author"); if (s != null) { - // fixme translate info using bundle specified in appProps desc.append("") .append(_("Author")).append("").append(s); } @@ -109,7 +108,6 @@ public class ConfigClientsHelper extends HelperBase { if (s == null) s = appProps.getProperty("description"); if (s != null) { - // fixme translate info using bundle specified in appProps desc.append("") .append(_("Description")).append("").append(s); } @@ -123,14 +121,14 @@ public class ConfigClientsHelper extends HelperBase { desc.append("") .append("").append(_("Website")).append(" "); } - s = appProps.getProperty("updateURL"); - if (s != null) { + String updateURL = appProps.getProperty("updateURL"); + if (updateURL != null) { desc.append("") - .append("").append(_("Update link")).append(" "); + .append("").append(_("Update link")).append(" "); } desc.append(""); renderForm(buf, app, app, false, - "true".equals(val), false, desc.toString(), false, false); + "true".equals(val), false, desc.toString(), false, false, updateURL != null); } } buf.append("\n"); @@ -139,7 +137,8 @@ public class ConfigClientsHelper extends HelperBase { /** ro trumps edit and showEditButton */ private void renderForm(StringBuilder buf, String index, String name, boolean urlify, - boolean enabled, boolean ro, String desc, boolean edit, boolean showEditButton) { + boolean enabled, boolean ro, String desc, boolean edit, + boolean showEditButton, boolean showUpdateButton) { buf.append(""); if (urlify && enabled) { String link = "/"; @@ -165,10 +164,14 @@ public class ConfigClientsHelper extends HelperBase { if ((!enabled) && !edit) { buf.append(""); } - if (showEditButton && (!edit) && !ro) { + if (showEditButton && (!edit) && !ro) buf.append(""); - buf.append(""); + if (showUpdateButton && (!edit) && !ro) { + buf.append(""); + buf.append(""); } + if ((!edit) && !ro) + buf.append(""); buf.append(""); if (edit && !ro) { buf.append(" Date: Mon, 8 Feb 2010 20:37:49 +0000 Subject: [PATCH 13/25] plugin stopper --- .../src/net/i2p/router/web/PluginStarter.java | 50 +++++++++++++++++-- .../i2p/router/web/PluginUpdateHandler.java | 7 ++- .../i2p/router/startup/ClientAppConfig.java | 10 ++-- 3 files changed, 58 insertions(+), 9 deletions(-) diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java index df777712c2..0bf04dc134 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java @@ -76,7 +76,7 @@ public class PluginStarter implements Runnable { Properties props = new Properties(); DataHelper.loadProps(props, clientConfig); List clients = ClientAppConfig.getClientApps(clientConfig); - runClientApps(ctx, pluginDir, clients); + runClientApps(ctx, pluginDir, clients, true); } // start console webapps in console/webapps @@ -142,6 +142,37 @@ public class PluginStarter implements Runnable { return true; } + /** @return true on success */ + static boolean stopPlugin(RouterContext ctx, String appName) throws Exception { + Log log = ctx.logManager().getLog(PluginStarter.class); + File pluginDir = new File(ctx.getAppDir(), PluginUpdateHandler.PLUGIN_DIR + '/' + appName); + if ((!pluginDir.exists()) || (!pluginDir.isDirectory())) { + log.error("Cannot stop nonexistent plugin: " + appName); + return false; + } + + // stop things in clients.config + File clientConfig = new File(pluginDir, "clients.config"); + if (clientConfig.exists()) { + Properties props = new Properties(); + DataHelper.loadProps(props, clientConfig); + List clients = ClientAppConfig.getClientApps(clientConfig); + runClientApps(ctx, pluginDir, clients, false); + } + + // stop console webapps in console/webapps + + // remove summary bar link + Properties props = pluginProperties(ctx, appName); + String name = props.getProperty("consoleLinkName_" + Messages.getLanguage(ctx)); + if (name == null) + name = props.getProperty("consoleLinkName"); + if (name != null && name.length() > 0) + NavHelper.unregisterApp(name); + + return true; + } + /** plugin.config */ public static Properties pluginProperties(I2PAppContext ctx, String appName) { File cfgFile = new File(ctx.getAppDir(), PluginUpdateHandler.PLUGIN_DIR + '/' + appName + '/' + "plugin.config"); @@ -202,12 +233,21 @@ public class PluginStarter implements Runnable { return null; } - private static void runClientApps(RouterContext ctx, File pluginDir, List apps) { + /** @param start true=start, false=stop */ + private static void runClientApps(RouterContext ctx, File pluginDir, List apps, boolean start) { Log log = ctx.logManager().getLog(PluginStarter.class); for(ClientAppConfig app : apps) { - if (app.disabled) + if (start && app.disabled) continue; - String argVal[] = LoadClientAppsJob.parseArgs(app.args); + String argVal[]; + if (start) { + argVal = LoadClientAppsJob.parseArgs(app.args); + } else { + // stopargs must be present + if (app.stopargs == null || app.stopargs.length() <= 0) + continue; + argVal = LoadClientAppsJob.parseArgs(app.stopargs); + } // do this after parsing so we don't need to worry about quoting for (int i = 0; i < argVal.length; i++) { if (argVal[i].indexOf("$") >= 0) { @@ -225,7 +265,7 @@ public class PluginStarter implements Runnable { } addToClasspath(cp, app.clientName, log); } - if (app.delay == 0) { + if (app.delay == 0 || !start) { // run this guy now LoadClientAppsJob.runClient(app.className, app.clientName, argVal, log); } else { diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java index 7eb343a685..c68346b99a 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java @@ -287,7 +287,12 @@ public class PluginUpdateHandler extends UpdateHandler { return; } - // check if it is running now and stop it? + // check if it is running first? + try { + if (!PluginStarter.stopPlugin(_context, appName)) { + // failed, ignore + } + } catch (Exception e) {} // ignore } else { if (Boolean.valueOf(props.getProperty("update-only")).booleanValue()) { diff --git a/router/java/src/net/i2p/router/startup/ClientAppConfig.java b/router/java/src/net/i2p/router/startup/ClientAppConfig.java index 7afd0fc992..3d57699a33 100644 --- a/router/java/src/net/i2p/router/startup/ClientAppConfig.java +++ b/router/java/src/net/i2p/router/startup/ClientAppConfig.java @@ -35,6 +35,8 @@ public class ClientAppConfig { public boolean disabled; /** @since 0.7.12 */ public String classpath; + /** @since 0.7.12 */ + public String stopargs; public ClientAppConfig(String cl, String client, String a, long d, boolean dis) { className = cl; @@ -45,9 +47,10 @@ public class ClientAppConfig { } /** @since 0.7.12 */ - public ClientAppConfig(String cl, String client, String a, long d, boolean dis, String cp) { + public ClientAppConfig(String cl, String client, String a, long d, boolean dis, String cp, String sa) { this(cl, client, a, d, dis); classpath = cp; + stopargs = sa; } public static File configFile(I2PAppContext ctx) { @@ -114,6 +117,7 @@ public class ClientAppConfig { String onBoot = clientApps.getProperty(PREFIX + i + ".onBoot"); String disabled = clientApps.getProperty(PREFIX + i + ".startOnLoad"); String classpath = clientApps.getProperty(PREFIX + i + ".classpath"); + String stopargs = clientApps.getProperty(PREFIX + i + ".stopargs"); i++; boolean dis = disabled != null && "false".equals(disabled); @@ -125,12 +129,12 @@ public class ClientAppConfig { if (delayStr != null && !onStartup) try { delay = 1000*Integer.parseInt(delayStr); } catch (NumberFormatException nfe) {} - rv.add(new ClientAppConfig(className, clientName, args, delay, dis, classpath)); + rv.add(new ClientAppConfig(className, clientName, args, delay, dis, classpath, stopargs)); } return rv; } - /** classpath not supported */ + /** classpath and stopargs not supported */ public static void writeClientAppConfig(RouterContext ctx, List apps) { File cfgFile = configFile(ctx); FileOutputStream fos = null; From a109ebef28239c88dd8ba306ae3e0c40574df105 Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 8 Feb 2010 20:57:30 +0000 Subject: [PATCH 14/25] stop button --- .../i2p/router/web/ConfigClientsHelper.java | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java index 1779d613b2..ce4efb1439 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java @@ -35,18 +35,18 @@ public class ConfigClientsHelper extends HelperBase { public String getForm1() { StringBuilder buf = new StringBuilder(1024); buf.append("\n"); - buf.append("\n"); + buf.append("\n"); List clients = ClientAppConfig.getClientApps(_context); for (int cur = 0; cur < clients.size(); cur++) { ClientAppConfig ca = clients.get(cur); renderForm(buf, ""+cur, ca.clientName, false, !ca.disabled, "webConsole".equals(ca.clientName) || "Web console".equals(ca.clientName), - ca.className + ((ca.args != null) ? " " + ca.args : ""), (""+cur).equals(_edit), true, false); + ca.className + ((ca.args != null) ? " " + ca.args : ""), (""+cur).equals(_edit), true, false, false); } if ("new".equals(_edit)) - renderForm(buf, "" + clients.size(), "", false, false, false, "", true, false, false); + renderForm(buf, "" + clients.size(), "", false, false, false, "", true, false, false, false); buf.append("
" + _("Client") + "" + _("Run at Startup?") + "" + _("Start Now") + "" + _("Class and arguments") + "
" + _("Client") + "" + _("Run at Startup?") + "" + _("Control") + "" + _("Class and arguments") + "
\n"); return buf.toString(); } @@ -54,7 +54,7 @@ public class ConfigClientsHelper extends HelperBase { public String getForm2() { StringBuilder buf = new StringBuilder(1024); buf.append("\n"); - buf.append("\n"); + buf.append("\n"); Properties props = RouterConsoleRunner.webAppProperties(); Set keys = new TreeSet(props.keySet()); for (Iterator iter = keys.iterator(); iter.hasNext(); ) { @@ -63,7 +63,7 @@ public class ConfigClientsHelper extends HelperBase { String app = name.substring(RouterConsoleRunner.PREFIX.length(), name.lastIndexOf(RouterConsoleRunner.ENABLED)); String val = props.getProperty(name); renderForm(buf, app, app, !"addressbook".equals(app), - "true".equals(val), RouterConsoleRunner.ROUTERCONSOLE.equals(app), app + ".war", false, false, false); + "true".equals(val), RouterConsoleRunner.ROUTERCONSOLE.equals(app), app + ".war", false, false, false, false); } } buf.append("
" + _("WebApp") + "" + _("Run at Startup?") + "" + _("Start Now") + "" + _("Description") + "
" + _("WebApp") + "" + _("Run at Startup?") + "" + _("Control") + "" + _("Description") + "
\n"); @@ -73,7 +73,7 @@ public class ConfigClientsHelper extends HelperBase { public String getForm3() { StringBuilder buf = new StringBuilder(1024); buf.append("\n"); - buf.append("\n"); + buf.append("\n"); Properties props = PluginStarter.pluginProperties(); Set keys = new TreeSet(props.keySet()); for (Iterator iter = keys.iterator(); iter.hasNext(); ) { @@ -128,7 +128,8 @@ public class ConfigClientsHelper extends HelperBase { } desc.append("
" + _("Plugin") + "" + _("Run at Startup?") + "" + _("Start Now") + "" + _("Description") + "
" + _("Plugin") + "" + _("Run at Startup?") + "" + _("Control") + "" + _("Description") + "
"); renderForm(buf, app, app, false, - "true".equals(val), false, desc.toString(), false, false, updateURL != null); + "true".equals(val), false, desc.toString(), false, false, + updateURL != null, true); } } buf.append("\n"); @@ -138,7 +139,7 @@ public class ConfigClientsHelper extends HelperBase { /** ro trumps edit and showEditButton */ private void renderForm(StringBuilder buf, String index, String name, boolean urlify, boolean enabled, boolean ro, String desc, boolean edit, - boolean showEditButton, boolean showUpdateButton) { + boolean showEditButton, boolean showUpdateButton, boolean showStopButton) { buf.append(""); if (urlify && enabled) { String link = "/"; @@ -166,6 +167,8 @@ public class ConfigClientsHelper extends HelperBase { } if (showEditButton && (!edit) && !ro) buf.append(""); + if (showStopButton && (!edit)) + buf.append(""); if (showUpdateButton && (!edit) && !ro) { buf.append(""); buf.append(""); From 2df7247e83c5e5cf3154d0ca785d858989ee7b3a Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 8 Feb 2010 22:19:59 +0000 Subject: [PATCH 15/25] fix start webapp, add stop webapp --- .../src/net/i2p/router/web/PluginStarter.java | 28 +++++++++++++++++-- .../src/net/i2p/router/web/WebAppStarter.java | 14 ++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java index 0bf04dc134..0b71b52de0 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java @@ -84,13 +84,19 @@ public class PluginStarter implements Runnable { if (server != null) { File consoleDir = new File(pluginDir, "console"); Properties props = RouterConsoleRunner.webAppProperties(consoleDir.getAbsolutePath()); - File webappDir = new File(pluginDir, "webapps"); + File webappDir = new File(consoleDir, "webapps"); String fileNames[] = webappDir.list(RouterConsoleRunner.WarFilenameFilter.instance()); if (fileNames != null) { for (int i = 0; i < fileNames.length; i++) { try { String warName = fileNames[i].substring(0, fileNames[i].lastIndexOf(".war")); - // check for duplicates in $I2P ? + // check for duplicates in $I2P + // easy way for now... + if (warName.equals("i2psnark") || warName.equals("susidns") || warName.equals("i2ptunnel") || + warName.equals("susimail") || warName.equals("addressbook")) { + log.error("Skipping duplicate webapp " + warName + " in plugin " + appName); + continue; + } String enabled = props.getProperty(PREFIX + warName + ENABLED); if (! "false".equals(enabled)) { String path = new File(webappDir, fileNames[i]).getCanonicalPath(); @@ -161,6 +167,24 @@ public class PluginStarter implements Runnable { } // stop console webapps in console/webapps + Server server = getConsoleServer(); + if (server != null) { + File consoleDir = new File(pluginDir, "console"); + Properties props = RouterConsoleRunner.webAppProperties(consoleDir.getAbsolutePath()); + File webappDir = new File(consoleDir, "webapps"); + String fileNames[] = webappDir.list(RouterConsoleRunner.WarFilenameFilter.instance()); + if (fileNames != null) { + for (int i = 0; i < fileNames.length; i++) { + String warName = fileNames[i].substring(0, fileNames[i].lastIndexOf(".war")); + if (warName.equals("i2psnark") || warName.equals("susidns") || warName.equals("i2ptunnel") || + warName.equals("susimail") || warName.equals("addressbook")) { + continue; + } + WebAppStarter.stopWebApp(server, warName); + } + } + } + // remove summary bar link Properties props = pluginProperties(ctx, appName); diff --git a/apps/routerconsole/java/src/net/i2p/router/web/WebAppStarter.java b/apps/routerconsole/java/src/net/i2p/router/web/WebAppStarter.java index dafc1c10a5..3e24a38c41 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/WebAppStarter.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/WebAppStarter.java @@ -7,6 +7,7 @@ import java.util.StringTokenizer; import net.i2p.I2PAppContext; +import org.mortbay.http.HttpContext; import org.mortbay.jetty.Server; import org.mortbay.jetty.servlet.WebApplicationContext; @@ -57,4 +58,17 @@ public class WebAppStarter { wac.setConfigurationClassNames(newClassNames); return wac; } + + /** + * stop it + */ + static void stopWebApp(Server server, String appName) { + // this will return a new context if one does not exist + HttpContext wac = server.getContext('/' + appName); + try { + // false -> not graceful + wac.stop(false); + } catch (InterruptedException ie) {} + } + } From 9d1ae891bbe152a1ef1c69a30b509e02223ecaae Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 8 Feb 2010 22:50:30 +0000 Subject: [PATCH 16/25] check for mismatched versions --- .../java/src/net/i2p/router/web/PluginUpdateHandler.java | 9 ++++++++- apps/routerconsole/jsp/configclients.jsp | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java index c68346b99a..4aa6b2ab1a 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java @@ -209,6 +209,8 @@ public class PluginUpdateHandler extends UpdateHandler { return; } } + + String sudVersion = TrustedUpdate.getVersionString(f); f.delete(); String appName = props.getProperty("name"); @@ -219,6 +221,11 @@ public class PluginUpdateHandler extends UpdateHandler { updateStatus("" + _("Plugin from {0} has invalid name or version", url) + ""); return; } + if (!version.equals(sudVersion)) { + to.delete(); + updateStatus("" + _("Plugin {0} has mismatched versions", appName) + ""); + return; + } // todo compare sud version with property version @@ -242,7 +249,7 @@ public class PluginUpdateHandler extends UpdateHandler { if (destDir.exists()) { if (Boolean.valueOf(props.getProperty("install-only")).booleanValue()) { to.delete(); - updateStatus("" + _("Downloaded plugin is not for upgrading but the plugin is already installed", url) + ""); + updateStatus("" + _("Downloaded plugin is for new installs only, but the plugin is already installed", url) + ""); return; } diff --git a/apps/routerconsole/jsp/configclients.jsp b/apps/routerconsole/jsp/configclients.jsp index 0fcb2e2e97..6d52901b8b 100644 --- a/apps/routerconsole/jsp/configclients.jsp +++ b/apps/routerconsole/jsp/configclients.jsp @@ -55,7 +55,7 @@ button span.hide{


" />

<%=intl._("Plugin Configuration")%>

- <%=intl._("The plugins listed below are started by the webConsole client and run in the same JVM as the router. They are usually web applications accessible through the router console.")%> + <%=intl._("The plugins listed below are started by the webConsole client.")%>


From a820c01ba51804062af3ef50a56405f4ab246832 Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 8 Feb 2010 23:28:09 +0000 Subject: [PATCH 17/25] strip HTML from fields --- .../i2p/router/web/ConfigClientsHelper.java | 41 ++++++++++++++----- .../src/net/i2p/router/web/PluginStarter.java | 10 ++--- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java index ce4efb1439..2f38774591 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java @@ -84,10 +84,15 @@ public class ConfigClientsHelper extends HelperBase { Properties appProps = PluginStarter.pluginProperties(_context, app); StringBuilder desc = new StringBuilder(256); desc.append("") - .append("\n"); } + + /** + * Like in DataHelper but doesn't convert null to "" + * There's a lot worse things a plugin could do but... + */ + static String stripHTML(Properties props, String key) { + String orig = props.getProperty(key); + if (orig == null) return null; + String t1 = orig.replace('<', ' '); + String rv = t1.replace('>', ' '); + return rv; + } } diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java index 0b71b52de0..25d64ca30e 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java @@ -138,10 +138,10 @@ public class PluginStarter implements Runnable { // add summary bar link Properties props = pluginProperties(ctx, appName); - String name = props.getProperty("consoleLinkName_" + Messages.getLanguage(ctx)); + String name = ConfigClientsHelper.stripHTML(props, "consoleLinkName_" + Messages.getLanguage(ctx)); if (name == null) - name = props.getProperty("consoleLinkName"); - String url = props.getProperty("consoleLinkURL"); + name = ConfigClientsHelper.stripHTML(props, "consoleLinkName"); + String url = ConfigClientsHelper.stripHTML(props, "consoleLinkURL"); if (name != null && url != null && name.length() > 0 && url.length() > 0) NavHelper.registerApp(name, url); @@ -188,9 +188,9 @@ public class PluginStarter implements Runnable { // remove summary bar link Properties props = pluginProperties(ctx, appName); - String name = props.getProperty("consoleLinkName_" + Messages.getLanguage(ctx)); + String name = ConfigClientsHelper.stripHTML(props, "consoleLinkName_" + Messages.getLanguage(ctx)); if (name == null) - name = props.getProperty("consoleLinkName"); + name = ConfigClientsHelper.stripHTML(props, "consoleLinkName"); if (name != null && name.length() > 0) NavHelper.unregisterApp(name); From 54171e4be20051a44f50d5b88ea9356a9fe31c2f Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 8 Feb 2010 23:37:31 +0000 Subject: [PATCH 18/25] ... and more --- .../src/net/i2p/router/web/PluginUpdateHandler.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java index 4aa6b2ab1a..83b8d125bc 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java @@ -216,7 +216,9 @@ public class PluginUpdateHandler extends UpdateHandler { 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) { + appName.indexOf("<") >= 0 || appName.indexOf(">") >= 0 || + version.indexOf("<") >= 0 || version.indexOf(">") >= 0 || + appName.startsWith(".") || appName.indexOf("/") >= 0 || appName.indexOf("\\") >= 0) { to.delete(); updateStatus("" + _("Plugin from {0} has invalid name or version", url) + ""); return; @@ -229,7 +231,7 @@ public class PluginUpdateHandler extends UpdateHandler { // todo compare sud version with property version - String minVersion = props.getProperty("min-i2p-version"); + String minVersion = ConfigClientsHelper.stripHTML(props, "min-i2p-version"); if (minVersion != null && (new VersionComparator()).compare(CoreVersion.VERSION, minVersion) < 0) { to.delete(); @@ -237,7 +239,7 @@ public class PluginUpdateHandler extends UpdateHandler { return; } - minVersion = props.getProperty("min-java-version"); + minVersion = ConfigClientsHelper.stripHTML(props, "min-java-version"); if (minVersion != null && (new VersionComparator()).compare(System.getProperty("java.version"), minVersion) < 0) { to.delete(); @@ -279,14 +281,14 @@ public class PluginUpdateHandler extends UpdateHandler { updateStatus("" + _("New plugin version {0} is not newer than installed plugin", version) + ""); return; } - minVersion = props.getProperty("min-installed-version"); + minVersion = ConfigClientsHelper.stripHTML(props, "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"); + String maxVersion = ConfigClientsHelper.stripHTML(props, "max-installed-version"); if (maxVersion != null && (new VersionComparator()).compare(maxVersion, oldVersion) < 0) { to.delete(); From 880f1866dc53436b96e6b8ed17288a3a60df20b7 Mon Sep 17 00:00:00 2001 From: zzz Date: Wed, 10 Feb 2010 12:06:30 +0000 Subject: [PATCH 19/25] take version number off jrobin jar so we can overwrite if we upgrade --- apps/routerconsole/java/build.xml | 3 ++- build.xml | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/routerconsole/java/build.xml b/apps/routerconsole/java/build.xml index 95568170dc..78384113f9 100644 --- a/apps/routerconsole/java/build.xml +++ b/apps/routerconsole/java/build.xml @@ -64,7 +64,8 @@ - + + diff --git a/build.xml b/build.xml index f56f141785..6d3195443b 100644 --- a/build.xml +++ b/build.xml @@ -314,8 +314,8 @@ - - + + @@ -490,7 +490,8 @@ - + + From cfc49ab2610f3f5de22bff76acb8c8f4ba6785ac Mon Sep 17 00:00:00 2001 From: zzz Date: Wed, 10 Feb 2010 15:35:00 +0000 Subject: [PATCH 20/25] * Plugins: - Check plugin key against all installed plugins - Reword some error messages * VersionComparator: add '-' and '_' as valid separators to better support plugin and java version checking --- .../src/net/i2p/router/web/PluginStarter.java | 41 +++++++++++++++++-- .../i2p/router/web/PluginUpdateHandler.java | 29 ++++++++----- .../src/net/i2p/util/VersionComparator.java | 10 +++-- 3 files changed, 62 insertions(+), 18 deletions(-) diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java index 25d64ca30e..d24e28d00d 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java @@ -5,9 +5,12 @@ import java.io.IOException; import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; +import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Properties; import java.util.StringTokenizer; @@ -220,15 +223,45 @@ public class PluginStarter implements Runnable { DataHelper.loadProps(rv, cfgFile); } catch (IOException ioe) {} + List names = getPlugins(); + for (String name : names) { + String prop = PREFIX + name + ENABLED; + if (rv.getProperty(prop) == null) + rv.setProperty(prop, "true"); + } + return rv; + } + + /** + * all installed plugins whether enabled or not + */ + public static List getPlugins() { + List rv = new ArrayList(); File pluginDir = new File(I2PAppContext.getGlobalContext().getAppDir(), PluginUpdateHandler.PLUGIN_DIR); File[] files = pluginDir.listFiles(); if (files == null) return rv; for (int i = 0; i < files.length; i++) { - String name = files[i].getName(); - String prop = PREFIX + name + ENABLED; - if (files[i].isDirectory() && rv.getProperty(prop) == null) - rv.setProperty(prop, "true"); + if (files[i].isDirectory()) + rv.add(files[i].getName()); + } + return rv; + } + + /** + * The signing keys from all the plugins + * @return Map of key to keyname + * Last one wins if a dup (installer should prevent dups) + */ + public static Map getPluginKeys(I2PAppContext ctx) { + Map rv = new HashMap(); + List names = getPlugins(); + for (String name : names) { + Properties props = pluginProperties(ctx, name); + String pubkey = props.getProperty("key"); + String keyName = props.getProperty("keyName"); + if (pubkey != null && keyName != null && pubkey.length() == 172 && keyName.length() > 0) + rv.put(pubkey, keyName); } return rv; } diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java index 83b8d125bc..60df3f89ec 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java @@ -2,6 +2,7 @@ package net.i2p.router.web; import java.io.File; import java.io.IOException; +import java.util.Map; import java.util.Properties; import net.i2p.CoreVersion; @@ -184,6 +185,14 @@ public class PluginUpdateHandler extends UpdateHandler { return; } + // add all existing plugin keys, so any conflicts with existing keys + // will be discovered and rejected + Map existingKeys = PluginStarter.getPluginKeys(_context); + for (Map.Entry e : existingKeys.entrySet()) { + // ignore dups/bad keys + up.addKey(e.getKey(), e.getValue()); + } + if (up.haveKey(pubkey)) { // the key is already in the TrustedUpdate keyring if (!up.verify(f)) { @@ -285,14 +294,14 @@ public class PluginUpdateHandler extends UpdateHandler { if (minVersion != null && (new VersionComparator()).compare(minVersion, oldVersion) > 0) { to.delete(); - updateStatus("" + _("Plugin update requires installed version {0} or higher", minVersion) + ""); + updateStatus("" + _("Plugin update requires installed plugin version {0} or higher", minVersion) + ""); return; } String maxVersion = ConfigClientsHelper.stripHTML(props, "max-installed-version"); if (maxVersion != null && (new VersionComparator()).compare(maxVersion, oldVersion) < 0) { to.delete(); - updateStatus("" + _("Plugin update requires installed version {0} or lower", maxVersion) + ""); + updateStatus("" + _("Plugin update requires installed plugin version {0} or lower", maxVersion) + ""); return; } @@ -306,7 +315,7 @@ public class PluginUpdateHandler extends UpdateHandler { } else { if (Boolean.valueOf(props.getProperty("update-only")).booleanValue()) { to.delete(); - updateStatus("" + _("Plugin is for upgrades only, but the plugin is not installed", url) + ""); + updateStatus("" + _("Plugin is for upgrades only, but the plugin is not installed") + ""); return; } if (!destDir.mkdir()) { @@ -319,16 +328,16 @@ public class PluginUpdateHandler extends UpdateHandler { // 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()) + ""); + updateStatus("" + _("Failed to install plugin in {0}", destDir.getAbsolutePath()) + ""); return; } to.delete(); if (Boolean.valueOf(props.getProperty("dont-start-at-install")).booleanValue()) { if (Boolean.valueOf(props.getProperty("router-restart-required")).booleanValue()) - updateStatus("" + _("Plugin {0} successfully installed, router restart required", appName) + ""); + updateStatus("" + _("Plugin {0} installed, router restart required", appName) + ""); else { - updateStatus("" + _("Plugin {0} successfully installed", appName) + ""); + updateStatus("" + _("Plugin {0} installed", appName) + ""); Properties pluginProps = PluginStarter.pluginProperties(); pluginProps.setProperty(PluginStarter.PREFIX + appName + PluginStarter.ENABLED, "false"); PluginStarter.storePluginProperties(pluginProps); @@ -337,11 +346,11 @@ public class PluginUpdateHandler extends UpdateHandler { // start everything try { if (PluginStarter.startPlugin(_context, appName)) - updateStatus("" + _("Plugin {0} started", appName) + ""); + updateStatus("" + _("Plugin {0} installed and started", appName) + ""); else - updateStatus("" + _("Failed to start plugin {0}, check logs", appName) + ""); + updateStatus("" + _("Plugin {0} installed but failed to start, check logs", appName) + ""); } catch (Exception e) { - updateStatus("" + _("Failed to start plugin {0}:", appName) + ' ' + e + ""); + updateStatus("" + _("Plugin {0} installed but failed to start", appName) + ": " + e + ""); } } } @@ -350,7 +359,7 @@ public class PluginUpdateHandler extends UpdateHandler { 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) + ""); + updateStatus("" + _("Failed to download plugin from {0}", url) + ""); } } diff --git a/core/java/src/net/i2p/util/VersionComparator.java b/core/java/src/net/i2p/util/VersionComparator.java index 3b643442ef..4c4400f3de 100644 --- a/core/java/src/net/i2p/util/VersionComparator.java +++ b/core/java/src/net/i2p/util/VersionComparator.java @@ -5,7 +5,8 @@ import java.util.StringTokenizer; /** * Compares versions. - * Characters other than [0-9.] are ignored. + * Characters other than [0-9.-_] are ignored. + * I2P only uses '.' but Sun Java uses '_' and plugins may use any of '.-_' * Moved from TrustedUpdate.java * @since 0.7.10 */ @@ -15,8 +16,8 @@ public class VersionComparator implements Comparator { // try it the easy way first if (l.equals(r)) return 0; - StringTokenizer lTokens = new StringTokenizer(sanitize(l), "."); - StringTokenizer rTokens = new StringTokenizer(sanitize(r), "."); + StringTokenizer lTokens = new StringTokenizer(sanitize(l), VALID_SEPARATOR_CHARS); + StringTokenizer rTokens = new StringTokenizer(sanitize(r), VALID_SEPARATOR_CHARS); while (lTokens.hasMoreTokens() && rTokens.hasMoreTokens()) { String lNumber = lTokens.nextToken(); @@ -48,7 +49,8 @@ public class VersionComparator implements Comparator { return left - right; } - private static final String VALID_VERSION_CHARS = "0123456789."; + private static final String VALID_SEPARATOR_CHARS = ".-_"; + private static final String VALID_VERSION_CHARS = "0123456789" + VALID_SEPARATOR_CHARS; private static final String sanitize(String versionString) { StringBuilder versionStringBuilder = new StringBuilder(versionString); From 949aea951e4fada832750a16ee26f28c2d6f2f41 Mon Sep 17 00:00:00 2001 From: zzz Date: Wed, 10 Feb 2010 19:09:35 +0000 Subject: [PATCH 21/25] * Plugins: - Hook up update/delete/check/save buttons - Implement delete - Hide unless router.enablePlugins=true --- .../i2p/router/web/ConfigClientsHandler.java | 90 ++++++++++++++++++- .../i2p/router/web/ConfigClientsHelper.java | 6 +- .../src/net/i2p/router/web/PluginStarter.java | 29 +++++- .../i2p/router/web/PluginUpdateChecker.java | 11 ++- .../i2p/router/web/PluginUpdateHandler.java | 4 +- .../i2p/router/web/RouterConsoleRunner.java | 6 +- apps/routerconsole/jsp/configclients.jsp | 10 ++- 7 files changed, 136 insertions(+), 20 deletions(-) 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 6d0a4faa9f..1f433b60f4 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java @@ -35,6 +35,10 @@ public class ConfigClientsHandler extends FormHandler { saveWebAppChanges(); return; } + if (_action.equals(_("Save Plugin Configuration"))) { + savePluginChanges(); + return; + } if (_action.equals(_("Install Plugin"))) { installPlugin(); return; @@ -60,10 +64,42 @@ public class ConfigClientsHandler extends FormHandler { try { appnum = Integer.parseInt(app); } catch (NumberFormatException nfe) {} - if (appnum >= 0) + if (appnum >= 0) { deleteClient(appnum); + } else { + try { + PluginStarter.stopPlugin(_context, app); + } catch (Exception e) {} + PluginStarter.deletePlugin(_context, app); + addFormNotice(_("Deleted plugin {0}", app)); + } return; } + + // value + if (_action.startsWith("Stop ")) { + String app = _action.substring(5); + try { + PluginStarter.stopPlugin(_context, app); + } catch (Exception e) {} + addFormNotice(_("Stopped plugin {0}", app)); + return; + } + + // value + if (_action.startsWith("Update ")) { + String app = _action.substring(7); + updatePlugin(app); + return; + } + + // value + if (_action.startsWith("Check ")) { + String app = _action.substring(6); + checkPlugin(app); + return; + } + // label (IE) String xStart = _("Start"); if (_action.toLowerCase().startsWith(xStart + " ") && @@ -81,6 +117,7 @@ public class ConfigClientsHandler extends FormHandler { } else { addFormError(_("Unsupported") + ' ' + _action + '.'); } + } public void setSettings(Map settings) { _settings = new HashMap(settings); } @@ -175,7 +212,23 @@ public class ConfigClientsHandler extends FormHandler { props.setProperty(name, "" + (val != null)); } RouterConsoleRunner.storeWebAppProperties(props); - addFormNotice(_("WebApp configuration saved successfully - restart required to take effect.")); + addFormNotice(_("WebApp configuration saved.")); + } + + private void savePluginChanges() { + Properties props = PluginStarter.pluginProperties(); + Set keys = props.keySet(); + int cur = 0; + for (Iterator iter = keys.iterator(); iter.hasNext(); ) { + String name = (String)iter.next(); + if (! (name.startsWith(PluginStarter.PREFIX) && name.endsWith(PluginStarter.ENABLED))) + continue; + String app = name.substring(PluginStarter.PREFIX.length(), name.lastIndexOf(PluginStarter.ENABLED)); + Object val = _settings.get(app + ".enabled"); + props.setProperty(name, "" + (val != null)); + } + PluginStarter.storePluginProperties(props); + addFormNotice(_("Plugin configuration saved.")); } /** @@ -205,6 +258,20 @@ public class ConfigClientsHandler extends FormHandler { addFormError(_("No plugin URL specified.")); return; } + installPlugin(url); + } + + private void updatePlugin(String app) { + Properties props = PluginStarter.pluginProperties(_context, app); + String url = props.getProperty("updateURL"); + if (url == null) { + addFormError(_("No update URL specified for {0}",app)); + return; + } + installPlugin(url); + } + + private void installPlugin(String url) { if ("true".equals(System.getProperty(UpdateHandler.PROP_UPDATE_IN_PROGRESS))) { addFormError(_("Plugin or update download already in progress.")); return; @@ -221,4 +288,23 @@ public class ConfigClientsHandler extends FormHandler { Thread.sleep(1000); } catch (InterruptedException ie) {} } + + private void checkPlugin(String app) { + if ("true".equals(System.getProperty(UpdateHandler.PROP_UPDATE_IN_PROGRESS))) { + addFormError(_("Plugin or update download already in progress.")); + return; + } + PluginUpdateChecker puc = PluginUpdateChecker.getInstance(_context); + if (puc.isRunning()) { + addFormError(_("Plugin or update download already in progress.")); + return; + } + puc.update(app); + addFormNotice(_("Checking plugin {0} for updates", app)); + // 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/ConfigClientsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java index 2f38774591..d0daac4b2e 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java @@ -70,6 +70,10 @@ public class ConfigClientsHelper extends HelperBase { return buf.toString(); } + public boolean showPlugins() { + return PluginStarter.pluginsEnabled(_context); + } + public String getForm3() { StringBuilder buf = new StringBuilder(1024); buf.append("
").append(_("Version")).append("").append(appProps.getProperty("version")) + .append("
").append(_("Version")).append("").append(stripHTML(appProps, "version")) .append("
") - .append(_("Signed by")).append("").append(appProps.getProperty("keyName")); - String s = appProps.getProperty("date"); + .append(_("Signed by")).append(""); + String s = stripHTML(appProps, "keyName"); + if (s.indexOf("@") > 0) + desc.append("").append(s).append(""); + else + desc.append(s); + s = stripHTML(appProps, "date"); if (s != null) { long ms = 0; try { @@ -99,29 +104,33 @@ public class ConfigClientsHelper extends HelperBase { .append(_("Date")).append("").append(date); } } - s = appProps.getProperty("author"); + s = stripHTML(appProps, "author"); if (s != null) { desc.append("
") - .append(_("Author")).append("").append(s); + .append(_("Author")).append(""); + if (s.indexOf("@") > 0) + desc.append("").append(s).append(""); + else + desc.append(s); } - s = appProps.getProperty("description_" + Messages.getLanguage(_context)); + s = stripHTML(appProps, "description_" + Messages.getLanguage(_context)); if (s == null) - s = appProps.getProperty("description"); + s = stripHTML(appProps, "description"); if (s != null) { desc.append("
") .append(_("Description")).append("").append(s); } - s = appProps.getProperty("license"); + s = stripHTML(appProps, "license"); if (s != null) { desc.append("
") .append(_("License")).append("").append(s); } - s = appProps.getProperty("websiteURL"); + s = stripHTML(appProps, "websiteURL"); if (s != null) { desc.append("
") .append("").append(_("Website")).append(" "); } - String updateURL = appProps.getProperty("updateURL"); + String updateURL = stripHTML(appProps, "updateURL"); if (updateURL != null) { desc.append("
") .append("").append(_("Update link")).append(" "); @@ -185,4 +194,16 @@ public class ConfigClientsHelper extends HelperBase { } buf.append("
\n"); @@ -170,7 +174,7 @@ public class ConfigClientsHelper extends HelperBase { if (ro) buf.append("disabled=\"true\" "); } - buf.append("/>
"); + buf.append(">"); if ((!enabled) && !edit) { buf.append(""); } diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java index d24e28d00d..5b92ec0c3b 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java @@ -19,6 +19,7 @@ import net.i2p.data.DataHelper; import net.i2p.router.RouterContext; import net.i2p.router.startup.ClientAppConfig; import net.i2p.router.startup.LoadClientAppsJob; +import net.i2p.util.FileUtil; import net.i2p.util.Log; import net.i2p.util.Translate; @@ -41,6 +42,10 @@ public class PluginStarter implements Runnable { _context = ctx; } + static boolean pluginsEnabled(I2PAppContext ctx) { + return Boolean.valueOf(ctx.getProperty("router.enablePlugins")).booleanValue(); + } + public void run() { startPlugins(_context); } @@ -50,9 +55,9 @@ public class PluginStarter implements Runnable { Properties props = pluginProperties(); for (Iterator iter = props.keySet().iterator(); iter.hasNext(); ) { String name = (String)iter.next(); - if (name.startsWith(PluginStarter.PREFIX) && name.endsWith(PluginStarter.ENABLED)) { + if (name.startsWith(PREFIX) && name.endsWith(ENABLED)) { if (Boolean.valueOf(props.getProperty(name)).booleanValue()) { - String app = name.substring(PluginStarter.PREFIX.length(), name.lastIndexOf(PluginStarter.ENABLED)); + String app = name.substring(PREFIX.length(), name.lastIndexOf(ENABLED)); try { if (!startPlugin(ctx, app)) log.error("Failed to start plugin: " + app); @@ -188,7 +193,6 @@ public class PluginStarter implements Runnable { } } - // remove summary bar link Properties props = pluginProperties(ctx, appName); String name = ConfigClientsHelper.stripHTML(props, "consoleLinkName_" + Messages.getLanguage(ctx)); @@ -200,6 +204,25 @@ public class PluginStarter implements Runnable { return true; } + /** @return true on success - call stopPlugin() first */ + static boolean deletePlugin(RouterContext ctx, String appName) { + Log log = ctx.logManager().getLog(PluginStarter.class); + File pluginDir = new File(ctx.getAppDir(), PluginUpdateHandler.PLUGIN_DIR + '/' + appName); + if ((!pluginDir.exists()) || (!pluginDir.isDirectory())) { + log.error("Cannot stop nonexistent plugin: " + appName); + return false; + } + FileUtil.rmdir(pluginDir, false); + Properties props = pluginProperties(); + for (Iterator iter = props.keySet().iterator(); iter.hasNext(); ) { + String name = (String)iter.next(); + if (name.startsWith(PREFIX + appName)) + iter.remove(); + } + storePluginProperties(props); + return true; + } + /** plugin.config */ public static Properties pluginProperties(I2PAppContext ctx, String appName) { File cfgFile = new File(ctx.getAppDir(), PluginUpdateHandler.PLUGIN_DIR + '/' + appName + '/' + "plugin.config"); diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateChecker.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateChecker.java index e19e23d91f..8b5d2c0147 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateChecker.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateChecker.java @@ -62,14 +62,15 @@ public class PluginUpdateChecker extends UpdateHandler { String oldVersion = props.getProperty("version"); String xpi2pURL = props.getProperty("updateURL"); if (oldVersion == null || xpi2pURL == null) { - updateStatus("" + _("Cannot update, plugin {0} is not installed", appName) + ""); + updateStatus("" + _("Cannot check, plugin {0} is not installed", appName) + ""); return; } if (_pluginUpdateCheckerRunner == null) - _pluginUpdateCheckerRunner = new PluginUpdateCheckerRunner(xpi2pURL); + _pluginUpdateCheckerRunner = new PluginUpdateCheckerRunner(); if (_pluginUpdateCheckerRunner.isRunning()) return; + _xpi2pURL = xpi2pURL; _appName = appName; _oldVersion = oldVersion; System.setProperty(PROP_UPDATE_IN_PROGRESS, "true"); @@ -89,18 +90,16 @@ public class PluginUpdateChecker extends UpdateHandler { } public class PluginUpdateCheckerRunner extends UpdateRunner implements Runnable, EepGet.StatusListener { - String _updateURL; ByteArrayOutputStream _baos; - public PluginUpdateCheckerRunner(String url) { + public PluginUpdateCheckerRunner() { super(); - _updateURL = url; _baos = new ByteArrayOutputStream(TrustedUpdate.HEADER_BYTES); } @Override protected void update() { - updateStatus("" + _("Checking plugin {0} for updates", _appName) + ""); + updateStatus("" + _("Checking for update of plugin {0}", _appName) + ""); // 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); diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java index 60df3f89ec..8e0ba021f6 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java @@ -90,11 +90,9 @@ public class PluginUpdateHandler extends UpdateHandler { } public class PluginUpdateRunner extends UpdateRunner implements Runnable, EepGet.StatusListener { - String _updateURL; public PluginUpdateRunner(String url) { super(); - _updateURL = url; } @Override @@ -287,7 +285,7 @@ public class PluginUpdateHandler extends UpdateHandler { if (oldVersion == null || (new VersionComparator()).compare(oldVersion, version) >= 0) { to.delete(); - updateStatus("" + _("New plugin version {0} is not newer than installed plugin", version) + ""); + updateStatus("" + _("Downloaded plugin version {0} is not newer than installed plugin", version) + ""); return; } minVersion = ConfigClientsHelper.stripHTML(props, "min-installed-version"); 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 07d3c2ed75..f4f7bd79e0 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java @@ -189,8 +189,10 @@ public class RouterConsoleRunner { List contexts = RouterContext.listContexts(); if (contexts != null) { - t = new I2PAppThread(new PluginStarter(contexts.get(0)), "PluginStarter", true); - t.start(); + if (PluginStarter.pluginsEnabled(contexts.get(0))) { + t = new I2PAppThread(new PluginStarter(contexts.get(0)), "PluginStarter", true); + t.start(); + } } } diff --git a/apps/routerconsole/jsp/configclients.jsp b/apps/routerconsole/jsp/configclients.jsp index 6d52901b8b..4bb17007c6 100644 --- a/apps/routerconsole/jsp/configclients.jsp +++ b/apps/routerconsole/jsp/configclients.jsp @@ -54,16 +54,20 @@ button span.hide{ <%=intl._("All changes require restart to take effect.")%>


" /> -

<%=intl._("Plugin Configuration")%>

+ +<% if (clientshelper.showPlugins()) { %> +

<%=intl._("Plugin Configuration")%>

<%=intl._("The plugins listed below are started by the webConsole client.")%>


" />

<%=intl._("Plugin Installation")%>

- <%=intl._("To install a plugin, enter the URL to download the plugin from:")%> + <%=intl._("To install a plugin, enter the download URL:")%>


" /> -
+ +<% } %> + From cada9fae44759c8170a3a951d11c65288e12b76a Mon Sep 17 00:00:00 2001 From: zzz Date: Wed, 10 Feb 2010 20:13:07 +0000 Subject: [PATCH 22/25] flush requests in I2PTunelRunner --- apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelRunner.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelRunner.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelRunner.java index 446ad55630..f408b60154 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelRunner.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelRunner.java @@ -125,7 +125,7 @@ public class I2PTunnelRunner extends I2PAppThread implements I2PSocket.SocketErr if (initialI2PData != null) { synchronized (slock) { i2pout.write(initialI2PData); - //i2pout.flush(); + i2pout.flush(); } } if (initialSocketData != null) { From 62308f26bc7a3182a265887a3154b5e07d3775ea Mon Sep 17 00:00:00 2001 From: zzz Date: Thu, 11 Feb 2010 19:18:26 +0000 Subject: [PATCH 23/25] * Plugins: - Fix classpath setting for webapps - Implement uninstall args in clients.config --- .../i2p/router/web/ConfigClientsHandler.java | 11 +++-- .../src/net/i2p/router/web/PluginStarter.java | 44 ++++++++++++++----- .../i2p/router/web/PluginUpdateHandler.java | 2 +- .../i2p/router/web/WebAppConfiguration.java | 15 ++++--- .../i2p/router/startup/ClientAppConfig.java | 9 +++- 5 files changed, 57 insertions(+), 24 deletions(-) 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 1f433b60f4..a496cd3999 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java @@ -1,6 +1,7 @@ package net.i2p.router.web; import java.io.File; +import java.io.IOException; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -69,9 +70,11 @@ public class ConfigClientsHandler extends FormHandler { } else { try { PluginStarter.stopPlugin(_context, app); - } catch (Exception e) {} - PluginStarter.deletePlugin(_context, app); - addFormNotice(_("Deleted plugin {0}", app)); + PluginStarter.deletePlugin(_context, app); + addFormNotice(_("Deleted plugin {0}", app)); + } catch (IOException e) { + addFormError(_("Error deleting plugin {0}", app) + ": " + e); + } } return; } @@ -81,7 +84,7 @@ public class ConfigClientsHandler extends FormHandler { String app = _action.substring(5); try { PluginStarter.stopPlugin(_context, app); - } catch (Exception e) {} + } catch (IOException e) {} addFormNotice(_("Stopped plugin {0}", app)); return; } diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java index 5b92ec0c3b..c6dd398cbb 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java @@ -77,6 +77,7 @@ public class PluginStarter implements Runnable { log.error("Cannot start nonexistent plugin: " + appName); return false; } + //log.error("Starting plugin: " + appName); // load and start things in clients.config File clientConfig = new File(pluginDir, "clients.config"); @@ -84,7 +85,7 @@ public class PluginStarter implements Runnable { Properties props = new Properties(); DataHelper.loadProps(props, clientConfig); List clients = ClientAppConfig.getClientApps(clientConfig); - runClientApps(ctx, pluginDir, clients, true); + runClientApps(ctx, pluginDir, clients, "start"); } // start console webapps in console/webapps @@ -98,6 +99,7 @@ public class PluginStarter implements Runnable { for (int i = 0; i < fileNames.length; i++) { try { String warName = fileNames[i].substring(0, fileNames[i].lastIndexOf(".war")); + //log.error("Found webapp: " + warName); // check for duplicates in $I2P // easy way for now... if (warName.equals("i2psnark") || warName.equals("susidns") || warName.equals("i2ptunnel") || @@ -107,6 +109,7 @@ public class PluginStarter implements Runnable { } String enabled = props.getProperty(PREFIX + warName + ENABLED); if (! "false".equals(enabled)) { + //log.error("Starting webapp: " + warName); String path = new File(webappDir, fileNames[i]).getCanonicalPath(); WebAppStarter.startWebApp(ctx, server, warName, path); } @@ -157,7 +160,7 @@ public class PluginStarter implements Runnable { } /** @return true on success */ - static boolean stopPlugin(RouterContext ctx, String appName) throws Exception { + static boolean stopPlugin(RouterContext ctx, String appName) throws IOException { Log log = ctx.logManager().getLog(PluginStarter.class); File pluginDir = new File(ctx.getAppDir(), PluginUpdateHandler.PLUGIN_DIR + '/' + appName); if ((!pluginDir.exists()) || (!pluginDir.isDirectory())) { @@ -171,7 +174,7 @@ public class PluginStarter implements Runnable { Properties props = new Properties(); DataHelper.loadProps(props, clientConfig); List clients = ClientAppConfig.getClientApps(clientConfig); - runClientApps(ctx, pluginDir, clients, false); + runClientApps(ctx, pluginDir, clients, "stop"); } // stop console webapps in console/webapps @@ -201,17 +204,26 @@ public class PluginStarter implements Runnable { if (name != null && name.length() > 0) NavHelper.unregisterApp(name); + log.error("Stopping plugin: " + appName); return true; } /** @return true on success - call stopPlugin() first */ - static boolean deletePlugin(RouterContext ctx, String appName) { + static boolean deletePlugin(RouterContext ctx, String appName) throws IOException { Log log = ctx.logManager().getLog(PluginStarter.class); File pluginDir = new File(ctx.getAppDir(), PluginUpdateHandler.PLUGIN_DIR + '/' + appName); if ((!pluginDir.exists()) || (!pluginDir.isDirectory())) { log.error("Cannot stop nonexistent plugin: " + appName); return false; } + // uninstall things in clients.config + File clientConfig = new File(pluginDir, "clients.config"); + if (clientConfig.exists()) { + Properties props = new Properties(); + DataHelper.loadProps(props, clientConfig); + List clients = ClientAppConfig.getClientApps(clientConfig); + runClientApps(ctx, pluginDir, clients, "uninstall"); + } FileUtil.rmdir(pluginDir, false); Properties props = pluginProperties(); for (Iterator iter = props.keySet().iterator(); iter.hasNext(); ) { @@ -313,20 +325,28 @@ public class PluginStarter implements Runnable { return null; } - /** @param start true=start, false=stop */ - private static void runClientApps(RouterContext ctx, File pluginDir, List apps, boolean start) { + /** @param action "start" or "stop" or "uninstall" */ + private static void runClientApps(RouterContext ctx, File pluginDir, List apps, String action) { Log log = ctx.logManager().getLog(PluginStarter.class); for(ClientAppConfig app : apps) { - if (start && app.disabled) + if (action.equals("start") && app.disabled) continue; String argVal[]; - if (start) { + if (action.equals("start")) { + // start argVal = LoadClientAppsJob.parseArgs(app.args); } else { - // stopargs must be present - if (app.stopargs == null || app.stopargs.length() <= 0) + String args; + if (action.equals("stop")) + args = app.stopargs; + else if (action.equals("uninstall")) + args = app.uninstallargs; + else + throw new IllegalArgumentException("bad action"); + // args must be present + if (args == null || args.length() <= 0) continue; - argVal = LoadClientAppsJob.parseArgs(app.stopargs); + argVal = LoadClientAppsJob.parseArgs(args); } // do this after parsing so we don't need to worry about quoting for (int i = 0; i < argVal.length; i++) { @@ -345,7 +365,7 @@ public class PluginStarter implements Runnable { } addToClasspath(cp, app.clientName, log); } - if (app.delay == 0 || !start) { + if (app.delay == 0 || !action.equals("start")) { // run this guy now LoadClientAppsJob.runClient(app.className, app.clientName, argVal, log); } else { diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java index 8e0ba021f6..903000fbd7 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java @@ -308,7 +308,7 @@ public class PluginUpdateHandler extends UpdateHandler { if (!PluginStarter.stopPlugin(_context, appName)) { // failed, ignore } - } catch (Exception e) {} // ignore + } catch (IOException e) {} // ignore } else { if (Boolean.valueOf(props.getProperty("update-only")).booleanValue()) { diff --git a/apps/routerconsole/java/src/net/i2p/router/web/WebAppConfiguration.java b/apps/routerconsole/java/src/net/i2p/router/web/WebAppConfiguration.java index d903d83caf..121145bf3c 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/WebAppConfiguration.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/WebAppConfiguration.java @@ -53,8 +53,10 @@ public class WebAppConfiguration implements WebApplicationContext.Configuration I2PAppContext i2pContext = I2PAppContext.getGlobalContext(); File libDir = new File(i2pContext.getBaseDir(), "lib"); - File pluginLibDir = new File(i2pContext.getAppDir(), - PluginUpdateHandler.PLUGIN_DIR + ctxPath + '/' + "lib"); + // FIXME this only works if war is the same name as the plugin + File pluginDir = new File(i2pContext.getAppDir(), + PluginUpdateHandler.PLUGIN_DIR + ctxPath); + File dir = libDir; String cp; if (ctxPath.equals("/susidns")) { @@ -63,10 +65,11 @@ public class WebAppConfiguration implements WebApplicationContext.Configuration } 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()); + } else if (pluginDir.exists()) { + File consoleDir = new File(pluginDir, "console"); + Properties props = RouterConsoleRunner.webAppProperties(consoleDir.getAbsolutePath()); cp = props.getProperty(RouterConsoleRunner.PREFIX + appName + CLASSPATH); - dir = pluginLibDir; + dir = pluginDir; } else { Properties props = RouterConsoleRunner.webAppProperties(); cp = props.getProperty(RouterConsoleRunner.PREFIX + appName + CLASSPATH); @@ -79,6 +82,8 @@ public class WebAppConfiguration implements WebApplicationContext.Configuration String path; if (elem.startsWith("$I2P")) path = i2pContext.getBaseDir().getAbsolutePath() + '/' + elem.substring(4); + else if (elem.startsWith("$PLUGIN")) + path = dir.getAbsolutePath() + '/' + elem.substring(7); else path = dir.getAbsolutePath() + '/' + elem; System.err.println("Adding " + path + " to classpath for " + appName); diff --git a/router/java/src/net/i2p/router/startup/ClientAppConfig.java b/router/java/src/net/i2p/router/startup/ClientAppConfig.java index 3d57699a33..4c5125bc2c 100644 --- a/router/java/src/net/i2p/router/startup/ClientAppConfig.java +++ b/router/java/src/net/i2p/router/startup/ClientAppConfig.java @@ -37,6 +37,8 @@ public class ClientAppConfig { public String classpath; /** @since 0.7.12 */ public String stopargs; + /** @since 0.7.12 */ + public String uninstallargs; public ClientAppConfig(String cl, String client, String a, long d, boolean dis) { className = cl; @@ -47,10 +49,11 @@ public class ClientAppConfig { } /** @since 0.7.12 */ - public ClientAppConfig(String cl, String client, String a, long d, boolean dis, String cp, String sa) { + public ClientAppConfig(String cl, String client, String a, long d, boolean dis, String cp, String sa, String ua) { this(cl, client, a, d, dis); classpath = cp; stopargs = sa; + uninstallargs = ua; } public static File configFile(I2PAppContext ctx) { @@ -118,6 +121,7 @@ public class ClientAppConfig { String disabled = clientApps.getProperty(PREFIX + i + ".startOnLoad"); String classpath = clientApps.getProperty(PREFIX + i + ".classpath"); String stopargs = clientApps.getProperty(PREFIX + i + ".stopargs"); + String uninstallargs = clientApps.getProperty(PREFIX + i + ".uninstallargs"); i++; boolean dis = disabled != null && "false".equals(disabled); @@ -129,7 +133,8 @@ public class ClientAppConfig { if (delayStr != null && !onStartup) try { delay = 1000*Integer.parseInt(delayStr); } catch (NumberFormatException nfe) {} - rv.add(new ClientAppConfig(className, clientName, args, delay, dis, classpath, stopargs)); + rv.add(new ClientAppConfig(className, clientName, args, delay, dis, + classpath, stopargs, uninstallargs)); } return rv; } From f265db4037e314ce05699f035136a1337540fdba Mon Sep 17 00:00:00 2001 From: zzz Date: Thu, 11 Feb 2010 21:41:54 +0000 Subject: [PATCH 24/25] fix stop button; catch and log exceptions better --- .../net/i2p/router/web/ConfigClientsHandler.java | 15 ++++++++++----- .../net/i2p/router/web/ConfigClientsHelper.java | 2 +- .../src/net/i2p/router/web/PluginStarter.java | 13 ++++++++++--- .../net/i2p/router/web/PluginUpdateHandler.java | 8 ++++++-- .../src/net/i2p/router/web/WebAppStarter.java | 2 ++ 5 files changed, 29 insertions(+), 11 deletions(-) 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 a496cd3999..1fd1191265 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java @@ -72,8 +72,9 @@ public class ConfigClientsHandler extends FormHandler { PluginStarter.stopPlugin(_context, app); PluginStarter.deletePlugin(_context, app); addFormNotice(_("Deleted plugin {0}", app)); - } catch (IOException e) { + } catch (Throwable e) { addFormError(_("Error deleting plugin {0}", app) + ": " + e); + _log.error("Error deleting plugin " + app, e); } } return; @@ -84,8 +85,11 @@ public class ConfigClientsHandler extends FormHandler { String app = _action.substring(5); try { PluginStarter.stopPlugin(_context, app); - } catch (IOException e) {} - addFormNotice(_("Stopped plugin {0}", app)); + addFormNotice(_("Stopped plugin {0}", app)); + } catch (Throwable e) { + addFormError(_("Error stopping plugin {0}", app) + ": " + e); + _log.error("Error stopping plugin " + app, e); + } return; } @@ -247,8 +251,9 @@ public class ConfigClientsHandler extends FormHandler { path = new File(path, app + ".war"); WebAppStarter.startWebApp(_context, s, app, path.getAbsolutePath()); addFormNotice(_("WebApp") + " " + _(app) + " " + _("started") + '.'); - } catch (Exception ioe) { - addFormError(_("Failed to start") + ' ' + _(app) + " " + ioe + '.'); + } catch (Throwable e) { + addFormError(_("Failed to start") + ' ' + _(app) + " " + e + '.'); + _log.error("Failed to start webapp " + app, e); } return; } diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java index d0daac4b2e..a096df5f08 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java @@ -181,7 +181,7 @@ public class ConfigClientsHelper extends HelperBase { if (showEditButton && (!edit) && !ro) buf.append(""); if (showStopButton && (!edit)) - buf.append(""); + buf.append(""); if (showUpdateButton && (!edit) && !ro) { buf.append(""); buf.append(""); diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java index c6dd398cbb..76920fc68b 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java @@ -50,6 +50,7 @@ public class PluginStarter implements Runnable { startPlugins(_context); } + /** this shouldn't throw anything */ static void startPlugins(RouterContext ctx) { Log log = ctx.logManager().getLog(PluginStarter.class); Properties props = pluginProperties(); @@ -61,7 +62,7 @@ public class PluginStarter implements Runnable { try { if (!startPlugin(ctx, app)) log.error("Failed to start plugin: " + app); - } catch (Exception e) { + } catch (Throwable e) { log.error("Failed to start plugin: " + app, e); } } @@ -69,7 +70,10 @@ public class PluginStarter implements Runnable { } } - /** @return true on success */ + /** + * @return true on success + * @throws just about anything, caller would be wise to catch Throwable + */ static boolean startPlugin(RouterContext ctx, String appName) throws Exception { Log log = ctx.logManager().getLog(PluginStarter.class); File pluginDir = new File(ctx.getAppDir(), PluginUpdateHandler.PLUGIN_DIR + '/' + appName); @@ -159,7 +163,10 @@ public class PluginStarter implements Runnable { return true; } - /** @return true on success */ + /** + * @return true on success + * @throws just about anything, caller would be wise to catch Throwable + */ static boolean stopPlugin(RouterContext ctx, String appName) throws IOException { Log log = ctx.logManager().getLog(PluginStarter.class); File pluginDir = new File(ctx.getAppDir(), PluginUpdateHandler.PLUGIN_DIR + '/' + appName); diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java index 903000fbd7..46ff441cb3 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java @@ -308,7 +308,10 @@ public class PluginUpdateHandler extends UpdateHandler { if (!PluginStarter.stopPlugin(_context, appName)) { // failed, ignore } - } catch (IOException e) {} // ignore + } catch (Throwable e) { + // no updateStatus() for this one + _log.error("Error stopping plugin " + appName, e); + } } else { if (Boolean.valueOf(props.getProperty("update-only")).booleanValue()) { @@ -347,8 +350,9 @@ public class PluginUpdateHandler extends UpdateHandler { updateStatus("" + _("Plugin {0} installed and started", appName) + ""); else updateStatus("" + _("Plugin {0} installed but failed to start, check logs", appName) + ""); - } catch (Exception e) { + } catch (Throwable e) { updateStatus("" + _("Plugin {0} installed but failed to start", appName) + ": " + e + ""); + _log.error("Error starting plugin " + appName, e); } } } diff --git a/apps/routerconsole/java/src/net/i2p/router/web/WebAppStarter.java b/apps/routerconsole/java/src/net/i2p/router/web/WebAppStarter.java index 3e24a38c41..1117fd93ab 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/WebAppStarter.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/WebAppStarter.java @@ -30,6 +30,7 @@ public class WebAppStarter { /** * adds and starts + * @throws just about anything, caller would be wise to catch Throwable */ 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()); @@ -61,6 +62,7 @@ public class WebAppStarter { /** * stop it + * @throws just about anything, caller would be wise to catch Throwable */ static void stopWebApp(Server server, String appName) { // this will return a new context if one does not exist From a1fb5ef6edd2dcd7ec1ff6f826bd218b96473ae9 Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 15 Feb 2010 16:12:49 +0000 Subject: [PATCH 25/25] verify that signing key name matches --- .../net/i2p/router/web/PluginUpdateHandler.java | 8 ++++++-- .../net/i2p/router/web/WebAppConfiguration.java | 4 ++-- core/java/src/net/i2p/crypto/TrustedUpdate.java | 17 +++++++++++++++++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java index 46ff441cb3..fb54235e4f 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java @@ -193,7 +193,9 @@ public class PluginUpdateHandler extends UpdateHandler { if (up.haveKey(pubkey)) { // the key is already in the TrustedUpdate keyring - if (!up.verify(f)) { + // verify the sig and verify that it is signed by the keyName in the plugin.config file + String signingKeyName = up.verifyAndGetSigner(f); + if (!keyName.equals(signingKeyName)) { f.delete(); to.delete(); updateStatus("" + _("Plugin signature verification of {0} failed", url) + ""); @@ -209,7 +211,9 @@ public class PluginUpdateHandler extends UpdateHandler { return; } // ...and try the verify again - if (!up.verify(f)) { + // verify the sig and verify that it is signed by the keyName in the plugin.config file + String signingKeyName = up.verifyAndGetSigner(f); + if (!keyName.equals(signingKeyName)) { f.delete(); to.delete(); updateStatus("" + _("Plugin signature verification of {0} failed", url) + ""); diff --git a/apps/routerconsole/java/src/net/i2p/router/web/WebAppConfiguration.java b/apps/routerconsole/java/src/net/i2p/router/web/WebAppConfiguration.java index 121145bf3c..c3fcc334aa 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/WebAppConfiguration.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/WebAppConfiguration.java @@ -81,9 +81,9 @@ public class WebAppConfiguration implements WebApplicationContext.Configuration String elem = tok.nextToken().trim(); String path; if (elem.startsWith("$I2P")) - path = i2pContext.getBaseDir().getAbsolutePath() + '/' + elem.substring(4); + path = i2pContext.getBaseDir().getAbsolutePath() + elem.substring(4); else if (elem.startsWith("$PLUGIN")) - path = dir.getAbsolutePath() + '/' + elem.substring(7); + path = dir.getAbsolutePath() + elem.substring(7); else path = dir.getAbsolutePath() + '/' + elem; System.err.println("Adding " + path + " to classpath for " + appName); diff --git a/core/java/src/net/i2p/crypto/TrustedUpdate.java b/core/java/src/net/i2p/crypto/TrustedUpdate.java index c567fcb310..054d15358c 100644 --- a/core/java/src/net/i2p/crypto/TrustedUpdate.java +++ b/core/java/src/net/i2p/crypto/TrustedUpdate.java @@ -682,6 +682,23 @@ D8usM7Dxp5yrDrCYZ5AIijc= return false; } + /** + * Verifies the DSA signature of a signed update file. + * + * @param signedFile The signed update file to check. + * + * @return signer (could be empty string) or null if invalid + * @since 0.7.12 + */ + public String verifyAndGetSigner(File signedFile) { + for (SigningPublicKey signingPublicKey : _trustedKeys.keySet()) { + boolean isValidSignature = verify(signedFile, signingPublicKey); + if (isValidSignature) + return _trustedKeys.get(signingPublicKey); + } + return null; + } + /** * Verifies the DSA signature of a signed update file. *