commit 5c1494966f2af90f4bdb024c3c561cccad23b24e Author: Karsten Loesing karsten.loesing@gmx.net Date: Thu Feb 2 08:11:15 2012 +0100
Add support for parsing exit lists. --- src/org/torproject/descriptor/ExitList.java | 16 ++ src/org/torproject/descriptor/ExitListEntry.java | 25 +++ .../torproject/descriptor/impl/DescriptorImpl.java | 3 + .../descriptor/impl/ExitListEntryImpl.java | 170 ++++++++++++++++++++ .../torproject/descriptor/impl/ExitListImpl.java | 121 ++++++++++++++ 5 files changed, 335 insertions(+), 0 deletions(-)
diff --git a/src/org/torproject/descriptor/ExitList.java b/src/org/torproject/descriptor/ExitList.java new file mode 100644 index 0000000..5372b8b --- /dev/null +++ b/src/org/torproject/descriptor/ExitList.java @@ -0,0 +1,16 @@ +/* Copyright 2012 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.descriptor; + +import java.util.Set; + +/* Exit list containing all known exit scan results at a given time. */ +public interface ExitList extends Descriptor { + + /* Return the publication time of the exit list. */ + public long getPublishedMillis(); + + /* Return the unordered set of exit scan results. */ + public Set<ExitListEntry> getExitListEntries(); +} + diff --git a/src/org/torproject/descriptor/ExitListEntry.java b/src/org/torproject/descriptor/ExitListEntry.java new file mode 100644 index 0000000..74438de --- /dev/null +++ b/src/org/torproject/descriptor/ExitListEntry.java @@ -0,0 +1,25 @@ +/* Copyright 2012 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.descriptor; + +/* Exit list entry containing results from a single exit scan. */ +public interface ExitListEntry { + + /* 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 address that was determined in the scan. */ + public String getExitAddress(); + + /* Return the scan time. */ + public long getScanMillis(); +} + diff --git a/src/org/torproject/descriptor/impl/DescriptorImpl.java b/src/org/torproject/descriptor/impl/DescriptorImpl.java index 7db8aaf..e173612 100644 --- a/src/org/torproject/descriptor/impl/DescriptorImpl.java +++ b/src/org/torproject/descriptor/impl/DescriptorImpl.java @@ -63,6 +63,9 @@ public abstract class DescriptorImpl implements Descriptor { failUnrecognizedDescriptorLines)); } else if (firstLines.startsWith("dir-key-certificate-version ")) { /* TODO Implement parsing of directory certificates. */ + } else if (firstLines.startsWith("ExitNode ")) { + parsedDescriptors.add(new ExitListImpl(rawDescriptorBytes, fileName, + failUnrecognizedDescriptorLines)); } else { throw new DescriptorParseException("Could not detect descriptor " + "type in descriptor starting with '" + firstLines + "'."); diff --git a/src/org/torproject/descriptor/impl/ExitListEntryImpl.java b/src/org/torproject/descriptor/impl/ExitListEntryImpl.java new file mode 100644 index 0000000..b3ed8f1 --- /dev/null +++ b/src/org/torproject/descriptor/impl/ExitListEntryImpl.java @@ -0,0 +1,170 @@ +/* Copyright 2012 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.descriptor.impl; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.torproject.descriptor.ExitListEntry; + +public class ExitListEntryImpl implements ExitListEntry { + + private byte[] exitListEntryBytes; + public byte[] getExitListEntryBytes() { + return this.exitListEntryBytes; + } + + private boolean failUnrecognizedDescriptorLines; + private List<String> unrecognizedLines; + protected List<String> getAndClearUnrecognizedLines() { + List<String> lines = this.unrecognizedLines; + this.unrecognizedLines = null; + return lines; + } + + protected ExitListEntryImpl(byte[] exitListEntryBytes, + boolean failUnrecognizedDescriptorLines) + throws DescriptorParseException { + this.exitListEntryBytes = exitListEntryBytes; + this.failUnrecognizedDescriptorLines = + failUnrecognizedDescriptorLines; + this.initializeKeywords(); + this.parseExitListEntryBytes(); + this.checkKeywords(); + } + + private SortedSet<String> exactlyOnceKeywords; + private void initializeKeywords() { + this.exactlyOnceKeywords = new TreeSet<String>(); + this.exactlyOnceKeywords.add("ExitNode"); + this.exactlyOnceKeywords.add("Published"); + this.exactlyOnceKeywords.add("LastStatus"); + this.exactlyOnceKeywords.add("ExitAddress"); + } + + private void parsedExactlyOnceKeyword(String keyword) + throws DescriptorParseException { + if (!this.exactlyOnceKeywords.contains(keyword)) { + throw new DescriptorParseException("Duplicate '" + keyword + + "' line in exit list entry."); + } + this.exactlyOnceKeywords.remove(keyword); + } + + private void checkKeywords() throws DescriptorParseException { + for (String missingKeyword : this.exactlyOnceKeywords) { + throw new DescriptorParseException("Missing '" + missingKeyword + + "' line in exit list entry."); + } + } + + private void parseExitListEntryBytes() + throws DescriptorParseException { + try { + BufferedReader br = new BufferedReader(new StringReader( + new String(this.exitListEntryBytes))); + String line; + while ((line = br.readLine()) != null) { + 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<String>(); + } + this.unrecognizedLines.add(line); + } + } + } catch (IOException e) { + throw new RuntimeException("Internal error: Ran into an " + + "IOException while parsing a String in memory. Something's " + + "really wrong.", e); + } + } + + private void parseExitNodeLine(String line, String[] parts) + throws DescriptorParseException { + if (parts.length != 2) { + throw new DescriptorParseException("Invalid line '" + line + "' in " + + "exit list entry."); + } + this.parsedExactlyOnceKeyword(parts[0]); + this.fingerprint = ParseHelper.parseTwentyByteHexString(line, + parts[1]); + } + + private void parsePublishedLine(String line, String[] parts) + throws DescriptorParseException { + if (parts.length != 3) { + throw new DescriptorParseException("Invalid line '" + line + "' in " + + "exit list entry."); + } + this.parsedExactlyOnceKeyword(parts[0]); + this.publishedMillis = ParseHelper.parseTimestampAtIndex(line, parts, + 1, 2); + } + + private void parseLastStatusLine(String line, String[] parts) + throws DescriptorParseException { + if (parts.length != 3) { + throw new DescriptorParseException("Invalid line '" + line + "' in " + + "exit list entry."); + } + this.parsedExactlyOnceKeyword(parts[0]); + this.lastStatusMillis = ParseHelper.parseTimestampAtIndex(line, parts, + 1, 2); + } + + private void parseExitAddressLine(String line, String[] parts) + throws DescriptorParseException { + if (parts.length != 4) { + 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); + } + + private String fingerprint; + public String getFingerprint() { + return this.fingerprint; + } + + private long publishedMillis; + public long getPublishedMillis() { + return this.publishedMillis; + } + + private long lastStatusMillis; + public long getLastStatusMillis() { + return this.lastStatusMillis; + } + + private String exitAddress; + public String getExitAddress() { + return this.exitAddress; + } + + private long scanMillis; + public long getScanMillis() { + return this.scanMillis; + } +} + diff --git a/src/org/torproject/descriptor/impl/ExitListImpl.java b/src/org/torproject/descriptor/impl/ExitListImpl.java new file mode 100644 index 0000000..4861fa0 --- /dev/null +++ b/src/org/torproject/descriptor/impl/ExitListImpl.java @@ -0,0 +1,121 @@ +/* Copyright 2012 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.descriptor.impl; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +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, + boolean failUnrecognizedDescriptorLines) + throws DescriptorParseException { + super(rawDescriptorBytes, failUnrecognizedDescriptorLines); + this.setPublishedMillisFromFileName(fileName); + this.splitAndParseExitListEntries(rawDescriptorBytes); + } + + private void setPublishedMillisFromFileName(String fileName) + throws DescriptorParseException { + if (fileName.length() == "2012-02-01-04-06-24".length()) { + try { + SimpleDateFormat fileNameFormat = new SimpleDateFormat( + "yyyy-MM-dd-HH-mm-ss"); + fileNameFormat.setLenient(false); + fileNameFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + this.publishedMillis = fileNameFormat.parse(fileName).getTime(); + } catch (ParseException e) { + /* Handle below. */ + } + } + if (this.publishedMillis == 0L) { + throw new DescriptorParseException("Unrecognized exit list file " + + "name '" + fileName + "'."); + } + } + + private void splitAndParseExitListEntries(byte[] rawDescriptorBytes) + throws DescriptorParseException { + if (this.rawDescriptorBytes.length == 0) { + throw new DescriptorParseException("Descriptor is empty."); + } + String descriptorString = new String(rawDescriptorBytes); + if (descriptorString.startsWith("\n") || + descriptorString.contains("\n\n")) { + throw new DescriptorParseException("Empty lines are not allowed."); + } + try { + BufferedReader br = new BufferedReader(new StringReader( + descriptorString)); + String line; + StringBuilder sb = new StringBuilder(); + while ((line = br.readLine()) != null) { + String[] parts = line.split(" "); + String keyword = parts[0]; + 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<String>(); + } + this.unrecognizedLines.add(line); + } + } + } catch (IOException e) { + throw new RuntimeException("Internal error: Ran into an " + + "IOException while parsing a String in memory. Something's " + + "really wrong.", e); + } + } + + protected void parseExitListEntry(byte[] exitListEntryBytes) + throws DescriptorParseException { + ExitListEntryImpl exitListEntry = new ExitListEntryImpl( + exitListEntryBytes, this.failUnrecognizedDescriptorLines); + this.exitListEntries.add(exitListEntry); + List<String> unrecognizedExitListEntryLines = exitListEntry. + getAndClearUnrecognizedLines(); + if (unrecognizedExitListEntryLines != null) { + if (this.unrecognizedLines == null) { + this.unrecognizedLines = new ArrayList<String>(); + } + this.unrecognizedLines.addAll(unrecognizedExitListEntryLines); + } + } + + private long publishedMillis; + public long getPublishedMillis() { + return this.publishedMillis; + } + + private Set<ExitListEntry> exitListEntries = + new HashSet<ExitListEntry>(); + public Set<ExitListEntry> getExitListEntries() { + return new HashSet<ExitListEntry>(this.exitListEntries); + } +} +
tor-commits@lists.torproject.org