/** * Modified from LoadClientAppsJob and I2PTunnelHTTPClientBase * All keys are mapped to lower case. * * @param args non-null * @since 0.9.4 */ private static Map<String, String> parseArgs(String args) { Map<String, String> rv = new HashMap<String, String>(8); char data[] = args.toCharArray(); StringBuilder buf = new StringBuilder(32); boolean isQuoted = false; String key = null; for (int i = 0; i < data.length; i++) { switch (data[i]) { case '\'': case '"': if (isQuoted) { // keys never quoted if (key != null) { rv.put(key, buf.toString().trim()); key = null; } buf.setLength(0); } isQuoted = !isQuoted; break; case ' ': case '\r': case '\n': case '\t': case ',': // whitespace - if we're in a quoted section, keep this as part of the quote, // otherwise use it as a delim if (isQuoted) { buf.append(data[i]); } else { if (key != null) { rv.put(key, buf.toString().trim()); key = null; } buf.setLength(0); } break; case '=': if (isQuoted) { buf.append(data[i]); } else { key = buf.toString().trim().toLowerCase(Locale.US); buf.setLength(0); } break; default: buf.append(data[i]); break; } } if (key != null) rv.put(key, buf.toString().trim()); return rv; }
/** * @return true on success * @throws just about anything, caller would be wise to catch Throwable */ public static boolean startPlugin(RouterContext ctx, String appName) throws Exception { Log log = ctx.logManager().getLog(PluginStarter.class); File pluginDir = new File(ctx.getConfigDir(), PLUGIN_DIR + '/' + appName); String iconfile = null; if ((!pluginDir.exists()) || (!pluginDir.isDirectory())) { log.error("Cannot start nonexistent plugin: " + appName); disablePlugin(appName); return false; } // Do we need to extract an update? File pluginUpdate = new File(ctx.getConfigDir(), PLUGIN_DIR + '/' + appName + "/app.xpi2p.zip"); if (pluginUpdate.exists()) { // Compare the start time of the router with the plugin. if (ctx.router().getWhenStarted() > pluginUpdate.lastModified()) { if (!FileUtil.extractZip(pluginUpdate, pluginDir)) { pluginUpdate.delete(); String foo = "Plugin '" + appName + "' failed to update! File '" + pluginUpdate + "' deleted. You may need to remove and install the plugin again."; log.error(foo); disablePlugin(appName); throw new Exception(foo); } else { pluginUpdate.delete(); // Need to always log this, and log.logAlways() did not work for me. System.err.println("INFO: Plugin updated: " + appName); } } // silently fail to update, because we have not restarted. } Properties props = pluginProperties(ctx, appName); String minVersion = ConfigClientsHelper.stripHTML(props, "min-i2p-version"); if (minVersion != null && VersionComparator.comp(CoreVersion.VERSION, minVersion) < 0) { String foo = "Plugin " + appName + " requires I2P version " + minVersion + " or higher"; log.error(foo); disablePlugin(appName); throw new Exception(foo); } minVersion = ConfigClientsHelper.stripHTML(props, "min-java-version"); if (minVersion != null && VersionComparator.comp(System.getProperty("java.version"), minVersion) < 0) { String foo = "Plugin " + appName + " requires Java version " + minVersion + " or higher"; log.error(foo); disablePlugin(appName); throw new Exception(foo); } String jVersion = LogsHelper.jettyVersion(); minVersion = ConfigClientsHelper.stripHTML(props, "min-jetty-version"); if (minVersion != null && VersionComparator.comp(minVersion, jVersion) > 0) { String foo = "Plugin " + appName + " requires Jetty version " + minVersion + " or higher"; log.error(foo); disablePlugin(appName); throw new Exception(foo); } String maxVersion = ConfigClientsHelper.stripHTML(props, "max-jetty-version"); if (maxVersion != null && VersionComparator.comp(maxVersion, jVersion) < 0) { String foo = "Plugin " + appName + " requires Jetty version " + maxVersion + " or lower"; log.error(foo); disablePlugin(appName); throw new Exception(foo); } if (log.shouldLog(Log.INFO)) log.info("Starting plugin: " + appName); // register themes File dir = new File(pluginDir, "console/themes"); File[] tfiles = dir.listFiles(); if (tfiles != null) { for (int i = 0; i < tfiles.length; i++) { String name = tfiles[i].getName(); if (tfiles[i].isDirectory() && (!Arrays.asList(STANDARD_THEMES).contains(tfiles[i]))) ctx.router() .setConfigSetting(ConfigUIHelper.PROP_THEME_PFX + name, tfiles[i].getAbsolutePath()); // we don't need to save } } // load and start things in clients.config File clientConfig = new File(pluginDir, "clients.config"); if (clientConfig.exists()) { Properties cprops = new Properties(); DataHelper.loadProps(cprops, clientConfig); List<ClientAppConfig> clients = ClientAppConfig.getClientApps(clientConfig); runClientApps(ctx, pluginDir, clients, "start"); } // start console webapps in console/webapps ContextHandlerCollection server = WebAppStarter.getConsoleServer(); if (server != null) { File consoleDir = new File(pluginDir, "console"); Properties wprops = RouterConsoleRunner.webAppProperties(consoleDir.getAbsolutePath()); File webappDir = new File(consoleDir, "webapps"); String fileNames[] = webappDir.list(RouterConsoleRunner.WarFilenameFilter.instance()); if (fileNames != null) { if (!pluginWars.containsKey(appName)) pluginWars.put(appName, new ConcurrentHashSet<String>()); 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 if (Arrays.asList(STANDARD_WEBAPPS).contains(warName)) { log.error("Skipping duplicate webapp " + warName + " in plugin " + appName); continue; } String enabled = wprops.getProperty(RouterConsoleRunner.PREFIX + warName + ENABLED); if (!"false".equals(enabled)) { if (log.shouldLog(Log.INFO)) log.info("Starting webapp: " + warName); String path = new File(webappDir, fileNames[i]).getCanonicalPath(); WebAppStarter.startWebApp(ctx, server, warName, path); pluginWars.get(appName).add(warName); } } catch (IOException ioe) { log.error("Error resolving '" + fileNames[i] + "' in '" + webappDir, ioe); } } // Check for iconfile in plugin.properties String icfile = ConfigClientsHelper.stripHTML(props, "console-icon"); if (icfile != null && !icfile.contains("..")) { StringBuilder buf = new StringBuilder(32); buf.append('/').append(appName); if (!icfile.startsWith("/")) buf.append('/'); buf.append(icfile); iconfile = buf.toString(); } } } else { log.error("No console web server to start plugins?"); } // 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 summary bar link 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) { String tip = ConfigClientsHelper.stripHTML(props, "consoleLinkTooltip_" + Messages.getLanguage(ctx)); if (tip == null) tip = ConfigClientsHelper.stripHTML(props, "consoleLinkTooltip"); NavHelper.registerApp(name, url, tip, iconfile); } return true; }
/** * Parse the installed (not the temp) news file for the latest version. * TODO: Real XML parsing * TODO: Check minVersion, use backup URLs specified */ void checkForUpdates() { FileInputStream in = null; try { in = new FileInputStream(_newsFile); StringBuilder buf = new StringBuilder(128); while (DataHelper.readLine(in, buf)) { int index = buf.indexOf(VERSION_PREFIX); if (index >= 0) { Map<String, String> args = parseArgs(buf.substring(index+VERSION_PREFIX.length())); String ver = args.get(VERSION_KEY); if (ver != null) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Found version: [" + ver + "]"); if (TrustedUpdate.needsUpdate(RouterVersion.VERSION, ver)) { if (NewsHelper.isUpdateDisabled(_context)) { String msg = _mgr._("In-network updates disabled. Check package manager."); _log.logAlways(Log.WARN, "Cannot update to version " + ver + ": " + msg); _mgr.notifyVersionConstraint(this, _currentURI, ROUTER_SIGNED, "", ver, msg); return; } if (NewsHelper.isBaseReadonly(_context)) { String msg = _mgr._("No write permission for I2P install directory."); _log.logAlways(Log.WARN, "Cannot update to version " + ver + ": " + msg); _mgr.notifyVersionConstraint(this, _currentURI, ROUTER_SIGNED, "", ver, msg); return; } String minRouter = args.get(MIN_VERSION_KEY); if (minRouter != null) { if (VersionComparator.comp(RouterVersion.VERSION, minRouter) < 0) { String msg = _mgr._("You must first update to version {0}", minRouter); _log.logAlways(Log.WARN, "Cannot update to version " + ver + ": " + msg); _mgr.notifyVersionConstraint(this, _currentURI, ROUTER_SIGNED, "", ver, msg); return; } } String minJava = args.get(MIN_JAVA_VERSION_KEY); if (minJava != null) { String ourJava = System.getProperty("java.version"); if (VersionComparator.comp(ourJava, minJava) < 0) { String msg = _mgr._("Requires Java version {0} but installed Java version is {1}", minJava, ourJava); _log.logAlways(Log.WARN, "Cannot update to version " + ver + ": " + msg); _mgr.notifyVersionConstraint(this, _currentURI, ROUTER_SIGNED, "", ver, msg); return; } } if (_log.shouldLog(Log.DEBUG)) _log.debug("Our version is out of date, update!"); // TODO if minversion > our version, continue // and look for a second entry with clearnet URLs // TODO clearnet URLs, notify with HTTP_CLEARNET and/or HTTPS_CLEARNET Map<UpdateMethod, List<URI>> sourceMap = new HashMap<UpdateMethod, List<URI>>(4); // Must do su3 first if (ConfigUpdateHandler.USE_SU3_UPDATE) { sourceMap.put(HTTP, _mgr.getUpdateURLs(ROUTER_SIGNED_SU3, "", HTTP)); addMethod(TORRENT, args.get(SU3_KEY), sourceMap); addMethod(HTTP_CLEARNET, args.get(CLEARNET_HTTP_SU3_KEY), sourceMap); addMethod(HTTPS_CLEARNET, args.get(CLEARNET_HTTPS_SU3_KEY), sourceMap); // notify about all sources at once _mgr.notifyVersionAvailable(this, _currentURI, ROUTER_SIGNED_SU3, "", sourceMap, ver, ""); sourceMap.clear(); } // now do sud/su2 sourceMap.put(HTTP, _mgr.getUpdateURLs(ROUTER_SIGNED, "", HTTP)); String key = FileUtil.isPack200Supported() ? SU2_KEY : SUD_KEY; addMethod(TORRENT, args.get(key), sourceMap); // notify about all sources at once _mgr.notifyVersionAvailable(this, _currentURI, ROUTER_SIGNED, "", sourceMap, ver, ""); } else { if (_log.shouldLog(Log.DEBUG)) _log.debug("Our version is current"); } return; } else { if (_log.shouldLog(Log.WARN)) _log.warn("No version in " + buf.toString()); } } else { if (_log.shouldLog(Log.DEBUG)) _log.debug("No match in " + buf.toString()); } buf.setLength(0); } } catch (IOException ioe) { if (_log.shouldLog(Log.WARN)) _log.warn("Error checking the news for an update", ioe); return; } finally { if (in != null) try { in.close(); } catch (IOException ioe) {} } if (_log.shouldLog(Log.WARN)) _log.warn("No version found in news.xml file"); }