diff --git a/apps/BOB/src/net/i2p/BOB/BOB.java b/apps/BOB/src/net/i2p/BOB/BOB.java index 8277fd2592..6dbbe3712c 100644 --- a/apps/BOB/src/net/i2p/BOB/BOB.java +++ b/apps/BOB/src/net/i2p/BOB/BOB.java @@ -119,15 +119,16 @@ public class BOB implements Runnable, ClientApp { public final static String PROP_BOB_HOST = "BOB.host"; public final static String PROP_CFG_VER = "BOB.CFG.VER"; + /** unused when started via the ClientApp interface */ private static BOB _bob; - private NamedDB database; - private Properties props = new Properties(); - private AtomicBoolean spin = new AtomicBoolean(true); + private final NamedDB database; + private final Properties props = new Properties(); + private final AtomicBoolean spin = new AtomicBoolean(true); private static final String P_RUNNING = "RUNNING"; private static final String P_STARTING = "STARTING"; private static final String P_STOPPING = "STOPPING"; - private AtomicBoolean lock = new AtomicBoolean(false); + private final AtomicBoolean lock = new AtomicBoolean(false); // no longer used. // private static int maxConnections = 0; @@ -143,8 +144,9 @@ public class BOB implements Runnable, ClientApp { * Stop BOB gracefully * @deprecated unused */ - public static void stop() { - _bob.shutdown(null); + public synchronized static void stop() { + if (_bob != null) + _bob.shutdown(null); } /** @@ -189,7 +191,7 @@ public class BOB implements Runnable, ClientApp { * * @param args */ - public static void main(String[] args) { + public synchronized static void main(String[] args) { try { _bob = new BOB(I2PAppContext.getGlobalContext(), null, args); _bob.startup(); diff --git a/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateHandler.java index 64304bd3bc..f327f739da 100644 --- a/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateHandler.java @@ -43,7 +43,9 @@ class PluginUpdateHandler implements Checker, Updater { Properties props = PluginStarter.pluginProperties(_context, appName); String oldVersion = props.getProperty("version"); - String xpi2pURL = props.getProperty("updateURL"); + String xpi2pURL = props.getProperty("updateURL.su3"); + if (xpi2pURL == null) + xpi2pURL = props.getProperty("updateURL"); List updateSources = null; if (xpi2pURL != null) { try { diff --git a/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateRunner.java b/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateRunner.java index 580948fc03..65e9f5584a 100644 --- a/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateRunner.java +++ b/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateRunner.java @@ -1,6 +1,7 @@ package net.i2p.router.update; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.lang.IllegalArgumentException; import java.net.URI; @@ -9,6 +10,7 @@ import java.util.Map; import java.util.Properties; import net.i2p.CoreVersion; +import net.i2p.crypto.SU3File; import net.i2p.crypto.TrustedUpdate; import net.i2p.data.DataFormatException; import net.i2p.data.DataHelper; @@ -121,7 +123,6 @@ class PluginUpdateRunner extends UpdateRunner { @Override public void transferComplete(long alreadyTransferred, long bytesTransferred, long bytesRemaining, String url, String outputFile, boolean notModified) { - boolean update = false; updateStatus("" + _("Plugin downloaded") + ""); File f = new File(_updateFile); File appDir = new SecureDirectory(_context.getConfigDir(), PLUGIN_DIR); @@ -130,7 +131,43 @@ class PluginUpdateRunner extends UpdateRunner { statusDone("" + _("Cannot create plugin directory {0}", appDir.getAbsolutePath()) + ""); return; } + boolean isSU3; + try { + isSU3 = isSU3File(f); + } catch (IOException ioe) { + f.delete(); + statusDone("" + ioe + ""); + return; + } + if (isSU3) + processSU3(f, appDir, url); + else + processSUD(f, appDir, url); + } + /** + * @since 0.9.15 + * @return if SU3 + */ + private static boolean isSU3File(File f) throws IOException { + FileInputStream fis = null; + try { + fis = new FileInputStream(f); + for (int i = 0; i < SU3File.MAGIC.length(); i++) { + if (fis.read() != SU3File.MAGIC.charAt(i)) + return false; + } + return true; + } finally { + if (fis != null) try { fis.close(); } catch (IOException ioe) {} + } + } + + /** + * @since 0.9.15 + * @return success + */ + private void processSUD(File f, File appDir, String url) { 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 @@ -141,27 +178,9 @@ class PluginUpdateRunner extends UpdateRunner { to.delete(); return; } - File tempDir = new File(_context.getTempDir(), "tmp" + _context.random().nextInt() + "-unzip"); - if (!FileUtil.extractZip(to, tempDir, Log.ERROR)) { - f.delete(); - to.delete(); - FileUtil.rmdir(tempDir, false); - statusDone("" + _("Plugin from {0} is corrupt", url) + ""); + Properties props = getPluginConfig(f, to, url); + if (props == null) 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); - statusDone("" + _("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"); @@ -251,7 +270,90 @@ class PluginUpdateRunner extends UpdateRunner { String sudVersion = TrustedUpdate.getVersionString(f); f.delete(); + processFinal(to, appDir, url, props, sudVersion, pubkey, signer); + } + /** + * @since 0.9.15 + */ + private void processSU3(File f, File appDir, String url) { + SU3File su3 = new SU3File(_context, f); + File to = new File(_context.getTempDir(), "tmp" + _context.random().nextInt() + ZIP); + String sudVersion; + String signingKeyName; + try { + su3.verifyAndMigrate(to); + if (su3.getFileType() != SU3File.TYPE_ZIP) + throw new IOException("bad file type"); + if (su3.getContentType() != SU3File.CONTENT_PLUGIN) + throw new IOException("bad content type"); + sudVersion = su3.getVersionString(); + signingKeyName = su3.getSignerString(); + } catch (IOException ioe) { + statusDone("" + ioe + ' ' + _("from {0}", url) + " "); + f.delete(); + to.delete(); + return; + } + Properties props = getPluginConfig(f, to, url); + if (props == null) + return; + String signer = props.getProperty("signer"); + if (signer == null || signer.length() <= 0) { + f.delete(); + to.delete(); + statusDone("" + _("Plugin from {0} contains an invalid key", url) + ""); + return; + } + if (!signer.equals(signingKeyName)) { + f.delete(); + to.delete(); + if (signingKeyName == null) + _log.error("Failed to verify plugin signature, corrupt plugin or bad signature, signed by: " + signer); + else + // shouldn't happen + _log.error("Plugin signer \"" + signer + "\" does not match new signer in plugin.config file \"" + signingKeyName + "\""); + statusDone("" + _("Plugin signature verification of {0} failed", url) + ""); + return; + } + processFinal(to, appDir, url, props, sudVersion, null, signer); + } + + /** + * @since 0.9.15 + * @return null on error + */ + private Properties getPluginConfig(File f, File to, String url) { + File tempDir = new File(_context.getTempDir(), "tmp" + _context.random().nextInt() + "-unzip"); + if (!FileUtil.extractZip(to, tempDir, Log.ERROR)) { + f.delete(); + to.delete(); + FileUtil.rmdir(tempDir, false); + statusDone("" + _("Plugin from {0} is corrupt", url) + ""); + return null; + } + File installProps = new File(tempDir, "plugin.config"); + Properties props = new OrderedProperties(); + try { + DataHelper.loadProps(props, installProps); + } catch (IOException ioe) { + f.delete(); + to.delete(); + statusDone("" + _("Plugin from {0} does not contain the required configuration file", url) + ""); + return null; + } finally { + // we don't need this anymore, we will unzip again + FileUtil.rmdir(tempDir, false); + } + return props; + } + + /** + * @param pubkey null OK for su3 + * @since 0.9.15 + */ + private void processFinal(File to, File appDir, String url, Properties props, String sudVersion, String pubkey, String signer) { + boolean update = false; String appName = props.getProperty("name"); String version = props.getProperty("version"); if (appName == null || version == null || appName.length() <= 0 || version.length() <= 0 || @@ -302,14 +404,13 @@ class PluginUpdateRunner extends UpdateRunner { DataHelper.loadProps(oldProps, oldPropFile); } catch (IOException ioe) { to.delete(); - FileUtil.rmdir(tempDir, false); statusDone("" + _("Installed plugin does not contain the required configuration file", url) + ""); return; } String oldPubkey = oldProps.getProperty("key"); String oldKeyName = oldProps.getProperty("signer"); String oldAppName = oldProps.getProperty("name"); - if ((!pubkey.equals(oldPubkey)) || (!signer.equals(oldKeyName)) || (!appName.equals(oldAppName))) { + if ((pubkey != null && !pubkey.equals(oldPubkey)) || (!signer.equals(oldKeyName)) || (!appName.equals(oldAppName))) { to.delete(); statusDone("" + _("Signature of downloaded plugin does not match installed plugin") + ""); return; 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 aac87fbfb9..48343ff199 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java @@ -390,7 +390,9 @@ public class ConfigClientsHandler extends FormHandler { private void updatePlugin(String app) { Properties props = PluginStarter.pluginProperties(_context, app); - String url = props.getProperty("updateURL"); + String url = props.getProperty("updateURL.su3"); + if (url == null) + url = props.getProperty("updateURL"); if (url == null) { addFormError(_("No update URL specified for {0}",app)); return; diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java index 4d0adeca68..c9620cb7a9 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java @@ -248,7 +248,9 @@ public class ConfigClientsHelper extends HelperBase { desc.append("") .append("").append(_("Website")).append(" "); } - String updateURL = stripHTML(appProps, "updateURL"); + String updateURL = stripHTML(appProps, "updateURL.su3"); + if (updateURL == null) + updateURL = stripHTML(appProps, "updateURL"); if (updateURL != null) { desc.append("") .append("").append(_("Update link")).append(" "); diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHelper.java index d3450eb6f3..efe9f4f819 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHelper.java @@ -298,9 +298,9 @@ public class ConfigNetHelper extends HelperBase { StringBuilder buf = new StringBuilder(256); buf.append("