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/routerconsole/java/build.xml b/apps/routerconsole/java/build.xml
index d8d85e21dd..78384113f9 100644
--- a/apps/routerconsole/java/build.xml
+++ b/apps/routerconsole/java/build.xml
@@ -64,13 +64,16 @@
-
+
+
+
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..1fd1191265 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,7 @@
package net.i2p.router.web;
import java.io.File;
-import java.util.Collection;
+import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@@ -13,7 +13,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;
/**
@@ -37,6 +36,14 @@ public class ConfigClientsHandler extends FormHandler {
saveWebAppChanges();
return;
}
+ if (_action.equals(_("Save Plugin Configuration"))) {
+ savePluginChanges();
+ return;
+ }
+ if (_action.equals(_("Install Plugin"))) {
+ installPlugin();
+ return;
+ }
// value
if (_action.startsWith("Start ")) {
String app = _action.substring(6);
@@ -58,10 +65,48 @@ 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);
+ PluginStarter.deletePlugin(_context, app);
+ addFormNotice(_("Deleted plugin {0}", app));
+ } catch (Throwable e) {
+ addFormError(_("Error deleting plugin {0}", app) + ": " + e);
+ _log.error("Error deleting plugin " + app, e);
+ }
+ }
return;
}
+
+ // value
+ if (_action.startsWith("Stop ")) {
+ String app = _action.substring(5);
+ try {
+ PluginStarter.stopPlugin(_context, app);
+ addFormNotice(_("Stopped plugin {0}", app));
+ } catch (Throwable e) {
+ addFormError(_("Error stopping plugin {0}", app) + ": " + e);
+ _log.error("Error stopping plugin " + app, e);
+ }
+ 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 + " ") &&
@@ -79,6 +124,7 @@ public class ConfigClientsHandler extends FormHandler {
} else {
addFormError(_("Unsupported") + ' ' + _action + '.');
}
+
}
public void setSettings(Map settings) { _settings = new HashMap(settings); }
@@ -173,32 +219,100 @@ 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."));
}
- // 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 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."));
+ }
+
+ /**
+ * 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");
- 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 + '.');
+ } catch (Throwable e) {
+ addFormError(_("Failed to start") + ' ' + _(app) + " " + e + '.');
+ _log.error("Failed to start webapp " + app, e);
}
return;
- }
- }
}
addFormError(_("Failed to find server."));
}
+
+ private void installPlugin() {
+ String url = getString("pluginURL");
+ if (url == null || url.length() <= 0) {
+ 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;
+ }
+ 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) {}
+ }
+
+ 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 acef26f721..a096df5f08 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;
@@ -33,18 +35,18 @@ public class ConfigClientsHelper extends HelperBase {
public String getForm1() {
StringBuilder buf = new StringBuilder(1024);
buf.append("\n");
- buf.append("" + _("Client") + " | " + _("Run at Startup?") + " | " + _("Start Now") + " | " + _("Class and arguments") + " |
\n");
+ buf.append("" + _("Client") + " | " + _("Run at Startup?") + " | " + _("Control") + " | " + _("Class and arguments") + " |
\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);
+ 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);
+ renderForm(buf, "" + clients.size(), "", false, false, false, "", true, false, false, false);
buf.append("
\n");
return buf.toString();
}
@@ -52,7 +54,7 @@ public class ConfigClientsHelper extends HelperBase {
public String getForm2() {
StringBuilder buf = new StringBuilder(1024);
buf.append("\n");
- buf.append("" + _("WebApp") + " | " + _("Run at Startup?") + " | " + _("Start Now") + " | " + _("Description") + " |
\n");
+ buf.append("" + _("WebApp") + " | " + _("Run at Startup?") + " | " + _("Control") + " | " + _("Description") + " |
\n");
Properties props = RouterConsoleRunner.webAppProperties();
Set keys = new TreeSet(props.keySet());
for (Iterator iter = keys.iterator(); iter.hasNext(); ) {
@@ -61,7 +63,86 @@ 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, false);
+ }
+ }
+ buf.append("
\n");
+ return buf.toString();
+ }
+
+ public boolean showPlugins() {
+ return PluginStarter.pluginsEnabled(_context);
+ }
+
+ public String getForm3() {
+ StringBuilder buf = new StringBuilder(1024);
+ buf.append("\n");
+ buf.append("" + _("Plugin") + " | " + _("Run at Startup?") + " | " + _("Control") + " | " + _("Description") + " |
\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);
+ Properties appProps = PluginStarter.pluginProperties(_context, app);
+ StringBuilder desc = new StringBuilder(256);
+ desc.append("")
+ .append("").append(_("Version")).append(" | ").append(stripHTML(appProps, "version"))
+ .append(" |
")
+ .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 {
+ 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 = stripHTML(appProps, "author");
+ if (s != null) {
+ desc.append(" |
")
+ .append(_("Author")).append(" | ");
+ if (s.indexOf("@") > 0)
+ desc.append("").append(s).append("");
+ else
+ desc.append(s);
+ }
+ s = stripHTML(appProps, "description_" + Messages.getLanguage(_context));
+ if (s == null)
+ s = stripHTML(appProps, "description");
+ if (s != null) {
+ desc.append(" |
")
+ .append(_("Description")).append(" | ").append(s);
+ }
+ s = stripHTML(appProps, "license");
+ if (s != null) {
+ desc.append(" |
")
+ .append(_("License")).append(" | ").append(s);
+ }
+ s = stripHTML(appProps, "websiteURL");
+ if (s != null) {
+ desc.append(" |
")
+ .append("").append(_("Website")).append(" | ");
+ }
+ String updateURL = stripHTML(appProps, "updateURL");
+ if (updateURL != null) {
+ desc.append(" |
")
+ .append("").append(_("Update link")).append(" | ");
+ }
+ desc.append(" |
");
+ renderForm(buf, app, app, false,
+ "true".equals(val), false, desc.toString(), false, false,
+ updateURL != null, true);
}
}
buf.append("
\n");
@@ -70,7 +151,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, boolean showStopButton) {
buf.append("");
if (urlify && enabled) {
String link = "/";
@@ -92,14 +174,20 @@ public class ConfigClientsHelper extends HelperBase {
if (ro)
buf.append("disabled=\"true\" ");
}
- buf.append("/> | ");
+ buf.append("> | ");
if ((!enabled) && !edit) {
buf.append("");
}
- if (showEditButton && (!edit) && !ro) {
+ if (showEditButton && (!edit) && !ro)
buf.append("");
- buf.append("");
+ if (showStopButton && (!edit))
+ buf.append("");
+ if (showUpdateButton && (!edit) && !ro) {
+ buf.append("");
+ buf.append("");
}
+ if ((!edit) && !ro)
+ buf.append("");
buf.append(" | ");
if (edit && !ro) {
buf.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/ConfigUpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHandler.java
index 512471a101..cea2a11b2f 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/NavHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/NavHelper.java
index cabb68f156..488275a23b 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,16 @@ public class NavHelper extends HelperBase {
_apps.remove(name);
}
- public String getClientAppLinks() {
+ /**
+ * Translated string is loaded by PluginStarter
+ */
+ 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(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
new file mode 100644
index 0000000000..76920fc68b
--- /dev/null
+++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java
@@ -0,0 +1,418 @@
+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.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;
+
+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.FileUtil;
+import net.i2p.util.Log;
+import net.i2p.util.Translate;
+
+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;
+ }
+
+ static boolean pluginsEnabled(I2PAppContext ctx) {
+ return Boolean.valueOf(ctx.getProperty("router.enablePlugins")).booleanValue();
+ }
+
+ public void run() {
+ startPlugins(_context);
+ }
+
+ /** this shouldn't throw anything */
+ 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();
+ if (name.startsWith(PREFIX) && name.endsWith(ENABLED)) {
+ if (Boolean.valueOf(props.getProperty(name)).booleanValue()) {
+ String app = name.substring(PREFIX.length(), name.lastIndexOf(ENABLED));
+ try {
+ if (!startPlugin(ctx, app))
+ log.error("Failed to start plugin: " + app);
+ } catch (Throwable e) {
+ log.error("Failed to start plugin: " + app, e);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * @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);
+ if ((!pluginDir.exists()) || (!pluginDir.isDirectory())) {
+ 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");
+ if (clientConfig.exists()) {
+ Properties props = new Properties();
+ DataHelper.loadProps(props, clientConfig);
+ List clients = ClientAppConfig.getClientApps(clientConfig);
+ runClientApps(ctx, pluginDir, clients, "start");
+ }
+
+ // 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(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"));
+ //log.error("Found webapp: " + warName);
+ // 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)) {
+ //log.error("Starting webapp: " + warName);
+ String path = new File(webappDir, fileNames[i]).getCanonicalPath();
+ WebAppStarter.startWebApp(ctx, server, warName, path);
+ }
+ } catch (IOException ioe) {
+ log.error("Error resolving '" + fileNames[i] + "' in '" + webappDir, ioe);
+ }
+ }
+ }
+ }
+
+ // 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
+
+ // add summary bar link
+ Properties props = pluginProperties(ctx, appName);
+ String name = ConfigClientsHelper.stripHTML(props, "consoleLinkName_" + Messages.getLanguage(ctx));
+ if (name == null)
+ 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);
+
+ return true;
+ }
+
+ /**
+ * @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);
+ 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, "stop");
+ }
+
+ // 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);
+ String name = ConfigClientsHelper.stripHTML(props, "consoleLinkName_" + Messages.getLanguage(ctx));
+ if (name == null)
+ name = ConfigClientsHelper.stripHTML(props, "consoleLinkName");
+ 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) 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(); ) {
+ 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");
+ 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();
+ File cfgFile = new File(dir, "plugins.config");
+
+ try {
+ 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++) {
+ 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;
+ }
+
+ /**
+ * 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();
+ 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;
+ }
+
+ /** @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 (action.equals("start") && app.disabled)
+ continue;
+ String argVal[];
+ if (action.equals("start")) {
+ // start
+ argVal = LoadClientAppsJob.parseArgs(app.args);
+ } else {
+ 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(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.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 || !action.equals("start")) {
+ // 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));
+ }
+ }
+ }
+
+ /**
+ * 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 {
+ 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);
+ }
+ }
+ }
+
+ /**
+ * 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/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..8b5d2c0147
--- /dev/null
+++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateChecker.java
@@ -0,0 +1,138 @@
+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 check, plugin {0} is not installed", appName) + "");
+ return;
+ }
+
+ if (_pluginUpdateCheckerRunner == null)
+ _pluginUpdateCheckerRunner = new PluginUpdateCheckerRunner();
+ if (_pluginUpdateCheckerRunner.isRunning())
+ return;
+ _xpi2pURL = xpi2pURL;
+ _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 {
+ ByteArrayOutputStream _baos;
+
+ public PluginUpdateCheckerRunner() {
+ super();
+ _baos = new ByteArrayOutputStream(TrustedUpdate.HEADER_BYTES);
+ }
+
+ @Override
+ protected void update() {
+ 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);
+ 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/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..fb54235e4f
--- /dev/null
+++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java
@@ -0,0 +1,378 @@
+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;
+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, 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 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 {
+
+ public PluginUpdateRunner(String url) {
+ super();
+ }
+
+ @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, "plugin.config");
+ 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;
+ }
+
+ // 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
+ // 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) + "");
+ 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
+ // 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) + "");
+ return;
+ }
+ }
+
+ String sudVersion = TrustedUpdate.getVersionString(f);
+ f.delete();
+
+ String appName = props.getProperty("name");
+ String version = props.getProperty("version");
+ if (appName == null || version == null || appName.length() <= 0 || version.length() <= 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;
+ }
+ if (!version.equals(sudVersion)) {
+ to.delete();
+ updateStatus("" + _("Plugin {0} has mismatched versions", appName) + "");
+ return;
+ }
+
+ // todo compare sud version with property version
+
+ String minVersion = ConfigClientsHelper.stripHTML(props, "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 = ConfigClientsHelper.stripHTML(props, "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;
+ }
+
+ File destDir = new File(appDir, appName);
+ if (destDir.exists()) {
+ if (Boolean.valueOf(props.getProperty("install-only")).booleanValue()) {
+ to.delete();
+ updateStatus("" + _("Downloaded plugin is for new installs only, but the plugin is already installed", url) + "");
+ return;
+ }
+
+ // compare previous version
+ File oldPropFile = new File(destDir, "plugin.config");
+ 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("" + _("Downloaded plugin version {0} is not newer than installed plugin", version) + "");
+ return;
+ }
+ minVersion = ConfigClientsHelper.stripHTML(props, "min-installed-version");
+ if (minVersion != null &&
+ (new VersionComparator()).compare(minVersion, oldVersion) > 0) {
+ to.delete();
+ 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 plugin version {0} or lower", maxVersion) + "");
+ return;
+ }
+
+ // check if it is running first?
+ try {
+ if (!PluginStarter.stopPlugin(_context, appName)) {
+ // failed, 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()) {
+ to.delete();
+ updateStatus("" + _("Plugin is for upgrades only, but the plugin is not installed") + "");
+ 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("" + _("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} installed, router restart required", appName) + "");
+ else {
+ updateStatus("" + _("Plugin {0} installed", appName) + "");
+ Properties pluginProps = PluginStarter.pluginProperties();
+ pluginProps.setProperty(PluginStarter.PREFIX + appName + PluginStarter.ENABLED, "false");
+ PluginStarter.storePluginProperties(pluginProps);
+ }
+ } else {
+ // start everything
+ try {
+ if (PluginStarter.startPlugin(_context, appName))
+ updateStatus("" + _("Plugin {0} installed and started", appName) + "");
+ else
+ updateStatus("" + _("Plugin {0} installed but failed to start, check logs", appName) + "");
+ } catch (Throwable e) {
+ updateStatus("" + _("Plugin {0} installed but failed to start", appName) + ": " + e + "");
+ _log.error("Error starting plugin " + appName, e);
+ }
+ }
+ }
+
+ @Override
+ public void transferFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt) {
+ File f = new File(_updateFile);
+ f.delete();
+ updateStatus("" + _("Failed to download plugin from {0}", url) + "");
+ }
+ }
+
+ @Override
+ protected void updateStatus(String s) {
+ super.updateStatus(s);
+ _appStatus = s;
+ }
+}
+
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..f4f7bd79e0 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");
@@ -181,16 +181,22 @@ 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) {
+ if (PluginStarter.pluginsEnabled(contexts.get(0))) {
+ t = new I2PAppThread(new PluginStarter(contexts.get(0)), "PluginStarter", true);
+ t.start();
+ }
+ }
}
- private void initialize(WebApplicationContext context) {
+ static void initialize(WebApplicationContext context) {
String password = getPassword();
if (password != null) {
HashUserRealm realm = new HashUserRealm("i2prouter");
@@ -205,11 +211,11 @@ public class RouterConsoleRunner {
}
}
- private String getPassword() {
- List contexts = RouterContext.listContexts();
+ static String getPassword() {
+ 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();
@@ -237,10 +243,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);
@@ -263,11 +273,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/java/src/net/i2p/router/web/SummaryBarRenderer.java b/apps/routerconsole/java/src/net/i2p/router/web/SummaryBarRenderer.java
index af3323571d..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" +
"
" />
-