propagate from branch 'i2p.i2p.zzz.plugin' (head fafcd8c8c41873b4d106a9e06504dd7b48109ad8)
to branch 'i2p.i2p' (head 7eafbe18b0a1e26f09b9488d374f5fed4c278a78)
This commit is contained in:
@ -53,7 +53,8 @@
|
||||
-->
|
||||
<target name="war" depends="jar, bundle">
|
||||
<war destfile="../i2psnark.war" webxml="../web.xml">
|
||||
<classes dir="./build/obj" includes="**/*.class" excludes="**/RunStandalone.class" />
|
||||
<!-- include only the web stuff, as of 0.7.12 the router will add i2psnark.jar to the classpath for the war -->
|
||||
<classes dir="./build/obj" includes="**/web/*.class" />
|
||||
</war>
|
||||
</target>
|
||||
|
||||
|
@ -64,13 +64,16 @@
|
||||
<target name="jar" depends="compile">
|
||||
<jar destfile="./build/routerconsole.jar" basedir="./build/obj" includes="**/*.class">
|
||||
<manifest>
|
||||
<attribute name="Class-Path" value="i2p.jar router.jar" />
|
||||
<!-- top level installer will rename to jrobin.jar -->
|
||||
<attribute name="Class-Path" value="i2p.jar router.jar jrobin.jar" />
|
||||
</manifest>
|
||||
</jar>
|
||||
<delete dir="./tmpextract" />
|
||||
<!-- jrobin taken out of routerconsole.jar in 0.7.12
|
||||
<unjar src="../../jrobin/jrobin-1.4.0.jar" dest="./tmpextract" />
|
||||
<jar destfile="./build/routerconsole.jar" basedir="./tmpextract" update="true" />
|
||||
<delete dir="./tmpextract" />
|
||||
-->
|
||||
|
||||
<ant target="war" />
|
||||
|
||||
|
@ -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 + "<span class=hide> ") &&
|
||||
@ -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") + " <a href=\"/" + app + "/\">" + _(app) + "</a> " + _("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) {}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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("<table>\n");
|
||||
buf.append("<tr><th align=\"right\">" + _("Client") + "</th><th>" + _("Run at Startup?") + "</th><th>" + _("Start Now") + "</th><th align=\"left\">" + _("Class and arguments") + "</th></tr>\n");
|
||||
buf.append("<tr><th align=\"right\">" + _("Client") + "</th><th>" + _("Run at Startup?") + "</th><th>" + _("Control") + "</th><th align=\"left\">" + _("Class and arguments") + "</th></tr>\n");
|
||||
|
||||
List<ClientAppConfig> 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("</table>\n");
|
||||
return buf.toString();
|
||||
}
|
||||
@ -52,7 +54,7 @@ public class ConfigClientsHelper extends HelperBase {
|
||||
public String getForm2() {
|
||||
StringBuilder buf = new StringBuilder(1024);
|
||||
buf.append("<table>\n");
|
||||
buf.append("<tr><th align=\"right\">" + _("WebApp") + "</th><th>" + _("Run at Startup?") + "</th><th>" + _("Start Now") + "</th><th align=\"left\">" + _("Description") + "</th></tr>\n");
|
||||
buf.append("<tr><th align=\"right\">" + _("WebApp") + "</th><th>" + _("Run at Startup?") + "</th><th>" + _("Control") + "</th><th align=\"left\">" + _("Description") + "</th></tr>\n");
|
||||
Properties props = RouterConsoleRunner.webAppProperties();
|
||||
Set<String> keys = new TreeSet(props.keySet());
|
||||
for (Iterator<String> 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("</table>\n");
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
public boolean showPlugins() {
|
||||
return PluginStarter.pluginsEnabled(_context);
|
||||
}
|
||||
|
||||
public String getForm3() {
|
||||
StringBuilder buf = new StringBuilder(1024);
|
||||
buf.append("<table>\n");
|
||||
buf.append("<tr><th align=\"right\">" + _("Plugin") + "</th><th>" + _("Run at Startup?") + "</th><th>" + _("Control") + "</th><th align=\"left\">" + _("Description") + "</th></tr>\n");
|
||||
Properties props = PluginStarter.pluginProperties();
|
||||
Set<String> keys = new TreeSet(props.keySet());
|
||||
for (Iterator<String> 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("<table border=\"0\">")
|
||||
.append("<tr><td><b>").append(_("Version")).append("<td>").append(stripHTML(appProps, "version"))
|
||||
.append("<tr><td><b>")
|
||||
.append(_("Signed by")).append("<td>");
|
||||
String s = stripHTML(appProps, "keyName");
|
||||
if (s.indexOf("@") > 0)
|
||||
desc.append("<a href=\"mailto:").append(s).append("\">").append(s).append("</a>");
|
||||
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("<tr><td><b>")
|
||||
.append(_("Date")).append("<td>").append(date);
|
||||
}
|
||||
}
|
||||
s = stripHTML(appProps, "author");
|
||||
if (s != null) {
|
||||
desc.append("<tr><td><b>")
|
||||
.append(_("Author")).append("<td>");
|
||||
if (s.indexOf("@") > 0)
|
||||
desc.append("<a href=\"mailto:").append(s).append("\">").append(s).append("</a>");
|
||||
else
|
||||
desc.append(s);
|
||||
}
|
||||
s = stripHTML(appProps, "description_" + Messages.getLanguage(_context));
|
||||
if (s == null)
|
||||
s = stripHTML(appProps, "description");
|
||||
if (s != null) {
|
||||
desc.append("<tr><td><b>")
|
||||
.append(_("Description")).append("<td>").append(s);
|
||||
}
|
||||
s = stripHTML(appProps, "license");
|
||||
if (s != null) {
|
||||
desc.append("<tr><td><b>")
|
||||
.append(_("License")).append("<td>").append(s);
|
||||
}
|
||||
s = stripHTML(appProps, "websiteURL");
|
||||
if (s != null) {
|
||||
desc.append("<tr><td>")
|
||||
.append("<a href=\"").append(s).append("\">").append(_("Website")).append("</a><td> ");
|
||||
}
|
||||
String updateURL = stripHTML(appProps, "updateURL");
|
||||
if (updateURL != null) {
|
||||
desc.append("<tr><td>")
|
||||
.append("<a href=\"").append(updateURL).append("\">").append(_("Update link")).append("</a><td> ");
|
||||
}
|
||||
desc.append("</table>");
|
||||
renderForm(buf, app, app, false,
|
||||
"true".equals(val), false, desc.toString(), false, false,
|
||||
updateURL != null, true);
|
||||
}
|
||||
}
|
||||
buf.append("</table>\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("<tr><td class=\"mediumtags\" align=\"right\" width=\"25%\">");
|
||||
if (urlify && enabled) {
|
||||
String link = "/";
|
||||
@ -92,14 +174,20 @@ public class ConfigClientsHelper extends HelperBase {
|
||||
if (ro)
|
||||
buf.append("disabled=\"true\" ");
|
||||
}
|
||||
buf.append("/></td><td align=\"center\" width=\"15%\">");
|
||||
buf.append("></td><td align=\"center\" width=\"15%\">");
|
||||
if ((!enabled) && !edit) {
|
||||
buf.append("<button type=\"submit\" name=\"action\" value=\"Start ").append(index).append("\" >" + _("Start") + "<span class=hide> ").append(index).append("</span></button>");
|
||||
}
|
||||
if (showEditButton && (!edit) && !ro) {
|
||||
if (showEditButton && (!edit) && !ro)
|
||||
buf.append("<button type=\"submit\" name=\"edit\" value=\"Edit ").append(index).append("\" >" + _("Edit") + "<span class=hide> ").append(index).append("</span></button>");
|
||||
buf.append("<button type=\"submit\" name=\"action\" value=\"Delete ").append(index).append("\" >" + _("Delete") + "<span class=hide> ").append(index).append("</span></button>");
|
||||
if (showStopButton && (!edit))
|
||||
buf.append("<button type=\"submit\" name=\"action\" value=\"Stop ").append(index).append("\" >" + _("Stop") + "<span class=hide> ").append(index).append("</span></button>");
|
||||
if (showUpdateButton && (!edit) && !ro) {
|
||||
buf.append("<button type=\"submit\" name=\"action\" value=\"Check ").append(index).append("\" >" + _("Check for updates") + "<span class=hide> ").append(index).append("</span></button>");
|
||||
buf.append("<button type=\"submit\" name=\"action\" value=\"Update ").append(index).append("\" >" + _("Update") + "<span class=hide> ").append(index).append("</span></button>");
|
||||
}
|
||||
if ((!edit) && !ro)
|
||||
buf.append("<button type=\"submit\" name=\"action\" value=\"Delete ").append(index).append("\" >" + _("Delete") + "<span class=hide> ").append(index).append("</span></button>");
|
||||
buf.append("</td><td align=\"left\" width=\"50%\">");
|
||||
if (edit && !ro) {
|
||||
buf.append("<input type=\"text\" size=\"80\" name=\"desc").append(index).append("\" value=\"");
|
||||
@ -110,4 +198,16 @@ public class ConfigClientsHelper extends HelperBase {
|
||||
}
|
||||
buf.append("</td></tr>\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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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<String, String> _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("<a href=\"").append(path).append("\">");
|
||||
buf.append(name).append("</a> |");
|
||||
for (Iterator<String> iter = _apps.keySet().iterator(); iter.hasNext(); ) {
|
||||
String name = iter.next();
|
||||
String path = _apps.get(name);
|
||||
buf.append(" <a target=\"_top\" href=\"").append(path).append("\">");
|
||||
buf.append(name).append("</a>");
|
||||
}
|
||||
return buf.toString();
|
||||
}
|
||||
|
@ -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<ClientAppConfig> 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<ClientAppConfig> 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<ClientAppConfig> 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<String> 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<String> getPlugins() {
|
||||
List<String> 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<String, String> getPluginKeys(I2PAppContext ctx) {
|
||||
Map<String, String> rv = new HashMap();
|
||||
List<String> 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<ClientAppConfig> 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});
|
||||
}
|
||||
}
|
@ -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("<b>" + _("Cannot check, plugin {0} is not installed", appName) + "</b>");
|
||||
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("<b>" + _("Checking for update of plugin {0}", _appName) + "</b>");
|
||||
// use the same settings as for updater
|
||||
boolean shouldProxy = Boolean.valueOf(_context.getProperty(ConfigUpdateHandler.PROP_SHOULD_PROXY, ConfigUpdateHandler.DEFAULT_SHOULD_PROXY)).booleanValue();
|
||||
String proxyHost = _context.getProperty(ConfigUpdateHandler.PROP_PROXY_HOST, ConfigUpdateHandler.DEFAULT_PROXY_HOST);
|
||||
int proxyPort = _context.getProperty(ConfigUpdateHandler.PROP_PROXY_PORT, ConfigUpdateHandler.DEFAULT_PROXY_PORT_INT);
|
||||
try {
|
||||
_get = new PartialEepGet(_context, proxyHost, proxyPort, _baos, _xpi2pURL, TrustedUpdate.HEADER_BYTES);
|
||||
_get.addStatusListener(PluginUpdateCheckerRunner.this);
|
||||
_get.fetch();
|
||||
} catch (Throwable t) {
|
||||
_log.error("Error checking update for plugin", t);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bytesTransferred(long alreadyTransferred, int currentWrite, long bytesTransferred, long bytesRemaining, String url) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transferComplete(long alreadyTransferred, long bytesTransferred, long bytesRemaining, String url, String outputFile, boolean notModified) {
|
||||
String newVersion = TrustedUpdate.getVersionString(new ByteArrayInputStream(_baos.toByteArray()));
|
||||
boolean newer = (new VersionComparator()).compare(newVersion, _oldVersion) > 0;
|
||||
if (newer)
|
||||
updateStatus("<b>" + _("New plugin version {0} is available", newVersion) + "</b>");
|
||||
else
|
||||
updateStatus("<b>" + _("No new version is available for plugin {0}", _appName) + "</b>");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transferFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt) {
|
||||
File f = new File(_updateFile);
|
||||
f.delete();
|
||||
updateStatus("<b>" + _("Update check failed for plugin {0}", _appName) + "</b>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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("<b>" + _("Downloading plugin from {0}", _xpi2pURL) + "</b>");
|
||||
// use the same settings as for updater
|
||||
boolean shouldProxy = Boolean.valueOf(_context.getProperty(ConfigUpdateHandler.PROP_SHOULD_PROXY, ConfigUpdateHandler.DEFAULT_SHOULD_PROXY)).booleanValue();
|
||||
String proxyHost = _context.getProperty(ConfigUpdateHandler.PROP_PROXY_HOST, ConfigUpdateHandler.DEFAULT_PROXY_HOST);
|
||||
int proxyPort = _context.getProperty(ConfigUpdateHandler.PROP_PROXY_PORT, ConfigUpdateHandler.DEFAULT_PROXY_PORT_INT);
|
||||
try {
|
||||
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("<b>").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("<b>" + _("Plugin downloaded") + "</b>");
|
||||
File f = new File(_updateFile);
|
||||
File appDir = new File(_context.getAppDir(), PLUGIN_DIR);
|
||||
if ((!appDir.exists()) && (!appDir.mkdir())) {
|
||||
f.delete();
|
||||
updateStatus("<b>" + _("Cannot create plugin directory {0}", appDir.getAbsolutePath()) + "</b>");
|
||||
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("<b>" + err + ' ' + _("from {0}", url) + " </b>");
|
||||
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("<b>" + _("Plugin from {0} is corrupt", url) + "</b>");
|
||||
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("<b>" + _("Plugin from {0} does not contain the required configuration file", url) + "</b>");
|
||||
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("<b>" + "Plugin contains an invalid key" + ' ' + pubkey + ' ' + keyName + "</b>");
|
||||
updateStatus("<b>" + _("Plugin from {0} contains an invalid key", url) + "</b>");
|
||||
return;
|
||||
}
|
||||
|
||||
// add all existing plugin keys, so any conflicts with existing keys
|
||||
// will be discovered and rejected
|
||||
Map<String, String> existingKeys = PluginStarter.getPluginKeys(_context);
|
||||
for (Map.Entry<String, String> 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("<b>" + _("Plugin signature verification of {0} failed", url) + "</b>");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// add to keyring...
|
||||
if(!up.addKey(pubkey, keyName)) {
|
||||
// bad or duplicate key
|
||||
f.delete();
|
||||
to.delete();
|
||||
updateStatus("<b>" + _("Plugin signature verification of {0} failed", url) + "</b>");
|
||||
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("<b>" + _("Plugin signature verification of {0} failed", url) + "</b>");
|
||||
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("<b>" + _("Plugin from {0} has invalid name or version", url) + "</b>");
|
||||
return;
|
||||
}
|
||||
if (!version.equals(sudVersion)) {
|
||||
to.delete();
|
||||
updateStatus("<b>" + _("Plugin {0} has mismatched versions", appName) + "</b>");
|
||||
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("<b>" + _("This plugin requires I2P version {0} or higher", minVersion) + "</b>");
|
||||
return;
|
||||
}
|
||||
|
||||
minVersion = ConfigClientsHelper.stripHTML(props, "min-java-version");
|
||||
if (minVersion != null &&
|
||||
(new VersionComparator()).compare(System.getProperty("java.version"), minVersion) < 0) {
|
||||
to.delete();
|
||||
updateStatus("<b>" + _("This plugin requires Java version {0} or higher", minVersion) + "</b>");
|
||||
return;
|
||||
}
|
||||
|
||||
File destDir = new File(appDir, appName);
|
||||
if (destDir.exists()) {
|
||||
if (Boolean.valueOf(props.getProperty("install-only")).booleanValue()) {
|
||||
to.delete();
|
||||
updateStatus("<b>" + _("Downloaded plugin is for new installs only, but the plugin is already installed", url) + "</b>");
|
||||
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("<b>" + _("Installed plugin does not contain the required configuration file", url) + "</b>");
|
||||
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("<b>" + _("Signature of downloaded plugin does not match installed plugin") + "</b>");
|
||||
return;
|
||||
}
|
||||
String oldVersion = oldProps.getProperty("version");
|
||||
if (oldVersion == null ||
|
||||
(new VersionComparator()).compare(oldVersion, version) >= 0) {
|
||||
to.delete();
|
||||
updateStatus("<b>" + _("Downloaded plugin version {0} is not newer than installed plugin", version) + "</b>");
|
||||
return;
|
||||
}
|
||||
minVersion = ConfigClientsHelper.stripHTML(props, "min-installed-version");
|
||||
if (minVersion != null &&
|
||||
(new VersionComparator()).compare(minVersion, oldVersion) > 0) {
|
||||
to.delete();
|
||||
updateStatus("<b>" + _("Plugin update requires installed plugin version {0} or higher", minVersion) + "</b>");
|
||||
return;
|
||||
}
|
||||
String maxVersion = ConfigClientsHelper.stripHTML(props, "max-installed-version");
|
||||
if (maxVersion != null &&
|
||||
(new VersionComparator()).compare(maxVersion, oldVersion) < 0) {
|
||||
to.delete();
|
||||
updateStatus("<b>" + _("Plugin update requires installed plugin version {0} or lower", maxVersion) + "</b>");
|
||||
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("<b>" + _("Plugin is for upgrades only, but the plugin is not installed") + "</b>");
|
||||
return;
|
||||
}
|
||||
if (!destDir.mkdir()) {
|
||||
to.delete();
|
||||
updateStatus("<b>" + _("Cannot create plugin directory {0}", destDir.getAbsolutePath()) + "</b>");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, extract the zip to the plugin directory
|
||||
if (!FileUtil.extractZip(to, destDir)) {
|
||||
to.delete();
|
||||
updateStatus("<b>" + _("Failed to install plugin in {0}", destDir.getAbsolutePath()) + "</b>");
|
||||
return;
|
||||
}
|
||||
|
||||
to.delete();
|
||||
if (Boolean.valueOf(props.getProperty("dont-start-at-install")).booleanValue()) {
|
||||
if (Boolean.valueOf(props.getProperty("router-restart-required")).booleanValue())
|
||||
updateStatus("<b>" + _("Plugin {0} installed, router restart required", appName) + "</b>");
|
||||
else {
|
||||
updateStatus("<b>" + _("Plugin {0} installed", appName) + "</b>");
|
||||
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("<b>" + _("Plugin {0} installed and started", appName) + "</b>");
|
||||
else
|
||||
updateStatus("<b>" + _("Plugin {0} installed but failed to start, check logs", appName) + "</b>");
|
||||
} catch (Throwable e) {
|
||||
updateStatus("<b>" + _("Plugin {0} installed but failed to start", appName) + ": " + e + "</b>");
|
||||
_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("<b>" + _("Failed to download plugin from {0}", url) + "</b>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateStatus(String s) {
|
||||
super.updateStatus(s);
|
||||
_appStatus = s;
|
||||
}
|
||||
}
|
||||
|
@ -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<RouterContext> 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<RouterContext> 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"));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -70,7 +70,11 @@ public class SummaryBarRenderer {
|
||||
.append(_("Anonymous resident webserver"))
|
||||
.append("\">")
|
||||
.append(_("Webserver"))
|
||||
.append("</a></td></tr></table>\n" +
|
||||
.append("</a>")
|
||||
|
||||
.append(NavHelper.getClientAppLinks(_context))
|
||||
|
||||
.append("</td></tr></table>\n" +
|
||||
|
||||
"<hr><h3><a href=\"/config.jsp\" target=\"_top\" title=\"")
|
||||
.append(_("Configure I2P Router"))
|
||||
@ -184,7 +188,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
|
||||
|
@ -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;
|
||||
|
@ -0,0 +1,96 @@
|
||||
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");
|
||||
// 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")) {
|
||||
// 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 (pluginDir.exists()) {
|
||||
File consoleDir = new File(pluginDir, "console");
|
||||
Properties props = RouterConsoleRunner.webAppProperties(consoleDir.getAbsolutePath());
|
||||
cp = props.getProperty(RouterConsoleRunner.PREFIX + appName + CLASSPATH);
|
||||
dir = pluginDir;
|
||||
} 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 if (elem.startsWith("$PLUGIN"))
|
||||
path = dir.getAbsolutePath() + elem.substring(7);
|
||||
else
|
||||
path = dir.getAbsolutePath() + '/' + elem;
|
||||
System.err.println("Adding " + path + " to classpath for " + appName);
|
||||
_wac.addClassPath(path);
|
||||
}
|
||||
}
|
||||
|
||||
public void configureDefaults() {}
|
||||
public void configureWebApp() {}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
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.http.HttpContext;
|
||||
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
|
||||
* @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());
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
HttpContext wac = server.getContext('/' + appName);
|
||||
try {
|
||||
// false -> not graceful
|
||||
wac.stop(false);
|
||||
} catch (InterruptedException ie) {}
|
||||
}
|
||||
|
||||
}
|
@ -54,4 +54,20 @@ button span.hide{
|
||||
<i><%=intl._("All changes require restart to take effect.")%></i>
|
||||
</p><hr><div class="formaction">
|
||||
<input type="submit" name="action" value="<%=intl._("Save WebApp Configuration")%>" />
|
||||
</div></div></form></div></div></body></html>
|
||||
</div></div>
|
||||
<% if (clientshelper.showPlugins()) { %>
|
||||
<h3><a name="webapp"></a><%=intl._("Plugin Configuration")%></h3><p>
|
||||
<%=intl._("The plugins listed below are started by the webConsole client.")%>
|
||||
</p><div class="wideload"><p>
|
||||
<jsp:getProperty name="clientshelper" property="form3" />
|
||||
</p><hr><div class="formaction">
|
||||
<input type="submit" name="action" value="<%=intl._("Save Plugin Configuration")%>" />
|
||||
</div></div><h3><a name="plugin"></a><%=intl._("Plugin Installation")%></h3><p>
|
||||
<%=intl._("To install a plugin, enter the download URL:")%>
|
||||
</p><div class="wideload"><p>
|
||||
<input type="text" size="60" name="pluginURL" >
|
||||
</p><hr><div class="formaction">
|
||||
<input type="submit" name="action" value="<%=intl._("Install Plugin")%>" />
|
||||
</div></div>
|
||||
<% } %>
|
||||
</form></div></div></body></html>
|
||||
|
@ -66,7 +66,9 @@
|
||||
<war destfile="${project}.war" webxml="WEB-INF/web-out.xml">
|
||||
<fileset dir=".">
|
||||
<include name="WEB-INF/**/*.class"/>
|
||||
<!-- pulled out of the jar in 0.7.12
|
||||
<include name="WEB-INF/lib/*.jar"/>
|
||||
-->
|
||||
<include name="images/*.png"/>
|
||||
<include name="css.css"/>
|
||||
<include name="index.html"/>
|
||||
|
10
build.xml
10
build.xml
@ -314,6 +314,8 @@
|
||||
<copy file="build/org.mortbay.jetty.jar" todir="pkg-temp/lib/" />
|
||||
<copy file="build/router.jar" todir="pkg-temp/lib/" />
|
||||
<copy file="build/routerconsole.jar" todir="pkg-temp/lib/" />
|
||||
<!-- pulled out of routerconsole.jar in 0.7.12; name without version so we can overwrite if we upgrade -->
|
||||
<copy file="apps/jrobin/jrobin-1.4.0.jar" tofile="pkg-temp/lib/jrobin.jar" />
|
||||
<copy file="build/sam.jar" todir="pkg-temp/lib/" />
|
||||
<copy file="build/BOB.jar" todir="pkg-temp/lib/" />
|
||||
<copy file="build/systray.jar" todir="pkg-temp/lib" />
|
||||
@ -325,6 +327,8 @@
|
||||
<copy file="build/addressbook.war" todir="pkg-temp/webapps/" />
|
||||
<copy file="build/susimail.war" todir="pkg-temp/webapps/" />
|
||||
<copy file="build/susidns.war" todir="pkg-temp/webapps/" />
|
||||
<copy file="apps/susidns/src/WEB-INF/lib/jstl.jar" todir="pkg-temp/lib/" />
|
||||
<copy file="apps/susidns/src/WEB-INF/lib/standard.jar" todir="pkg-temp/lib/" />
|
||||
<copy file="build/i2psnark.war" todir="pkg-temp/webapps/" />
|
||||
<copy file="apps/i2psnark/launch-i2psnark" todir="pkg-temp/" />
|
||||
<copy file="apps/i2psnark/jetty-i2psnark.xml" todir="pkg-temp/" />
|
||||
@ -470,6 +474,9 @@
|
||||
<copy file="build/systray.jar" todir="pkg-temp/lib/" />
|
||||
<copy file="build/susimail.war" todir="pkg-temp/webapps/" />
|
||||
<copy file="build/susidns.war" todir="pkg-temp/webapps/" />
|
||||
<!-- as of 0.7.12; someday, we can remove these from the updater -->
|
||||
<copy file="apps/susidns/src/WEB-INF/lib/jstl.jar" todir="pkg-temp/lib/" />
|
||||
<copy file="apps/susidns/src/WEB-INF/lib/standard.jar" todir="pkg-temp/lib/" />
|
||||
<copy file="build/i2psnark.war" todir="pkg-temp/webapps/" />
|
||||
<copy file="history.txt" todir="pkg-temp/" />
|
||||
<!-- the following overwrites history.txt on unix to shrink the update file -->
|
||||
@ -489,6 +496,9 @@
|
||||
<copy file="build/mstreaming.jar" todir="pkg-temp/lib/" />
|
||||
<copy file="build/streaming.jar" todir="pkg-temp/lib/" />
|
||||
<copy file="build/routerconsole.jar" todir="pkg-temp/lib/" />
|
||||
<!-- pulled out of routerconsole.jar in 0.7.12, someday we can take out of updater -->
|
||||
<!-- name without version so we can overwrite if we upgrade -->
|
||||
<copy file="apps/jrobin/jrobin-1.4.0.jar" tofile="pkg-temp/lib/jrobin.jar" />
|
||||
<copy file="build/i2ptunnel.war" todir="pkg-temp/webapps/" />
|
||||
<copy file="build/routerconsole.war" todir="pkg-temp/webapps/" />
|
||||
<copy file="build/addressbook.war" todir="pkg-temp/webapps/" />
|
||||
|
@ -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;
|
||||
@ -178,6 +179,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.
|
||||
@ -258,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 + "'");
|
||||
@ -331,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 {
|
||||
@ -364,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() {
|
||||
@ -410,6 +466,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 <code>null</code> if the
|
||||
* data was moved, and an error <code>String</code> otherwise.
|
||||
*/
|
||||
public String migrateFile(File signedFile, File outputFile) {
|
||||
if (!signedFile.exists())
|
||||
return "File not found: " + signedFile.getAbsolutePath();
|
||||
|
||||
FileInputStream fileInputStream = null;
|
||||
FileOutputStream fileOutputStream = null;
|
||||
|
||||
@ -610,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.
|
||||
*
|
||||
|
130
core/java/src/net/i2p/util/PartialEepGet.java
Normal file
130
core/java/src/net/i2p/util/PartialEepGet.java
Normal file
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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<String> {
|
||||
// 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<String> {
|
||||
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);
|
||||
|
@ -62,7 +62,7 @@ public class RouterContext extends I2PAppContext {
|
||||
private Calculator _capacityCalc;
|
||||
|
||||
|
||||
private static List _contexts = new ArrayList(1);
|
||||
private static List<RouterContext> _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<RouterContext> listContexts() { return _contexts; }
|
||||
|
||||
/** what router is this context working for? */
|
||||
public Router router() { return _router; }
|
||||
|
@ -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;
|
||||
|
||||
@ -32,6 +33,13 @@ public class ClientAppConfig {
|
||||
public String args;
|
||||
public long delay;
|
||||
public boolean disabled;
|
||||
/** @since 0.7.12 */
|
||||
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;
|
||||
clientName = client;
|
||||
@ -40,6 +48,14 @@ public class ClientAppConfig {
|
||||
disabled = dis;
|
||||
}
|
||||
|
||||
/** @since 0.7.12 */
|
||||
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) {
|
||||
String clientConfigFile = ctx.getProperty(PROP_CLIENT_CONFIG_FILENAME, DEFAULT_CLIENT_CONFIG_FILENAME);
|
||||
File cfgFile = new File(clientConfigFile);
|
||||
@ -72,6 +88,26 @@ public class ClientAppConfig {
|
||||
*/
|
||||
public static List<ClientAppConfig> getClientApps(RouterContext ctx) {
|
||||
Properties clientApps = getClientAppProps(ctx);
|
||||
return getClientApps(clientApps);
|
||||
}
|
||||
|
||||
/*
|
||||
* Go through the properties, and return a List of ClientAppConfig structures
|
||||
*/
|
||||
public static List<ClientAppConfig> 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<ClientAppConfig> getClientApps(Properties clientApps) {
|
||||
List<ClientAppConfig> rv = new ArrayList(8);
|
||||
int i = 0;
|
||||
while (true) {
|
||||
@ -83,6 +119,9 @@ 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");
|
||||
String stopargs = clientApps.getProperty(PREFIX + i + ".stopargs");
|
||||
String uninstallargs = clientApps.getProperty(PREFIX + i + ".uninstallargs");
|
||||
i++;
|
||||
boolean dis = disabled != null && "false".equals(disabled);
|
||||
|
||||
@ -94,11 +133,13 @@ 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, stopargs, uninstallargs));
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
/** classpath and stopargs not supported */
|
||||
public static void writeClientAppConfig(RouterContext ctx, List apps) {
|
||||
File cfgFile = configFile(ctx);
|
||||
FileOutputStream fos = null;
|
||||
|
@ -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() {
|
||||
|
Reference in New Issue
Block a user