diff --git a/apps/routerconsole/java/src/net/i2p/router/news/CRLEntry.java b/apps/routerconsole/java/src/net/i2p/router/news/CRLEntry.java new file mode 100644 index 0000000000000000000000000000000000000000..8b8f53935992bacfc372bd062afae7e2fb2839cb --- /dev/null +++ b/apps/routerconsole/java/src/net/i2p/router/news/CRLEntry.java @@ -0,0 +1,32 @@ +package net.i2p.router.news; + +import net.i2p.data.DataHelper; + +/** + * One CRL. + * Any String fields may be null. + * + * @since 0.9.26 + */ +public class CRLEntry { + public String data; + public String id; + public long updated; + + @Override + public boolean equals(Object o) { + if(o == null) + return false; + if(!(o instanceof CRLEntry)) + return false; + CRLEntry e = (CRLEntry) o; + return updated == e.updated && + DataHelper.eq(id, e.id) && + DataHelper.eq(data, e.data); + } + + @Override + public int hashCode() { + return (int) updated; + } +} diff --git a/apps/routerconsole/java/src/net/i2p/router/news/NewsXMLParser.java b/apps/routerconsole/java/src/net/i2p/router/news/NewsXMLParser.java index 5f35fa8c6dd62719376a17c0334bc62e4d92f528..534acb3053feb6d1a18a482ac7a8efc7a972278f 100644 --- a/apps/routerconsole/java/src/net/i2p/router/news/NewsXMLParser.java +++ b/apps/routerconsole/java/src/net/i2p/router/news/NewsXMLParser.java @@ -32,6 +32,7 @@ public class NewsXMLParser { private final I2PAppContext _context; private final Log _log; private List<NewsEntry> _entries; + private List<CRLEntry> _crlEntries; private NewsMetadata _metadata; private XHTMLMode _mode; @@ -145,11 +146,23 @@ public class NewsXMLParser { return _metadata; } + /** + * The news CRL entries. + * Must call parse() first. + * + * @return unsorted, null if none + * @since 0.9.26 + */ + public List<CRLEntry> getCRLEntries() { + return _crlEntries; + } + private void extract(Node root) throws I2PParserException { if (!root.getName().equals("feed")) throw new I2PParserException("no feed in XML"); _metadata = extractNewsMetadata(root); _entries = extractNewsEntries(root); + _crlEntries = extractCRLEntries(root); } private static NewsMetadata extractNewsMetadata(Node feed) throws I2PParserException { @@ -252,7 +265,7 @@ public class NewsXMLParser { /** * This does not check for any missing values. - * Any fields in any NewsEntry may be null. + * Any field in any NewsEntry may be null. */ private List<NewsEntry> extractNewsEntries(Node feed) throws I2PParserException { List<NewsEntry> rv = new ArrayList<NewsEntry>(); @@ -350,6 +363,40 @@ public class NewsXMLParser { return rv; } + /** + * This does not check for any missing values. + * Any field in any CRLEntry may be null. + * + * @return null if none + * @since 0.9.26 + */ + private List<CRLEntry> extractCRLEntries(Node feed) throws I2PParserException { + Node rev = feed.getNode("i2p:revocations"); + if (rev == null) + return null; + List<Node> entries = getNodes(rev, "i2p:crl"); + if (entries.isEmpty()) + return null; + List<CRLEntry> rv = new ArrayList<CRLEntry>(entries.size()); + for (Node entry : entries) { + CRLEntry e = new CRLEntry(); + String a = entry.getAttributeValue("id"); + if (a.length() > 0) + e.id = a; + a = entry.getAttributeValue("updated"); + if (a.length() > 0) { + long time = RFC3339Date.parse3339Date(a.trim()); + if (time > 0) + e.updated = time; + } + a = entry.getValue(); + if (a != null) + e.data = a.trim(); + rv.add(e); + } + return rv; + } + /** * Helper to get all Nodes matching the name * diff --git a/apps/routerconsole/java/src/net/i2p/router/update/NewsFetcher.java b/apps/routerconsole/java/src/net/i2p/router/update/NewsFetcher.java index f57d6d7c0dd24c6927266c2886c889cf551e9e01..39b42d105bcb0bac86390ca9e2c2c6e9caa6007b 100644 --- a/apps/routerconsole/java/src/net/i2p/router/update/NewsFetcher.java +++ b/apps/routerconsole/java/src/net/i2p/router/update/NewsFetcher.java @@ -23,9 +23,11 @@ import java.util.StringTokenizer; import net.i2p.app.ClientAppManager; import net.i2p.crypto.SU3File; import net.i2p.crypto.TrustedUpdate; +import net.i2p.data.Base64; import net.i2p.data.DataHelper; import net.i2p.router.RouterContext; import net.i2p.router.RouterVersion; +import net.i2p.router.news.CRLEntry; import net.i2p.router.news.NewsEntry; import net.i2p.router.news.NewsManager; import net.i2p.router.news.NewsMetadata; @@ -41,6 +43,7 @@ import net.i2p.util.FileUtil; import net.i2p.util.Log; import net.i2p.util.PortMapper; import net.i2p.util.ReusableGZIPInputStream; +import net.i2p.util.SecureFile; import net.i2p.util.SecureFileOutputStream; import net.i2p.util.SSLEepGet; import net.i2p.util.SystemVersion; @@ -509,6 +512,12 @@ class NewsFetcher extends UpdateRunner { nmgr.storeEntries(nodes); } } + // Persist any new CRL entries + List<CRLEntry> crlEntries = parser.getCRLEntries(); + if (crlEntries != null) + persistCRLEntries(crlEntries); + else + _log.info("No CRL entries found in news feed"); // store entries and metadata in old news.xml format String sudVersion = su3.getVersionString(); String signingKeyName = su3.getSignerString(); @@ -544,6 +553,52 @@ class NewsFetcher extends UpdateRunner { } } + /** + * Output any updated CRL entries + * + * @since 0.9.26 + */ + private void persistCRLEntries(List<CRLEntry> entries) { + File dir = new SecureFile(_context.getConfigDir(), "certificates"); + if (!dir.exists() && !dir.mkdir()) { + _log.error("Failed to create CRL directory " + dir); + return; + } + dir = new SecureFile(dir, "revocations"); + if (!dir.exists() && !dir.mkdir()) { + _log.error("Failed to create CRL directory " + dir); + return; + } + int i = 0; + for (CRLEntry e : entries) { + if (e.id == null || e.data == null) { + if (_log.shouldWarn()) + _log.warn("Bad CRL entry received"); + continue; + } + byte[] bid = DataHelper.getUTF8(e.id); + byte[] hash = new byte[32]; + _context.sha().calculateHash(bid, 0, bid.length, hash, 0); + String name = "crl-" + Base64.encode(hash) + ".crl"; + File f = new File(dir, name); + if (f.exists() && f.lastModified() >= e.updated) + continue; + OutputStream out = null; + try { + out = new SecureFileOutputStream(f); + out.write(DataHelper.getUTF8(e.data)); + } catch (IOException ioe) { + _log.error("Failed to write CRL", ioe); + } finally { + if (out != null) try { out.close(); } catch (IOException ioe) {} + } + f.setLastModified(e.updated); + i++; + } + if (i > 0) + _log.logAlways(Log.WARN, "Stored " + i + " new CRL " + (i > 1 ? "entries" : "entry")); + } + /** * Output in the old format. *