package net.i2p.router.update; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.net.URI; import java.util.List; import java.util.Locale; import net.i2p.crypto.TrustedUpdate; import net.i2p.router.RouterContext; import net.i2p.router.RouterVersion; import net.i2p.router.web.ConfigUpdateHandler; import net.i2p.update.*; import static net.i2p.update.UpdateMethod.*; import net.i2p.util.EepGet; import net.i2p.util.I2PAppThread; import net.i2p.util.Log; import net.i2p.util.PartialEepGet; import net.i2p.util.PortMapper; import net.i2p.util.SSLEepGet; import net.i2p.util.VersionComparator; /** * The downloader for router signed updates, * and the base class for all the other Checkers and Runners. * * @since 0.9.4 moved from UpdateHandler * */ class UpdateRunner extends I2PAppThread implements UpdateTask, EepGet.StatusListener { protected final RouterContext _context; protected final Log _log; protected final ConsoleUpdateManager _mgr; protected final UpdateType _type; protected final UpdateMethod _method; protected final List<URI> _urls; protected final String _updateFile; protected volatile boolean _isRunning; protected boolean done; protected EepGet _get; /** tells the listeners what mode we are in - set to true in extending classes for checks */ protected boolean _isPartial; /** set by the listeners on completion */ protected String _newVersion; /** 56 byte header, only used for suds */ protected final ByteArrayOutputStream _baos; protected URI _currentURI; private final String _currentVersion; protected static final long CONNECT_TIMEOUT = 55*1000; protected static final long INACTIVITY_TIMEOUT = 5*60*1000; protected static final long NOPROXY_INACTIVITY_TIMEOUT = 60*1000; /** * Uses router version for partial checks */ public UpdateRunner(RouterContext ctx, ConsoleUpdateManager mgr, UpdateType type, List<URI> uris) { this(ctx, mgr, type, uris, RouterVersion.VERSION); } /** * Uses router version for partial checks * @since 0.9.9 */ public UpdateRunner(RouterContext ctx, ConsoleUpdateManager mgr, UpdateType type, UpdateMethod method, List<URI> uris) { this(ctx, mgr, type, method, uris, RouterVersion.VERSION); } /** * @param currentVersion used for partial checks * @since 0.9.7 */ public UpdateRunner(RouterContext ctx, ConsoleUpdateManager mgr, UpdateType type, List<URI> uris, String currentVersion) { this(ctx, mgr, type, HTTP, uris, currentVersion); } /** * @param method HTTP, HTTP_CLEARNET, or HTTPS_CLEARNET * @param currentVersion used for partial checks * @since 0.9.9 */ public UpdateRunner(RouterContext ctx, ConsoleUpdateManager mgr, UpdateType type, UpdateMethod method, List<URI> uris, String currentVersion) { super("Update Runner"); setDaemon(true); _context = ctx; _log = ctx.logManager().getLog(getClass()); _mgr = mgr; _type = type; _method = method; _urls = uris; _baos = new ByteArrayOutputStream(TrustedUpdate.HEADER_BYTES); _updateFile = (new File(ctx.getTempDir(), "update" + ctx.random().nextInt() + ".tmp")).getAbsolutePath(); _currentVersion = currentVersion; } //////// begin UpdateTask methods public boolean isRunning() { return _isRunning; } public void shutdown() { _isRunning = false; interrupt(); } public UpdateType getType() { return _type; } public UpdateMethod getMethod() { return _method; } public URI getURI() { return _currentURI; } public String getID() { return ""; } //////// end UpdateTask methods @Override public void run() { _isRunning = true; try { update(); } catch (Throwable t) { _mgr.notifyTaskFailed(this, "", t); } finally { _isRunning = false; } } /** * Loop through the entire list of update URLs. * For each one, first get the version from the first 56 bytes and see if * it is newer than what we are running now. * If it is, get the whole thing. */ protected void update() { // Do a PartialEepGet on the selected URL, check for version we expect, // and loop if it isn't what we want. // This will allows us to do a release without waiting for the last host to install the update. // Alternative: In bytesTransferred(), Check the data in the output file after // we've received at least 56 bytes. Need a cancel() method in EepGet ? boolean shouldProxy; String proxyHost; int proxyPort; boolean isSSL = false; if (_method == HTTP) { shouldProxy = _context.getProperty(ConfigUpdateHandler.PROP_SHOULD_PROXY, ConfigUpdateHandler.DEFAULT_SHOULD_PROXY); if (shouldProxy) { proxyHost = _context.getProperty(ConfigUpdateHandler.PROP_PROXY_HOST, ConfigUpdateHandler.DEFAULT_PROXY_HOST); proxyPort = ConfigUpdateHandler.proxyPort(_context); if (proxyPort == ConfigUpdateHandler.DEFAULT_PROXY_PORT_INT && proxyHost.equals(ConfigUpdateHandler.DEFAULT_PROXY_HOST) && _context.portMapper().getPort(PortMapper.SVC_HTTP_PROXY) < 0) { String msg = _t("HTTP client proxy tunnel must be running"); if (_log.shouldWarn()) _log.warn(msg); updateStatus("<b>" + msg + "</b>"); _mgr.notifyTaskFailed(this, msg, null); return; } } else { // TODO, wrong method, fail proxyHost = null; proxyPort = 0; } } else if (_method == HTTP_CLEARNET) { shouldProxy = false; proxyHost = null; proxyPort = 0; } else if (_method == HTTPS_CLEARNET) { shouldProxy = false; proxyHost = null; proxyPort = 0; isSSL = true; } else { throw new IllegalArgumentException(); } if (_urls.isEmpty()) { // not likely, don't bother translating String msg = "Update source list is empty, cannot download update"; updateStatus("<b>" + msg + "</b>"); _log.error(msg); _mgr.notifyTaskFailed(this, msg, null); return; } for (URI uri : _urls) { _currentURI = uri; String updateURL = uri.toString(); if ((_method == HTTP && !"http".equals(uri.getScheme())) || (_method == HTTP_CLEARNET && !"http".equals(uri.getScheme())) || (_method == HTTPS_CLEARNET && !"https".equals(uri.getScheme())) || uri.getHost() == null || (_method != HTTP && uri.getHost().toLowerCase(Locale.US).endsWith(".i2p"))) { if (_log.shouldLog(Log.WARN)) _log.warn("Bad update URI " + uri + " for method " + _method); continue; } updateStatus("<b>" + _t("Updating from {0}", linkify(updateURL)) + "</b>"); if (_log.shouldLog(Log.DEBUG)) _log.debug("Selected update URL: " + updateURL); // Check the first 56 bytes for the version // FIXME PartialEepGet works with clearnet but not with SSL _newVersion = null; if (!isSSL) { _isPartial = true; _baos.reset(); try { // no retries _get = new PartialEepGet(_context, proxyHost, proxyPort, _baos, updateURL, TrustedUpdate.HEADER_BYTES); _get.addStatusListener(UpdateRunner.this); _get.fetch(CONNECT_TIMEOUT); } catch (Throwable t) { } _isPartial = false; if (_newVersion == null) continue; } // Now get the whole thing try { if (shouldProxy) // 40 retries!! _get = new EepGet(_context, proxyHost, proxyPort, 40, _updateFile, updateURL, false); else if (isSSL) _get = new SSLEepGet(_context, _updateFile, updateURL); else _get = new EepGet(_context, 1, _updateFile, updateURL, false); _get.addStatusListener(UpdateRunner.this); _get.fetch(CONNECT_TIMEOUT, -1, shouldProxy ? INACTIVITY_TIMEOUT : NOPROXY_INACTIVITY_TIMEOUT); } catch (Throwable t) { _log.error("Error updating", t); } if (this.done) break; } (new File(_updateFile)).delete(); if (!this.done) _mgr.notifyTaskFailed(this, "", null); } // EepGet Listeners below. // We use the same for both the partial and the full EepGet, // with a couple of adjustments depending on which mode. public void attemptFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt, int numRetries, Exception cause) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Attempt failed on " + url, cause); // ignored _mgr.notifyAttemptFailed(this, url, null); } /** subclasses should override */ public void bytesTransferred(long alreadyTransferred, int currentWrite, long bytesTransferred, long bytesRemaining, String url) { if (_isPartial) return; long d = currentWrite + bytesTransferred; String status = "<b>" + _t("Updating") + "</b>"; _mgr.notifyProgress(this, status, d, d + bytesRemaining); } /** subclasses should override */ public void transferComplete(long alreadyTransferred, long bytesTransferred, long bytesRemaining, String url, String outputFile, boolean notModified) { if (_isPartial) { // Compare version with what we have now String newVersion = TrustedUpdate.getVersionString(new ByteArrayInputStream(_baos.toByteArray())); boolean newer = VersionComparator.comp(newVersion, _currentVersion) > 0; if (newer) { _newVersion = newVersion; } else { updateStatus("<b>" + _t("No new version found at {0}", linkify(url)) + "</b>"); if (_log.shouldLog(Log.WARN)) _log.warn("Found old version \"" + newVersion + "\" at " + url); } return; } // FIXME if we didn't do a partial, we don't know if (_newVersion == null) _newVersion = "unknown"; File tmp = new File(_updateFile); if (_mgr.notifyComplete(this, _newVersion, tmp)) this.done = true; else tmp.delete(); // corrupt } /** subclasses should override */ public void transferFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt) { // don't display bytesTransferred as it is meaningless if (_log.shouldLog(Log.WARN)) _log.warn("Update from " + url + " did not download completely (" + bytesRemaining + " remaining after " + currentAttempt + " tries)"); updateStatus("<b>" + _t("Transfer failed from {0}", linkify(url)) + "</b>"); _mgr.notifyAttemptFailed(this, url, null); // update() will call notifyTaskFailed() after last URL } public void headerReceived(String url, int attemptNum, String key, String val) {} public void attempting(String url) {} protected void updateStatus(String s) { _mgr.notifyProgress(this, s); } protected static String linkify(String url) { return ConsoleUpdateManager.linkify(url); } /** translate a string */ protected String _t(String s) { return _mgr._t(s); } /** * translate a string with a parameter */ protected String _t(String s, Object o) { return _mgr._t(s, o); } @Override public String toString() { return getClass().getName() + ' ' + getType() + ' ' + getID() + ' ' + getMethod() + ' ' + getURI(); } }