I2P Address: [http://git.idk.i2p]

Skip to content
Snippets Groups Projects
To find the state of this project's repository at the time of any of these versions, check out the tags.
NewsFetcher.java 17.33 KiB
package net.i2p.router.web;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;

import net.i2p.I2PAppContext;
import net.i2p.crypto.TrustedUpdate;
import net.i2p.data.DataHelper;
import net.i2p.router.Router;
import net.i2p.router.RouterContext;
import net.i2p.router.RouterVersion;
import net.i2p.router.util.RFC822Date;
import net.i2p.util.EepGet;
import net.i2p.util.EepHead;
import net.i2p.util.FileUtil;
import net.i2p.util.Log;

/**
 * Task to periodically look for updates to the news.xml, and to keep
 * track of whether that has an announcement for a new version.
 */
public class NewsFetcher implements Runnable, EepGet.StatusListener {
    private final RouterContext _context;
    private final Log _log;
    private boolean _updateAvailable;
    private boolean _unsignedUpdateAvailable;
    private long _lastFetch;
    private long _lastUpdated;
    private String _updateVersion;
    private String _unsignedUpdateVersion;
    private String _lastModified;
    private boolean _invalidated;
    private final File _newsFile;
    private final File _tempFile;
    private static NewsFetcher _instance;
    private volatile boolean _isRunning;

    //public static final synchronized NewsFetcher getInstance() { return _instance; }
    public static final synchronized NewsFetcher getInstance(RouterContext ctx) { 
        if (_instance != null)
            return _instance;
        _instance = new NewsFetcher(ctx);
        return _instance;
    }

    private static final String NEWS_FILE = "docs/news.xml";
    private static final String TEMP_NEWS_FILE = "news.xml.temp";
    /** @since 0.7.14 not configurable */
    private static final String BACKUP_NEWS_URL = "http://www.i2p2.i2p/_static/news/news.xml";
    private static final String PROP_LAST_CHECKED = "router.newsLastChecked";
    /** @since 0.8.12 */
    private static final String PROP_LAST_HIDDEN = "routerconsole.newsLastHidden";
    
    private NewsFetcher(RouterContext ctx) {
        _context = ctx;
        _log = ctx.logManager().getLog(NewsFetcher.class);
        _instance = this;
        try {
            String last = ctx.getProperty(PROP_LAST_CHECKED);
            if (last != null)
                _lastFetch = Long.parseLong(last);
        } catch (NumberFormatException nfe) {}
        _newsFile = new File(_context.getRouterDir(), NEWS_FILE);
        _tempFile = new File(_context.getTempDir(), TEMP_NEWS_FILE);
        updateLastFetched();
        _updateVersion = "";
        _isRunning = true;
    }
    
    /** @since 0.8.8 */
    void shutdown() {
        _isRunning = false;
    }

    private void updateLastFetched() {
        if (_newsFile.exists()) {
            if (_lastUpdated == 0)
                _lastUpdated = _newsFile.lastModified();
            if (_lastFetch == 0)
                _lastFetch = _lastUpdated;
            if (_lastModified == null)
                _lastModified = RFC822Date.to822Date(_lastFetch);
        } else {
            _lastUpdated = 0;
            _lastFetch = 0;
            _lastModified = null;
        }
    }
    
    public boolean updateAvailable() { return _updateAvailable; }
    public String updateVersion() { return _updateVersion; }
    public boolean unsignedUpdateAvailable() { return _unsignedUpdateAvailable; }
    public String unsignedUpdateVersion() { return _unsignedUpdateVersion; }

    /**
     *  Is the news newer than the last time it was hidden?
     *  @since 0.8.12
     */
    public boolean shouldShowNews() {
        if (_lastUpdated <= 0)
            return true;
        String h = _context.getProperty(PROP_LAST_HIDDEN);
        if (h == null)
            return true;
        long last = 0;
        try {
            last = Long.parseLong(h);
        } catch (NumberFormatException nfe) {}
        return _lastUpdated > last;
    }

    /**
     *  Save config with the timestamp of the current news to hide, or 0 to show
     *  @since 0.8.12
     */
    public void showNews(boolean yes) {
        long stamp = yes ? 0 : _lastUpdated;
        _context.router().setConfigSetting(PROP_LAST_HIDDEN, Long.toString(stamp));
        _context.router().saveConfig();
    }

    /**
     *  @return HTML
     */
    public String status() {
         StringBuilder buf = new StringBuilder(128);
         long now = _context.clock().now();
         buf.append("<i>");
         if (_lastUpdated > 0) {
             buf.append(Messages.getString("News last updated {0} ago.",
                                           DataHelper.formatDuration2(now - _lastUpdated),
                                           _context))
                .append('\n');
         }
         if (_lastFetch > _lastUpdated) {
             buf.append(Messages.getString("News last checked {0} ago.",
                                           DataHelper.formatDuration2(now - _lastFetch),
                                           _context));
         }
         buf.append("</i>");
         String consoleNonce = System.getProperty("router.consoleNonce");
         if (_lastUpdated > 0 && consoleNonce != null) {
             if (shouldShowNews()) {
                 buf.append(" <a href=\"/?news=0&amp;consoleNonce=").append(consoleNonce).append("\">")
                    .append(Messages.getString("Hide news", _context));
             } else {
                 buf.append(" <a href=\"/?news=1&amp;consoleNonce=").append(consoleNonce).append("\">")
                    .append(Messages.getString("Show news", _context));
             }
             buf.append("</a>");
         }
         return buf.toString();
    }
    
    private static final long INITIAL_DELAY = 5*60*1000;
    private static final long RUN_DELAY = 10*60*1000;

    public void run() {
        try { Thread.sleep(INITIAL_DELAY + _context.random().nextLong(INITIAL_DELAY)); } catch (InterruptedException ie) {}
        while (_isRunning) {
            if (!_updateAvailable) checkForUpdates();
            if (shouldFetchNews()) {
                fetchNews();
                if (shouldFetchUnsigned())
                    fetchUnsignedHead();
            }
            try { Thread.sleep(RUN_DELAY); } catch (InterruptedException ie) {}
        }
    }
    
    boolean dontInstall() {
        File test = new File(_context.getBaseDir(), "history.txt");
        boolean readonly = ((test.exists() && !test.canWrite()) || (!_context.getBaseDir().canWrite()));
        boolean disabled = Boolean.valueOf(_context.getProperty(ConfigUpdateHandler.PROP_UPDATE_DISABLED)).booleanValue();
        return readonly || disabled;
    }

    private boolean shouldInstall() {
        String policy = _context.getProperty(ConfigUpdateHandler.PROP_UPDATE_POLICY);
        if ("notify".equals(policy) || dontInstall())
            return false;
        File zip = new File(_context.getRouterDir(), Router.UPDATE_FILE);
        return !zip.exists();
    }
    
    private boolean shouldFetchNews() {
        if (_invalidated)
            return true;
        updateLastFetched();
        String freq = _context.getProperty(ConfigUpdateHandler.PROP_REFRESH_FREQUENCY,
                                           ConfigUpdateHandler.DEFAULT_REFRESH_FREQUENCY);
        try {
            long ms = Long.parseLong(freq);
            if (ms <= 0)
                return false;
            
            if (_lastFetch + ms < _context.clock().now()) {
                return true;
            } else {
                if (_log.shouldLog(Log.DEBUG))
                    _log.debug("Last fetched " + DataHelper.formatDuration(_context.clock().now() - _lastFetch) + " ago");
                return false;
            }
        } catch (NumberFormatException nfe) {
            if (_log.shouldLog(Log.ERROR))
                _log.error("Invalid refresh frequency: " + freq);
            return false;
        }
    }

    /**
     *  Call this when changing news URLs to force an update next time the timer fires.
     *  @since 0.8.7
     */
    void invalidateNews() {
        _lastModified = null;
        _invalidated = true;
    }

    public void fetchNews() {
        String newsURL = ConfigUpdateHelper.getNewsURL(_context);
        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);
        if (_tempFile.exists())
            _tempFile.delete();
        
        try {
            EepGet get = null;
            if (shouldProxy)
                get = new EepGet(_context, true, proxyHost, proxyPort, 0, _tempFile.getAbsolutePath(), newsURL, true, null, _lastModified);
            else
                get = new EepGet(_context, false, null, 0, 0, _tempFile.getAbsolutePath(), newsURL, true, null, _lastModified);
            get.addStatusListener(this);
            if (get.fetch()) {
                _lastModified = get.getLastModified();
                _invalidated = false;
            } else {
                // backup news location - always proxied
                _tempFile.delete();
                get = new EepGet(_context, true, proxyHost, proxyPort, 0, _tempFile.getAbsolutePath(), BACKUP_NEWS_URL, true, null, _lastModified);
                get.addStatusListener(this);
                if (get.fetch())
                    _lastModified = get.getLastModified();
            }
        } catch (Throwable t) {
            _log.error("Error fetching the news", t);
        }
    }
    
    public boolean shouldFetchUnsigned() {
        String url = _context.getProperty(ConfigUpdateHandler.PROP_ZIP_URL);
        return url != null && url.length() > 0 &&
               Boolean.valueOf(_context.getProperty(ConfigUpdateHandler.PROP_UPDATE_UNSIGNED)).booleanValue() &&
               !dontInstall();
    }

    /**
     * HEAD the update url, and if the last-mod time is newer than the last update we
     * downloaded, as stored in the properties, then we download it using eepget.
     */
    public void fetchUnsignedHead() {
        String url = _context.getProperty(ConfigUpdateHandler.PROP_ZIP_URL);
        if (url == null || url.length() <= 0)
            return;
        // assume always proxied for now
        //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 {
            EepHead get = new EepHead(_context, proxyHost, proxyPort, 0, url);
            if (get.fetch()) {
                String lastmod = get.getLastModified();
                if (lastmod != null) {
                    long modtime = RFC822Date.parse822Date(lastmod);
                    if (modtime <= 0) return;
                    String lastUpdate = _context.getProperty(UpdateHandler.PROP_LAST_UPDATE_TIME);
                    if (lastUpdate == null) {
                        // we don't know what version you have, so stamp it with the current time,
                        // and we'll look for something newer next time around.
                        _context.router().setConfigSetting(UpdateHandler.PROP_LAST_UPDATE_TIME,
                                                           Long.toString(_context.clock().now()));
                        _context.router().saveConfig();
                        return;
                    }
                    long ms = 0;
                    try {
                        ms = Long.parseLong(lastUpdate);
                    } catch (NumberFormatException nfe) {}
                    if (ms <= 0) return;
                    if (modtime > ms) {
                        _unsignedUpdateAvailable = true;
                        // '07-Jul 21:09 UTC' with month name in the system locale
                        _unsignedUpdateVersion = (new SimpleDateFormat("dd-MMM HH:mm")).format(new Date(modtime)) + " UTC";
                        if (shouldInstall())
                            fetchUnsigned();
                    }
                }
            }
        } catch (Throwable t) {
            _log.error("Error fetching the unsigned update", t);
        }
    }

    public void fetchUnsigned() {
        String url = _context.getProperty(ConfigUpdateHandler.PROP_ZIP_URL);
        if (url == null || url.length() <= 0)
            return;
        UpdateHandler handler = new UnsignedUpdateHandler(_context, url,
                                                          _unsignedUpdateVersion);
        handler.update();
    }

    private static final String VERSION_STRING = "version=\"" + RouterVersion.VERSION + "\"";
    private static final String VERSION_PREFIX = "version=\"";
    private void checkForUpdates() {
        _updateAvailable = false;
        if ( (!_newsFile.exists()) || (_newsFile.length() <= 0) ) return;
        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 == -1) {
                    // skip
                } else {
                    int end = buf.indexOf("\"", index + VERSION_PREFIX.length());
                    if (end > index) {
                        String ver = buf.substring(index+VERSION_PREFIX.length(), end);
                        if (_log.shouldLog(Log.DEBUG))
                            _log.debug("Found version: [" + ver + "]");
                        if (TrustedUpdate.needsUpdate(RouterVersion.VERSION, ver)) {
                            if (_log.shouldLog(Log.DEBUG))
                                _log.debug("Our version is out of date, update!");
                            _updateVersion = ver;
                            break;
                        } else {
                            if (_log.shouldLog(Log.DEBUG))
                                _log.debug("Our version is current");
                            return;
                        }
                    }
                }
                if (buf.indexOf(VERSION_STRING) != -1) {
                    if (_log.shouldLog(Log.DEBUG))
                        _log.debug("Our version found, no need to update: " + buf.toString());
                    return;
                } 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) {}
        }
        // could not find version="0.5.0.1", so there must be an update ;)
        
        if (_log.shouldLog(Log.DEBUG))
            _log.debug("Our version was NOT found (" + RouterVersion.VERSION + "), update needed");
        _updateAvailable = !dontInstall();
        
        if (shouldInstall()) {
            if (_log.shouldLog(Log.DEBUG))
                _log.debug("Policy requests update, so we update");
            UpdateHandler handler = new UpdateHandler(_context);
            handler.update();
        } else {
            if (_log.shouldLog(Log.DEBUG))
                _log.debug("Policy requests manual update, so we do nothing");
        }
    }
    
    public void attemptFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt, int numRetries, Exception cause) {
        // ignore
    }
    public void bytesTransferred(long alreadyTransferred, int currentWrite, long bytesTransferred, long bytesRemaining, String url) {
        // ignore
    }
    public void transferComplete(long alreadyTransferred, long bytesTransferred, long bytesRemaining, String url, String outputFile, boolean notModified) {
        if (_log.shouldLog(Log.INFO))
            _log.info("News fetched from " + url + " with " + (alreadyTransferred+bytesTransferred));
        
        long now = _context.clock().now();
        if (_tempFile.exists()) {
            boolean copied = FileUtil.copy(_tempFile, _newsFile, true, false);
            if (copied) {
                _lastUpdated = now;
                _tempFile.delete();
                checkForUpdates();
            } else {
                if (_log.shouldLog(Log.ERROR))
                    _log.error("Failed to copy the news file!");
            }
        } else {
            if (_log.shouldLog(Log.WARN))
                _log.warn("Transfer complete, but no file? - probably 304 Not Modified");
        }
        _lastFetch = now;
        _context.router().setConfigSetting(PROP_LAST_CHECKED, Long.toString(now));
        _context.router().saveConfig();
    }
    
    public void transferFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt) {
        if (_log.shouldLog(Log.WARN))
            _log.warn("Failed to fetch the news from " + url);
        _tempFile.delete();
    }
    public void headerReceived(String url, int attemptNum, String key, String val) {}
    public void attempting(String url) {}
}