commit 0eb47d2650b6edf516fc9ec1eb508c08db86ca46 Author: Karsten Loesing karsten.loesing@gmx.net Date: Wed May 16 17:00:46 2012 +0200
Add support for parsing v2 network statuses. --- .../torproject/descriptor/RelayNetworkStatus.java | 68 ++++ .../descriptor/impl/BridgeNetworkStatusImpl.java | 2 +- .../torproject/descriptor/impl/DescriptorImpl.java | 3 + .../descriptor/impl/NetworkStatusImpl.java | 14 +- .../impl/RelayNetworkStatusConsensusImpl.java | 2 +- .../descriptor/impl/RelayNetworkStatusImpl.java | 360 ++++++++++++++++++++ .../impl/RelayNetworkStatusVoteImpl.java | 90 ++--- 7 files changed, 475 insertions(+), 64 deletions(-)
diff --git a/src/org/torproject/descriptor/RelayNetworkStatus.java b/src/org/torproject/descriptor/RelayNetworkStatus.java new file mode 100644 index 0000000..5d05f0c --- /dev/null +++ b/src/org/torproject/descriptor/RelayNetworkStatus.java @@ -0,0 +1,68 @@ +/* Copyright 2012 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.descriptor; + +import java.util.List; +import java.util.SortedMap; +import java.util.SortedSet; + +/* Contains a v2 network status. */ +public interface RelayNetworkStatus extends Descriptor { + + /* Return the network status version. */ + public int getNetworkStatusVersion(); + + /* Return the authority's hostname. */ + public String getHostname(); + + /* Return the authority's IP address. */ + public String getAddress(); + + /* Return the authority's directory port. */ + public int getDirport(); + + /* Return the directory's signing key's fingerprint. */ + public String getFingerprint(); + + /* Return the contact line. */ + public String getContactLine(); + + /* Return the directory signing key digest. */ + public String getDirSigningKey(); + + /* Return recommended server versions or null if the status doesn't + * contain recommended server versions. */ + public List<String> getRecommendedServerVersions(); + + /* Return recommended client versions or null if the status doesn't + * contain recommended client versions. */ + public List<String> getRecommendedClientVersions(); + + /* Return the published time in milliseconds. */ + public long getPublishedMillis(); + + /* Return the set of flags that this directory assigns to relays, or + * null if the status does not contain a dir-options line. */ + public SortedSet<String> getDirOptions(); + + /* Return status entries, one for each contained relay. */ + public SortedMap<String, NetworkStatusEntry> getStatusEntries(); + + /* Return whether a status entry with the given fingerprint exists. */ + public boolean containsStatusEntry(String fingerprint); + + /* Return a status entry by fingerprint or null if no such status entry + * exists. */ + public NetworkStatusEntry getStatusEntry(String fingerprint); + + /* Return the directory nickname. */ + public String getNickname(); + + /* Return the directory signature. */ + public String getDirectorySignature(); + + /* Return the status digest that the directory authority used to sign + * the network status. */ + public String getStatusDigest(); +} + diff --git a/src/org/torproject/descriptor/impl/BridgeNetworkStatusImpl.java b/src/org/torproject/descriptor/impl/BridgeNetworkStatusImpl.java index 1d9818c..e6d1942 100644 --- a/src/org/torproject/descriptor/impl/BridgeNetworkStatusImpl.java +++ b/src/org/torproject/descriptor/impl/BridgeNetworkStatusImpl.java @@ -15,7 +15,7 @@ public class BridgeNetworkStatusImpl extends NetworkStatusImpl protected BridgeNetworkStatusImpl(byte[] statusBytes, String fileName, boolean failUnrecognizedDescriptorLines) throws DescriptorParseException { - super(statusBytes, failUnrecognizedDescriptorLines); + super(statusBytes, failUnrecognizedDescriptorLines, false); this.setPublishedMillisFromFileName(fileName); }
diff --git a/src/org/torproject/descriptor/impl/DescriptorImpl.java b/src/org/torproject/descriptor/impl/DescriptorImpl.java index 6b1b167..7503419 100644 --- a/src/org/torproject/descriptor/impl/DescriptorImpl.java +++ b/src/org/torproject/descriptor/impl/DescriptorImpl.java @@ -66,6 +66,9 @@ public abstract class DescriptorImpl implements Descriptor { } else if (firstLines.startsWith("ExitNode ")) { parsedDescriptors.add(new ExitListImpl(rawDescriptorBytes, fileName, failUnrecognizedDescriptorLines)); + } else if (firstLines.startsWith("network-status-version 2\n")) { + parsedDescriptors.add(new RelayNetworkStatusImpl(rawDescriptorBytes, + failUnrecognizedDescriptorLines)); } else { throw new DescriptorParseException("Could not detect descriptor " + "type in descriptor starting with '" + firstLines + "'."); diff --git a/src/org/torproject/descriptor/impl/NetworkStatusImpl.java b/src/org/torproject/descriptor/impl/NetworkStatusImpl.java index d27e651..35e63b3 100644 --- a/src/org/torproject/descriptor/impl/NetworkStatusImpl.java +++ b/src/org/torproject/descriptor/impl/NetworkStatusImpl.java @@ -17,14 +17,14 @@ import org.torproject.descriptor.NetworkStatusEntry; public abstract class NetworkStatusImpl extends DescriptorImpl {
protected NetworkStatusImpl(byte[] rawDescriptorBytes, - boolean failUnrecognizedDescriptorLines) - throws DescriptorParseException { + boolean failUnrecognizedDescriptorLines, + boolean containsDirSourceEntries) throws DescriptorParseException { super(rawDescriptorBytes, failUnrecognizedDescriptorLines); - this.splitAndParseParts(rawDescriptorBytes); + this.splitAndParseParts(rawDescriptorBytes, containsDirSourceEntries); }
- private void splitAndParseParts(byte[] rawDescriptorBytes) - throws DescriptorParseException { + private void splitAndParseParts(byte[] rawDescriptorBytes, + boolean containsDirSourceEntries) throws DescriptorParseException { if (this.rawDescriptorBytes.length == 0) { throw new DescriptorParseException("Descriptor is empty."); } @@ -34,8 +34,8 @@ public abstract class NetworkStatusImpl extends DescriptorImpl { throw new DescriptorParseException("Empty lines are not allowed."); } int startIndex = 0; - int firstDirSourceIndex = this.findFirstIndexOfKeyword( - descriptorString, "dir-source"); + int firstDirSourceIndex = !containsDirSourceEntries ? -1 : + this.findFirstIndexOfKeyword(descriptorString, "dir-source"); int firstRIndex = this.findFirstIndexOfKeyword(descriptorString, "r"); int directoryFooterIndex = this.findFirstIndexOfKeyword( descriptorString, "directory-footer"); diff --git a/src/org/torproject/descriptor/impl/RelayNetworkStatusConsensusImpl.java b/src/org/torproject/descriptor/impl/RelayNetworkStatusConsensusImpl.java index 3d2af37..14082b8 100644 --- a/src/org/torproject/descriptor/impl/RelayNetworkStatusConsensusImpl.java +++ b/src/org/torproject/descriptor/impl/RelayNetworkStatusConsensusImpl.java @@ -41,7 +41,7 @@ public class RelayNetworkStatusConsensusImpl extends NetworkStatusImpl protected RelayNetworkStatusConsensusImpl(byte[] consensusBytes, boolean failUnrecognizedDescriptorLines) throws DescriptorParseException { - super(consensusBytes, failUnrecognizedDescriptorLines); + super(consensusBytes, failUnrecognizedDescriptorLines, true); Set<String> exactlyOnceKeywords = new HashSet<String>(Arrays.asList(( "vote-status,consensus-method,valid-after,fresh-until," + "valid-until,voting-delay,known-flags").split(","))); diff --git a/src/org/torproject/descriptor/impl/RelayNetworkStatusImpl.java b/src/org/torproject/descriptor/impl/RelayNetworkStatusImpl.java new file mode 100644 index 0000000..f3ab34a --- /dev/null +++ b/src/org/torproject/descriptor/impl/RelayNetworkStatusImpl.java @@ -0,0 +1,360 @@ +/* Copyright 2012 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.descriptor.impl; + +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Scanner; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.apache.commons.codec.digest.DigestUtils; +import org.torproject.descriptor.RelayNetworkStatus; + +/* TODO Write unit tests. */ + +public class RelayNetworkStatusImpl extends NetworkStatusImpl + implements RelayNetworkStatus { + + protected static List<RelayNetworkStatus> parseStatuses( + byte[] statusesBytes, boolean failUnrecognizedDescriptorLines) + throws DescriptorParseException { + List<RelayNetworkStatus> parsedStatuses = + new ArrayList<RelayNetworkStatus>(); + List<byte[]> splitStatusBytes = + DescriptorImpl.splitRawDescriptorBytes(statusesBytes, + "network-status-version 2"); + for (byte[] statusBytes : splitStatusBytes) { + RelayNetworkStatus parsedStatus = new RelayNetworkStatusImpl( + statusBytes, failUnrecognizedDescriptorLines); + parsedStatuses.add(parsedStatus); + } + return parsedStatuses; + } + + protected RelayNetworkStatusImpl(byte[] statusBytes, + boolean failUnrecognizedDescriptorLines) + throws DescriptorParseException { + super(statusBytes, failUnrecognizedDescriptorLines, false); + Set<String> exactlyOnceKeywords = new HashSet<String>(Arrays.asList(( + "network-status-version,dir-source,fingerprint,contact," + + "dir-signing-key,published").split(","))); + this.checkExactlyOnceKeywords(exactlyOnceKeywords); + Set<String> atMostOnceKeywords = new HashSet<String>(Arrays.asList( + "dir-options,client-versions,server-versions".split(","))); + this.checkAtMostOnceKeywords(atMostOnceKeywords); + this.checkFirstKeyword("network-status-version"); + this.calculateDigest(); + } + + private void calculateDigest() throws DescriptorParseException { + try { + String ascii = new String(this.getRawDescriptorBytes(), "US-ASCII"); + String startToken = "network-status-version "; + String sigToken = "\ndirectory-signature "; + if (!ascii.contains(sigToken)) { + return; + } + int start = ascii.indexOf(startToken); + int sig = ascii.indexOf(sigToken) + sigToken.length(); + sig = ascii.indexOf("\n", sig) + 1; + if (start >= 0 && sig >= 0 && sig > start) { + byte[] forDigest = new byte[sig - start]; + System.arraycopy(this.getRawDescriptorBytes(), start, + forDigest, 0, sig - start); + this.statusDigest = DigestUtils.shaHex(forDigest); + } + } catch (UnsupportedEncodingException e) { + /* Handle below. */ + } + if (this.statusDigest == null) { + throw new DescriptorParseException("Could not calculate status " + + "digest."); + } + } + + protected void parseHeader(byte[] headerBytes) + throws DescriptorParseException { + Scanner s = new Scanner(new String(headerBytes)).useDelimiter("\n"); + String nextCrypto = null; + StringBuilder crypto = null; + while (s.hasNext()) { + String line = s.next(); + String[] parts = line.split(" "); + String keyword = parts[0]; + if (keyword.equals("network-status-version")) { + this.parseNetworkStatusVersionLine(line, parts); + } else if (keyword.equals("dir-source")) { + this.parseDirSourceLine(line, parts); + } else if (keyword.equals("fingerprint")) { + this.parseFingerprintLine(line, parts); + } else if (keyword.equals("contact")) { + this.parseContactLine(line, parts); + } else if (keyword.equals("dir-signing-key")) { + this.parseDirSigningKeyLine(line, parts); + nextCrypto = "dir-signing-key"; + } else if (keyword.equals("client-versions")) { + this.parseClientVersionsLine(line, parts); + } else if (keyword.equals("server-versions")) { + this.parseServerVersionsLine(line, parts); + } else if (keyword.equals("published")) { + this.parsePublishedLine(line, parts); + } else if (keyword.equals("dir-options")) { + this.parseDirOptionsLine(line, parts); + } else if (line.startsWith("-----BEGIN")) { + crypto = new StringBuilder(); + crypto.append(line + "\n"); + } else if (line.startsWith("-----END")) { + crypto.append(line + "\n"); + String cryptoString = crypto.toString(); + crypto = null; + if (nextCrypto.equals("dir-signing-key")) { + this.dirSigningKey = cryptoString; + } else { + throw new DescriptorParseException("Unrecognized crypto " + + "block in v2 network status."); + } + nextCrypto = null; + } else if (crypto != null) { + crypto.append(line + "\n"); + } else if (this.failUnrecognizedDescriptorLines) { + throw new DescriptorParseException("Unrecognized line '" + line + + "' in v2 network status."); + } else { + if (this.unrecognizedLines == null) { + this.unrecognizedLines = new ArrayList<String>(); + } + this.unrecognizedLines.add(line); + } + } + } + + protected void parseFooter(byte[] footerBytes) + throws DescriptorParseException { + throw new DescriptorParseException("No directory footer expected in " + + "v2 network status."); + } + + protected void parseDirectorySignature(byte[] directorySignatureBytes) + throws DescriptorParseException { + Scanner s = new Scanner(new String(directorySignatureBytes)). + useDelimiter("\n"); + String nextCrypto = null; + StringBuilder crypto = null; + while (s.hasNext()) { + String line = s.next(); + String[] parts = line.split(" "); + String keyword = parts[0]; + if (keyword.equals("directory-signature")) { + this.parseDirectorySignatureLine(line, parts); + nextCrypto = "directory-signature"; + } else if (line.startsWith("-----BEGIN")) { + crypto = new StringBuilder(); + crypto.append(line + "\n"); + } else if (line.startsWith("-----END")) { + crypto.append(line + "\n"); + String cryptoString = crypto.toString(); + crypto = null; + if (nextCrypto.equals("directory-signature")) { + this.directorySignature = cryptoString; + } else { + throw new DescriptorParseException("Unrecognized crypto " + + "block in v2 network status."); + } + nextCrypto = null; + } else if (crypto != null) { + crypto.append(line + "\n"); + } else if (this.failUnrecognizedDescriptorLines) { + throw new DescriptorParseException("Unrecognized line '" + line + + "' in v2 network status."); + } else { + if (this.unrecognizedLines == null) { + this.unrecognizedLines = new ArrayList<String>(); + } + this.unrecognizedLines.add(line); + } + } + } + + private void parseNetworkStatusVersionLine(String line, String[] parts) + throws DescriptorParseException { + if (!line.equals("network-status-version 2")) { + throw new DescriptorParseException("Illegal network status version " + + "number in line '" + line + "'."); + } + this.networkStatusVersion = 2; + } + + private void parseDirSourceLine(String line, String[] parts) + throws DescriptorParseException { + if (parts.length != 4) { + throw new DescriptorParseException("Illegal line '" + line + + "' in v2 network status."); + } + if (parts[1].length() < 1) { + throw new DescriptorParseException("Illegal hostname in '" + line + + "'."); + } + this.address = ParseHelper.parseIpv4Address(line, parts[2]); + this.dirPort = ParseHelper.parsePort(line, parts[3]); + } + + + private void parseFingerprintLine(String line, String[] parts) + throws DescriptorParseException { + if (parts.length != 2) { + throw new DescriptorParseException("Illegal line '" + line + + "' in v2 network status."); + } + this.fingerprint = ParseHelper.parseTwentyByteHexString(line, + parts[1]); + } + + private void parseContactLine(String line, String[] parts) + throws DescriptorParseException { + if (line.length() > "contact ".length()) { + this.contactLine = line.substring("contact ".length()); + } else { + this.contactLine = ""; + } + } + + private void parseDirSigningKeyLine(String line, String[] parts) + throws DescriptorParseException { + if (!line.equals("dir-signing-key")) { + throw new DescriptorParseException("Illegal line '" + line + "'."); + } + } + + private void parseClientVersionsLine(String line, String[] parts) + throws DescriptorParseException { + this.recommendedClientVersions = this.parseClientOrServerVersions( + line, parts); + } + + private void parseServerVersionsLine(String line, String[] parts) + throws DescriptorParseException { + this.recommendedServerVersions = this.parseClientOrServerVersions( + line, parts); + } + + private List<String> parseClientOrServerVersions(String line, + String[] parts) throws DescriptorParseException { + List<String> result = new ArrayList<String>(); + if (parts.length == 1) { + return result; + } else if (parts.length > 2) { + throw new DescriptorParseException("Illegal versions line '" + line + + "'."); + } + String[] versions = parts[1].split(",", -1); + for (int i = 0; i < versions.length; i++) { + String version = versions[i]; + if (version.length() < 1) { + throw new DescriptorParseException("Illegal versions line '" + + line + "'."); + } + result.add(version); + } + return result; + } + + private void parsePublishedLine(String line, String[] parts) + throws DescriptorParseException { + this.publishedMillis = ParseHelper.parseTimestampAtIndex(line, parts, + 1, 2); + } + + private void parseDirOptionsLine(String line, String[] parts) + throws DescriptorParseException { + this.dirOptions = new TreeSet<String>(); + for (int i = 1; i < parts.length; i++) { + this.dirOptions.add(parts[i]); + } + } + + private void parseDirectorySignatureLine(String line, String[] parts) + throws DescriptorParseException { + if (parts.length < 2) { + throw new DescriptorParseException("Illegal line '" + line + "'."); + } + this.nickname = ParseHelper.parseNickname(line, parts[1]); + } + + private String statusDigest; + public String getStatusDigest() { + return this.statusDigest; + } + + private int networkStatusVersion; + public int getNetworkStatusVersion() { + return this.networkStatusVersion; + } + + private String hostname; + public String getHostname() { + return this.hostname; + } + + private String address; + public String getAddress() { + return this.address; + } + + private int dirPort; + public int getDirport() { + return this.dirPort; + } + + private String fingerprint; + public String getFingerprint() { + return this.fingerprint; + } + + private String contactLine; + public String getContactLine() { + return this.contactLine; + } + + private String dirSigningKey; + public String getDirSigningKey() { + return this.dirSigningKey; + } + + private List<String> recommendedClientVersions; + public List<String> getRecommendedClientVersions() { + return this.recommendedClientVersions == null ? null : + new ArrayList<String>(this.recommendedClientVersions); + } + + private List<String> recommendedServerVersions; + public List<String> getRecommendedServerVersions() { + return this.recommendedServerVersions == null ? null : + new ArrayList<String>(this.recommendedServerVersions); + } + + private long publishedMillis; + public long getPublishedMillis() { + return this.publishedMillis; + } + + private SortedSet<String> dirOptions; + public SortedSet<String> getDirOptions() { + return new TreeSet<String>(this.dirOptions); + } + + private String nickname; + public String getNickname() { + return this.nickname; + } + + private String directorySignature; + public String getDirectorySignature() { + return this.directorySignature; + } +} + diff --git a/src/org/torproject/descriptor/impl/RelayNetworkStatusVoteImpl.java b/src/org/torproject/descriptor/impl/RelayNetworkStatusVoteImpl.java index 5791688..7cce313 100644 --- a/src/org/torproject/descriptor/impl/RelayNetworkStatusVoteImpl.java +++ b/src/org/torproject/descriptor/impl/RelayNetworkStatusVoteImpl.java @@ -39,7 +39,7 @@ public class RelayNetworkStatusVoteImpl extends NetworkStatusImpl protected RelayNetworkStatusVoteImpl(byte[] voteBytes, boolean failUnrecognizedDescriptorLines) throws DescriptorParseException { - super(voteBytes, failUnrecognizedDescriptorLines); + super(voteBytes, failUnrecognizedDescriptorLines, false); Set<String> exactlyOnceKeywords = new HashSet<String>(Arrays.asList(( "vote-status,consensus-methods,published,valid-after,fresh-until," + "valid-until,voting-delay,known-flags,dir-source," @@ -57,6 +57,7 @@ public class RelayNetworkStatusVoteImpl extends NetworkStatusImpl protected void parseHeader(byte[] headerBytes) throws DescriptorParseException { Scanner s = new Scanner(new String(headerBytes)).useDelimiter("\n"); + boolean skipCrypto = false; /* TODO Parse crypto parts. */ while (s.hasNext()) { String line = s.next(); String[] parts = line.split(" "); @@ -85,14 +86,40 @@ public class RelayNetworkStatusVoteImpl extends NetworkStatusImpl this.parseKnownFlagsLine(line, parts); } else if (keyword.equals("params")) { this.parseParamsLine(line, parts); - } else if (this.failUnrecognizedDescriptorLines) { - throw new DescriptorParseException("Unrecognized line '" + line - + "' in vote."); - } else { - if (this.unrecognizedLines == null) { - this.unrecognizedLines = new ArrayList<String>(); + } else if (keyword.equals("dir-source")) { + this.parseDirSourceLine(line, parts); + } else if (keyword.equals("contact")) { + this.parseContactLine(line, parts); + } else if (keyword.equals("dir-key-certificate-version")) { + this.parseDirKeyCertificateVersionLine(line, parts); + } else if (keyword.equals("dir-address")) { + this.parseDirAddressLine(line, parts); + } else if (keyword.equals("fingerprint")) { + this.parseFingerprintLine(line, parts); + } else if (keyword.equals("legacy-dir-key")) { + this.parseLegacyDirKeyLine(line, parts); + } else if (keyword.equals("dir-key-published")) { + this.parseDirKeyPublished(line, parts); + } else if (keyword.equals("dir-key-expires")) { + this.parseDirKeyExpiresLine(line, parts); + } else if (keyword.equals("dir-identity-key") || + keyword.equals("dir-signing-key") || + keyword.equals("dir-key-crosscert") || + keyword.equals("dir-key-certification")) { + } else if (line.startsWith("-----BEGIN")) { + skipCrypto = true; + } else if (line.startsWith("-----END")) { + skipCrypto = false; + } else if (!skipCrypto) { + if (this.failUnrecognizedDescriptorLines) { + throw new DescriptorParseException("Unrecognized line '" + + line + "' in vote."); + } else { + if (this.unrecognizedLines == null) { + this.unrecognizedLines = new ArrayList<String>(); + } + this.unrecognizedLines.add(line); } - this.unrecognizedLines.add(line); } } } @@ -225,53 +252,6 @@ public class RelayNetworkStatusVoteImpl extends NetworkStatusImpl this.consensusParams = ParseHelper.parseKeyValuePairs(line, parts, 1); }
- protected void parseDirSource(byte[] dirSourceBytes) - throws DescriptorParseException { - Scanner s = new Scanner(new String(dirSourceBytes)). - useDelimiter("\n"); - boolean skipCrypto = false; - while (s.hasNext()) { - String line = s.next(); - String[] parts = line.split(" "); - String keyword = parts[0]; - if (keyword.equals("dir-source")) { - this.parseDirSourceLine(line, parts); - } else if (keyword.equals("contact")) { - this.parseContactLine(line, parts); - } else if (keyword.equals("dir-key-certificate-version")) { - this.parseDirKeyCertificateVersionLine(line, parts); - } else if (keyword.equals("dir-address")) { - this.parseDirAddressLine(line, parts); - } else if (keyword.equals("fingerprint")) { - this.parseFingerprintLine(line, parts); - } else if (keyword.equals("legacy-dir-key")) { - this.parseLegacyDirKeyLine(line, parts); - } else if (keyword.equals("dir-key-published")) { - this.parseDirKeyPublished(line, parts); - } else if (keyword.equals("dir-key-expires")) { - this.parseDirKeyExpiresLine(line, parts); - } else if (keyword.equals("dir-identity-key") || - keyword.equals("dir-signing-key") || - keyword.equals("dir-key-crosscert") || - keyword.equals("dir-key-certification")) { - } else if (line.startsWith("-----BEGIN")) { - skipCrypto = true; - } else if (line.startsWith("-----END")) { - skipCrypto = false; - } else if (!skipCrypto) { - if (this.failUnrecognizedDescriptorLines) { - throw new DescriptorParseException("Unrecognized line '" - + line + "' in vote."); - } else { - if (this.unrecognizedLines == null) { - this.unrecognizedLines = new ArrayList<String>(); - } - this.unrecognizedLines.add(line); - } - } - } - } - private void parseDirSourceLine(String line, String[] parts) throws DescriptorParseException { if (parts.length != 7) {
tor-commits@lists.torproject.org