forked from I2P_Developers/i2p.i2p
start of a plugin version checker
This commit is contained in:
@ -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>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import java.io.ByteArrayInputStream;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.SequenceInputStream;
|
import java.io.SequenceInputStream;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
@ -104,7 +105,7 @@ D8usM7Dxp5yrDrCYZ5AIijc=
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
private static final int VERSION_BYTES = 16;
|
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 final String PROP_TRUSTED_KEYS = "router.trustedUpdateKeys";
|
||||||
|
|
||||||
private static I2PAppContext _context;
|
private static I2PAppContext _context;
|
||||||
@ -274,7 +275,7 @@ D8usM7Dxp5yrDrCYZ5AIijc=
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static final void showVersionCLI(String signedFile) {
|
private static final void showVersionCLI(String signedFile) {
|
||||||
String versionString = new TrustedUpdate().getVersionString(new File(signedFile));
|
String versionString = getVersionString(new File(signedFile));
|
||||||
|
|
||||||
if (versionString.equals(""))
|
if (versionString.equals(""))
|
||||||
System.out.println("No version string found in file '" + signedFile + "'");
|
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
|
* @return The version string read, or an empty string if no version string
|
||||||
* is present.
|
* is present.
|
||||||
*/
|
*/
|
||||||
public String getVersionString(File signedFile) {
|
public static String getVersionString(File signedFile) {
|
||||||
FileInputStream fileInputStream = null;
|
FileInputStream fileInputStream = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -381,6 +382,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() */
|
/** version in the .sud file, valid only after calling migrateVerified() */
|
||||||
public String newVersion() {
|
public String newVersion() {
|
||||||
return _newVersion;
|
return _newVersion;
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user