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 a1204f393..cf81ee022 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 62d60358d..d9f23d8c9 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 000000000..8045c6e80 --- /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 af3323571..7dd28c0ef 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 050f0f975..1f1d3de26 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 2af8ab782..daf3bcf87 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:")%> +

+ +


+ " />