diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateChecker.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateChecker.java new file mode 100644 index 0000000000000000000000000000000000000000..e19e23d91f1b9cfc3f068a384f2eebedb2af1cb8 --- /dev/null +++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateChecker.java @@ -0,0 +1,139 @@ +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 update, plugin {0} is not installed", appName) + "</b>"); + return; + } + + if (_pluginUpdateCheckerRunner == null) + _pluginUpdateCheckerRunner = new PluginUpdateCheckerRunner(xpi2pURL); + if (_pluginUpdateCheckerRunner.isRunning()) + return; + _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 { + String _updateURL; + ByteArrayOutputStream _baos; + + public PluginUpdateCheckerRunner(String url) { + super(); + _updateURL = url; + _baos = new ByteArrayOutputStream(TrustedUpdate.HEADER_BYTES); + } + + @Override + protected void update() { + updateStatus("<b>" + _("Checking plugin {0} for updates", _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>"); + } + } +} + diff --git a/core/java/src/net/i2p/crypto/TrustedUpdate.java b/core/java/src/net/i2p/crypto/TrustedUpdate.java index 133a18033365d45ed8aa177ad22a0ee5745ef7a7..c567fcb3100445b0acb4cefd0bdbe05e2fc2aae2 100644 --- a/core/java/src/net/i2p/crypto/TrustedUpdate.java +++ b/core/java/src/net/i2p/crypto/TrustedUpdate.java @@ -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; @@ -274,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 + "'"); @@ -347,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 { @@ -380,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() { diff --git a/core/java/src/net/i2p/util/PartialEepGet.java b/core/java/src/net/i2p/util/PartialEepGet.java new file mode 100644 index 0000000000000000000000000000000000000000..5b18bc7619ffbdc4ebe2e749243d5f9186735e04 --- /dev/null +++ b/core/java/src/net/i2p/util/PartialEepGet.java @@ -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(); + } +}