package net.i2p.router.update; import java.io.BufferedWriter; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FilterInputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.StringTokenizer; import net.i2p.crypto.SU3File; import net.i2p.crypto.TrustedUpdate; import net.i2p.data.DataHelper; import net.i2p.router.RouterContext; import net.i2p.router.RouterVersion; import net.i2p.router.news.NewsEntry; import net.i2p.router.news.NewsMetadata; import net.i2p.router.news.NewsXMLParser; import net.i2p.router.util.RFC822Date; import net.i2p.router.web.ConfigUpdateHandler; import net.i2p.router.web.NewsHelper; import net.i2p.update.*; import static net.i2p.update.UpdateType.*; import static net.i2p.update.UpdateMethod.*; import net.i2p.util.EepGet; import net.i2p.util.FileUtil; import net.i2p.util.Log; import net.i2p.util.ReusableGZIPInputStream; import net.i2p.util.SecureFileOutputStream; import net.i2p.util.SSLEepGet; import net.i2p.util.VersionComparator; /** * Task to fetch updates to the news.xml, and to keep * track of whether that has an announcement for a new version. * * @since 0.9.4 moved from NewsFetcher and make an Updater */ class NewsFetcher extends UpdateRunner { private String _lastModified; private final File _newsFile; private final File _tempFile; /** is the news newer */ private boolean _isNewer; private boolean _success; private static final String TEMP_NEWS_FILE = "news.xml.temp"; public NewsFetcher(RouterContext ctx, ConsoleUpdateManager mgr, List uris) { super(ctx, mgr, NEWS, uris); _newsFile = new File(ctx.getRouterDir(), NewsHelper.NEWS_FILE); _tempFile = new File(ctx.getTempDir(), "tmp-" + ctx.random().nextLong() + TEMP_NEWS_FILE); long lastMod = NewsHelper.lastChecked(ctx); if (lastMod > 0) _lastModified = RFC822Date.to822Date(lastMod); } @Override public void run() { _isRunning = true; try { fetchNews(); } finally { _mgr.notifyCheckComplete(this, _isNewer, _success); _isRunning = false; } } public void fetchNews() { boolean shouldProxy = _context.getProperty(ConfigUpdateHandler.PROP_SHOULD_PROXY_NEWS, ConfigUpdateHandler.DEFAULT_SHOULD_PROXY_NEWS); String proxyHost = _context.getProperty(ConfigUpdateHandler.PROP_PROXY_HOST, ConfigUpdateHandler.DEFAULT_PROXY_HOST); int proxyPort = ConfigUpdateHandler.proxyPort(_context); for (URI uri : _urls) { _currentURI = uri; String newsURL = uri.toString(); if (_tempFile.exists()) _tempFile.delete(); try { EepGet get; if (shouldProxy) get = new EepGet(_context, true, proxyHost, proxyPort, 0, _tempFile.getAbsolutePath(), newsURL, true, null, _lastModified); else if ("https".equals(uri.getScheme())) // no constructor w/ last mod check get = new SSLEepGet(_context, _tempFile.getAbsolutePath(), newsURL); else get = new EepGet(_context, false, null, 0, 0, _tempFile.getAbsolutePath(), newsURL, true, null, _lastModified); get.addStatusListener(this); long start = _context.clock().now(); if (get.fetch()) { int status = get.getStatusCode(); if (status == 200 || status == 304) { _context.router().saveConfig(NewsHelper.PROP_LAST_CHECKED, Long.toString(start)); return; } } } catch (Throwable t) { _log.error("Error fetching the news", t); } } } // Fake XML parsing // Line must contain this, and full entry must be on one line private static final String VERSION_PREFIX = "= 0) { Map 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> sourceMap = new HashMap>(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"); } /** * Modified from LoadClientAppsJob and I2PTunnelHTTPClientBase * All keys are mapped to lower case. * * @param args non-null * @since 0.9.4 */ private static Map parseArgs(String args) { Map rv = new HashMap(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; } private static List tokenize(String URLs) { StringTokenizer tok = new StringTokenizer(URLs, " ,\r\n"); List rv = new ArrayList(); while (tok.hasMoreTokens()) { try { rv.add(new URI(tok.nextToken().trim())); } catch (URISyntaxException use) {} } return rv; } /** * Parse URLs and add to the map * @param urls may be null * @since 0.9.9 */ private void addMethod(UpdateMethod method, String urls, Map> map) { if (urls != null) { List uris = tokenize(urls); if (!uris.isEmpty()) { Collections.shuffle(uris, _context.random()); map.put(method, uris); } } } /** override to prevent status update */ @Override public void bytesTransferred(long alreadyTransferred, int currentWrite, long bytesTransferred, long bytesRemaining, String url) {} /** * Copies the file from temp dir to the news location, * calls checkForUpdates() */ @Override 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()) { File from; if (url.endsWith(".su3")) { try { from = processSU3(); } catch (IOException ioe) { _log.error("Failed to extract the news file", ioe); _tempFile.delete(); return; } } else { from = _tempFile; } boolean copied = FileUtil.rename(from, _newsFile); _tempFile.delete(); if (copied) { String newVer = Long.toString(now); _context.router().saveConfig(NewsHelper.PROP_LAST_UPDATED, newVer); // fixme su3 version ? but it will be older than file version, which is older than now. _mgr.notifyVersionAvailable(this, _currentURI, NEWS, "", HTTP, null, newVer, ""); _isNewer = true; 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"); } _success = true; } /** override to prevent status update */ @Override public void transferFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt) {} /** * Process the fetched su3 news file _tempFile. * Handles 3 types of contained files: xml.gz (preferred), xml, and html (old format fake xml) * * @return the temp file contining the HTML-format news.xml * @since 0.9.17 */ private File processSU3() throws IOException { SU3File su3 = new SU3File(_context, _tempFile); // real xml, maybe gz, maybe not File to1 = new File(_context.getTempDir(), "tmp-" + _context.random().nextInt() + ".xml"); // real xml File to2 = new File(_context.getTempDir(), "tmp2-" + _context.random().nextInt() + ".xml"); try { su3.verifyAndMigrate(to1); int type = su3.getFileType(); if (su3.getContentType() != SU3File.CONTENT_NEWS) throw new IOException("bad content type: " + su3.getContentType()); if (type == SU3File.TYPE_HTML) return to1; if (type != SU3File.TYPE_XML && type != SU3File.TYPE_XML_GZ) throw new IOException("bad file type: " + type); File xml; if (type == SU3File.TYPE_XML_GZ) { gunzip(to1, to2); xml = to2; to1.delete(); } else { xml = to1; } NewsXMLParser parser = new NewsXMLParser(_context); parser.parse(xml); xml.delete(); NewsMetadata data = parser.getMetadata(); List entries = parser.getEntries(); String sudVersion = su3.getVersionString(); String signingKeyName = su3.getSignerString(); File to3 = new File(_context.getTempDir(), "tmp3-" + _context.random().nextInt() + ".xml"); outputOldNewsXML(data, entries, sudVersion, signingKeyName, to3); return to3; } finally { to2.delete(); } } /** * Gunzip the file * * @since 0.9.17 */ private static void gunzip(File from, File to) throws IOException { ReusableGZIPInputStream in = ReusableGZIPInputStream.acquire(); OutputStream out = null; try { in.initialize(new FileInputStream(from)); out = new SecureFileOutputStream(to); byte buf[] = new byte[4096]; int read; while ((read = in.read(buf)) != -1) { out.write(buf, 0, read); } } finally { if (out != null) try { out.close(); } catch (IOException ioe) {} ReusableGZIPInputStream.release(in); } } /** * Output in the old format. * * @since 0.9.17 */ private void outputOldNewsXML(NewsMetadata data, List entries, String sudVersion, String signingKeyName, File to) throws IOException { Writer out = null; try { out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(to), "UTF-8")); out.write("\n"); if (entries == null) return; for (NewsEntry e : entries) { if (e.title == null || e.content == null) continue; out.write("\n"); out.write("

"); out.write(e.title); out.write("

\n"); out.write(e.content); out.write("\n\n"); } } finally { if (out != null) try { out.close(); } catch (IOException ioe) {} } } }