From b5a43ed60a8bc3afeed7fa2dc656236f771aa918 Sat Dec 12 15:00:00 2015 From: iwakeh Date: Sat, 12 Dec 2015 15:00:00 +0000 Subject: [PATCH] Patch for #17821 --- src/org/torproject/descriptor/ExitList.java | 26 ++++++ src/org/torproject/descriptor/ExitListEntry.java | 8 +- .../descriptor/impl/ExitListEntryImpl.java | 88 +++++++++++------- .../torproject/descriptor/impl/ExitListImpl.java | 77 +++++++++------ .../descriptor/impl/ExitListImplTest.java | 103 +++++++++++++++++++++ 5 files changed, 239 insertions(+), 63 deletions(-) create mode 100644 test/org/torproject/descriptor/impl/ExitListImplTest.java diff --git a/src/org/torproject/descriptor/ExitList.java b/src/org/torproject/descriptor/ExitList.java index 09d7c25..3807a13 100644 --- a/src/org/torproject/descriptor/ExitList.java +++ b/src/org/torproject/descriptor/ExitList.java @@ -2,15 +2,41 @@ * See LICENSE for licensing information */ package org.torproject.descriptor; +import java.util.Map; import java.util.Set; /* Exit list containing all known exit scan results at a given time. */ public interface ExitList extends Descriptor { + public final static String EOL = "\n"; + + /* Exit list entry containing results from a single exit scan. */ + public interface Entry { + + /* Return the scanned relay's fingerprint. */ + public String getFingerprint(); + + /* Return the publication time of the scanned relay's last known + * descriptor. */ + public long getPublishedMillis(); + + /* Return the publication time of the network status that this scan was + * based on. */ + public long getLastStatusMillis(); + + /* Return the IP addresses that were determined in the scan. */ + public Map getExitAddresses(); + } + /* Return the download time of the exit list. */ public long getDownloadedMillis(); /* Return the unordered set of exit scan results. */ + /* Use getEntries instead. */ + @Deprecated public Set getExitListEntries(); + + /* Return the unordered set of exit scan results. */ + public Set getEntries(); } diff --git a/src/org/torproject/descriptor/ExitListEntry.java b/src/org/torproject/descriptor/ExitListEntry.java index 201a172..4c01513 100644 --- a/src/org/torproject/descriptor/ExitListEntry.java +++ b/src/org/torproject/descriptor/ExitListEntry.java @@ -3,7 +3,9 @@ package org.torproject.descriptor; /* Exit list entry containing results from a single exit scan. */ -public interface ExitListEntry { +/* Use org.torproject.descriptor.ExitList.Entry instead. */ +@Deprecated +public interface ExitListEntry extends ExitList.Entry { /* Return the scanned relay's fingerprint. */ public String getFingerprint(); @@ -16,10 +18,10 @@ public interface ExitListEntry { * based on. */ public long getLastStatusMillis(); - /* Return the IP address that was determined in the scan. */ + /* Return one IP address that was determined in the scan. */ public String getExitAddress(); - /* Return the scan time. */ + /* Return the scan time of the ExitAddress returned by getExitAddress. */ public long getScanMillis(); } diff --git a/src/org/torproject/descriptor/impl/ExitListEntryImpl.java b/src/org/torproject/descriptor/impl/ExitListEntryImpl.java index a03e373..a0065ff 100644 --- a/src/org/torproject/descriptor/impl/ExitListEntryImpl.java +++ b/src/org/torproject/descriptor/impl/ExitListEntryImpl.java @@ -3,15 +3,19 @@ package org.torproject.descriptor.impl; import org.torproject.descriptor.DescriptorParseException; +import org.torproject.descriptor.ExitList; + import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Scanner; import java.util.SortedSet; import java.util.TreeSet; import org.torproject.descriptor.ExitListEntry; -public class ExitListEntryImpl implements ExitListEntry { +public class ExitListEntryImpl implements ExitListEntry, ExitList.Entry { private byte[] exitListEntryBytes; public byte[] getExitListEntryBytes() { @@ -37,56 +41,63 @@ public class ExitListEntryImpl implements ExitListEntry { this.checkAndClearKeywords(); } - private SortedSet exactlyOnceKeywords; + private SortedSet keywordCountingSet; private void initializeKeywords() { - this.exactlyOnceKeywords = new TreeSet(); - this.exactlyOnceKeywords.add("ExitNode"); - this.exactlyOnceKeywords.add("Published"); - this.exactlyOnceKeywords.add("LastStatus"); - this.exactlyOnceKeywords.add("ExitAddress"); + this.keywordCountingSet = new TreeSet(); + this.keywordCountingSet.add("ExitNode"); + this.keywordCountingSet.add("Published"); + this.keywordCountingSet.add("LastStatus"); + this.keywordCountingSet.add("ExitAddress"); } private void parsedExactlyOnceKeyword(String keyword) throws DescriptorParseException { - if (!this.exactlyOnceKeywords.contains(keyword)) { + if (!this.keywordCountingSet.contains(keyword)) { throw new DescriptorParseException("Duplicate '" + keyword + "' line in exit list entry."); } - this.exactlyOnceKeywords.remove(keyword); + this.keywordCountingSet.remove(keyword); } private void checkAndClearKeywords() throws DescriptorParseException { - for (String missingKeyword : this.exactlyOnceKeywords) { + for (String missingKeyword : this.keywordCountingSet) { throw new DescriptorParseException("Missing '" + missingKeyword + "' line in exit list entry."); } - this.exactlyOnceKeywords = null; + this.keywordCountingSet = null; } private void parseExitListEntryBytes() throws DescriptorParseException { Scanner s = new Scanner(new String(this.exitListEntryBytes)). - useDelimiter("\n"); + useDelimiter(ExitList.EOL); while (s.hasNext()) { String line = s.next(); String[] parts = line.split(" "); String keyword = parts[0]; - if (keyword.equals("ExitNode")) { - this.parseExitNodeLine(line, parts); - } else if (keyword.equals("Published")) { - this.parsePublishedLine(line, parts); - } else if (keyword.equals("LastStatus")) { - this.parseLastStatusLine(line, parts); - } else if (keyword.equals("ExitAddress")) { - this.parseExitAddressLine(line, parts); - } else if (this.failUnrecognizedDescriptorLines) { - throw new DescriptorParseException("Unrecognized line '" + line - + "' in exit list entry."); - } else { - if (this.unrecognizedLines == null) { - this.unrecognizedLines = new ArrayList(); - } - this.unrecognizedLines.add(line); + switch(keyword){ + case "ExitNode": + this.parseExitNodeLine(line, parts); + break; + case "Published": + this.parsePublishedLine(line, parts); + break; + case "LastStatus": + this.parseLastStatusLine(line, parts); + break; + case "ExitAddress": + this.parseExitAddressLine(line, parts); + break; + default: + if (this.failUnrecognizedDescriptorLines) { + throw new DescriptorParseException("Unrecognized line '" + line + + "' in exit list entry."); + } else { + if (this.unrecognizedLines == null) { + this.unrecognizedLines = new ArrayList(); + } + this.unrecognizedLines.add(line); + } } } } @@ -130,10 +141,9 @@ public class ExitListEntryImpl implements ExitListEntry { throw new DescriptorParseException("Invalid line '" + line + "' in " + "exit list entry."); } - this.parsedExactlyOnceKeyword(parts[0]); - this.exitAddress = ParseHelper.parseIpv4Address(line, parts[1]); - this.scanMillis = ParseHelper.parseTimestampAtIndex(line, parts, - 2, 3); + this.keywordCountingSet.remove(parts[0]); + exitAddresses.put(ParseHelper.parseIpv4Address(line, parts[1]), + ParseHelper.parseTimestampAtIndex(line, parts, 2, 3)); } private String fingerprint; @@ -153,12 +163,24 @@ public class ExitListEntryImpl implements ExitListEntry { private String exitAddress; public String getExitAddress() { + if(null == exitAddress){ + this.exitAddress = exitAddresses.entrySet().iterator().next().getKey(); + this.scanMillis = exitAddresses.entrySet().iterator().next().getValue(); + } return this.exitAddress; } + private Map exitAddresses = new HashMap<>(); + public Map getExitAddresses(){ + return new HashMap<>(this.exitAddresses); + } + private long scanMillis; public long getScanMillis() { - return this.scanMillis; + if(null == exitAddress){ + getExitAddress(); + } + return scanMillis; } } diff --git a/src/org/torproject/descriptor/impl/ExitListImpl.java b/src/org/torproject/descriptor/impl/ExitListImpl.java index 53dc112..8e12882 100644 --- a/src/org/torproject/descriptor/impl/ExitListImpl.java +++ b/src/org/torproject/descriptor/impl/ExitListImpl.java @@ -15,7 +15,6 @@ import java.util.TimeZone; import org.torproject.descriptor.ExitList; import org.torproject.descriptor.ExitListEntry; -/* TODO Add test class. */ public class ExitListImpl extends DescriptorImpl implements ExitList { protected ExitListImpl(byte[] rawDescriptorBytes, String fileName, @@ -52,36 +51,57 @@ public class ExitListImpl extends DescriptorImpl implements ExitList { throw new DescriptorParseException("Descriptor is empty."); } String descriptorString = new String(rawDescriptorBytes); - Scanner s = new Scanner(descriptorString).useDelimiter("\n"); + Scanner s = new Scanner(descriptorString).useDelimiter(EOL); StringBuilder sb = new StringBuilder(); + boolean firstEntry = true; while (s.hasNext()) { String line = s.next(); + if (line.startsWith("@")) { // skip annotation + if (!s.hasNext()) { + throw new DescriptorParseException("Descriptor is empty."); + } else { + line = s.next(); + } + } String[] parts = line.split(" "); String keyword = parts[0]; - if (keyword.equals("Downloaded")) { - this.downloadedMillis = ParseHelper.parseTimestampAtIndex(line, - parts, 1, 2); - } else if (keyword.equals("ExitNode")) { - sb = new StringBuilder(); - sb.append(line + "\n"); - } else if (keyword.equals("Published")) { - sb.append(line + "\n"); - } else if (keyword.equals("LastStatus")) { - sb.append(line + "\n"); - } else if (keyword.equals("ExitAddress")) { - String exitListEntryString = sb.toString() + line + "\n"; - byte[] exitListEntryBytes = exitListEntryString.getBytes(); - this.parseExitListEntry(exitListEntryBytes); - } else if (this.failUnrecognizedDescriptorLines) { - throw new DescriptorParseException("Unrecognized line '" + line - + "' in exit list."); - } else { - if (this.unrecognizedLines == null) { - this.unrecognizedLines = new ArrayList(); - } - this.unrecognizedLines.add(line); + switch (keyword){ + case "Downloaded": + this.downloadedMillis = ParseHelper.parseTimestampAtIndex(line, + parts, 1, 2); + break; + case "ExitNode": + if(!firstEntry){ + this.parseExitListEntry(sb.toString().getBytes()); + }else{ + firstEntry = false; + } + sb = new StringBuilder(); + sb.append(line).append(ExitList.EOL); + break; + case "Published": + sb.append(line).append(ExitList.EOL); + break; + case "LastStatus": + sb.append(line).append(ExitList.EOL); + break; + case "ExitAddress": + sb.append(line).append(ExitList.EOL); + break; + default: + if (this.failUnrecognizedDescriptorLines) { + throw new DescriptorParseException("Unrecognized line '" + line + + "' in exit list."); + } else { + if (this.unrecognizedLines == null) { + this.unrecognizedLines = new ArrayList(); + } + this.unrecognizedLines.add(line); + } } } + // parse the last descriptor + this.parseExitListEntry(sb.toString().getBytes()); } protected void parseExitListEntry(byte[] exitListEntryBytes) @@ -104,10 +124,13 @@ public class ExitListImpl extends DescriptorImpl implements ExitList { return this.downloadedMillis; } - private Set exitListEntries = - new HashSet(); + private Set exitListEntries = new HashSet<>(); public Set getExitListEntries() { - return new HashSet(this.exitListEntries); + return new HashSet<>(this.exitListEntries); + } + + public Set getEntries() { + return new HashSet(this.exitListEntries); } } diff --git a/test/org/torproject/descriptor/impl/ExitListImplTest.java b/test/org/torproject/descriptor/impl/ExitListImplTest.java new file mode 100644 index 0000000..7e4c4d6 --- /dev/null +++ b/test/org/torproject/descriptor/impl/ExitListImplTest.java @@ -0,0 +1,103 @@ +/* Copyright 2015 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.descriptor.impl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.List; +import java.util.Map; + +import org.junit.Test; + +import org.torproject.descriptor.Descriptor; + +public class ExitListImplTest { + + @Test() + public void testAnnotatedInput() throws Exception{ + ExitListImpl result = new ExitListImpl((tordnselAnnotation + input) + .getBytes("US-ASCII"), fileName, false); + assertEquals("Expected one annotation.", 1, + result.getAnnotations().size()); + assertEquals(tordnselAnnotation.substring(0, 18), + result.getAnnotations().get(0)); + assertEquals(1441065722000L, result.getDownloadedMillis()); + assertTrue("Unrecognized lines: " + result.getUnrecognizedLines(), + result.getUnrecognizedLines().isEmpty()); + assertEquals("Found: " + result.getExitListEntries(), + 5, result.getExitListEntries().size()); + assertEquals("Found: " + result.getExitListEntries(), + 5, result.getEntries().size()); + } + + @Test() + public void testMultipleExitAddresses() throws Exception{ + ExitListImpl result = new ExitListImpl( + (tordnselAnnotation + multiExitAddressInput) + .getBytes("US-ASCII"), fileName, false); + assertTrue("Unrecognized lines: " + result.getUnrecognizedLines(), + result.getUnrecognizedLines().isEmpty()); + Map map = result.getExitListEntries() + .iterator().next().getExitAddresses(); + assertEquals("Found: " + map, 3, map.size()); + assertTrue("Map: " + map, map.containsKey("81.7.17.171")); + assertTrue("Map: " + map, map.containsKey("81.7.17.172")); + assertTrue("Map: " + map, map.containsKey("81.7.17.173")); + } + + @Test() + public void testInsufficientInput() throws Exception{ + try{ + ExitListImpl result = + new ExitListImpl((tordnselAnnotation + insufficientInput) + .getBytes("US-ASCII"), fileName, false); + }catch(DescriptorParseException dpe){ + return; + } + fail("expected DescriptorParseException"); + } + + private static final String tordnselAnnotation = "@type tordnsel 1.0\n"; + private static final String fileName = "2015-09-01-00-02-02"; + private static final String insufficientInput = "Downloaded 2015-09-01 00:02:02\n" + + "ExitNode 0011BD2485AD45D984EC4159C88FC066E5E3300E\n" + + "Published 2015-08-31 16:17:30\n" + + "LastStatus 2015-08-31 17:03:18\n"; + private static final String multiExitAddressInput = + "Downloaded 2015-09-01 00:02:02\n" + + "ExitNode 0011BD2485AD45D984EC4159C88FC066E5E3300E\n" + + "Published 2015-08-31 16:17:30\n" + + "LastStatus 2015-08-31 17:03:18\n" + + "ExitAddress 81.7.17.171 2015-08-31 18:09:52\n" + + "ExitAddress 81.7.17.172 2015-08-31 18:10:52\n" + + "ExitAddress 81.7.17.173 2015-08-31 18:11:52\n"; + private static final String input = "Downloaded 2015-09-01 00:02:02\n" + + "ExitNode 0011BD2485AD45D984EC4159C88FC066E5E3300E\n" + + "Published 2015-08-31 16:17:30\n" + + "LastStatus 2015-08-31 17:03:18\n" + + "ExitAddress 162.247.72.201 2015-08-31 17:09:23\n" + + "ExitNode 0098C475875ABC4AA864738B1D1079F711C38287\n" + + "Published 2015-08-31 13:59:24\n" + + "LastStatus 2015-08-31 15:03:20\n" + + "ExitAddress 162.248.160.151 2015-08-31 15:07:27\n" + + "ExitNode 00C4B4731658D3B4987132A3F77100CFCB190D97\n" + + "Published 2015-08-31 17:47:52\n" + + "LastStatus 2015-08-31 18:03:17\n" + + "ExitAddress 81.7.17.171 2015-08-31 18:09:52\n" + + "ExitAddress 81.7.17.172 2015-08-31 18:10:52\n" + + "ExitAddress 81.7.17.173 2015-08-31 18:11:52\n" + + "ExitNode 00F2D93EBAF2F51D6EE4DCB0F37D91D72F824B16\n" + + "Published 2015-08-31 14:39:05\n" + + "LastStatus 2015-08-31 16:02:18\n" + + "ExitAddress 23.239.18.57 2015-08-31 16:06:07\n" + + "ExitNode 011B1D1E876B2C835D01FB9D407F2E00B28077F6\n" + + "Published 2015-08-31 05:14:35\n" + + "LastStatus 2015-08-31 06:03:29\n" + + "ExitAddress 104.131.51.150 2015-08-31 06:04:07\n"; +} + -- 2.0.0.head