Make consensus parsing a lot more robust.

This commit is contained in:
Karsten Loesing 2011-12-14 19:17:26 +01:00
parent 358b92b019
commit 228d6564f8
10 changed files with 1311 additions and 608 deletions

View File

@ -22,6 +22,9 @@ public interface DirSourceEntry {
/* Return the ORPort. */
public int getOrPort();
/* Return whether the dir-source was created using a legacy key. */
public boolean isLegacy();
/* Return the contact line. */
public String getContactLine();

View File

@ -30,16 +30,29 @@ public interface NetworkStatusEntry {
/* Return the DirPort. */
public int getDirPort();
/* Return the relay flags. */
/* Return the relay flags or null if the status entry didn't contain any
* relay flags. */
public SortedSet<String> getFlags();
/* Return the Tor software version. */
/* Return the Tor software version or null if the status entry didn't
* contain a version line. */
public String getVersion();
/* Return the bandwidth weight line. */
public String getBandwidth();
/* Return the bandwidth weight or -1L if the status entry didn't
* contain a bandwidth line. */
public long getBandwidth();
/* Return the port summary line. */
public String getPorts();
/* Return the measured bandwidth or -1L if the status entry didn't
* contain a bandwidth line or didn't contain a Measured= keyword in its
* bandwidth line. */
public long getMeasured();
/* Return the default policy of the port summary or null if the status
* entry didn't contain a port summary line. */
public String getDefaultPolicy();
/* Return the port list of the port summary or null if the status entry
* didn't contain a port summary line. */
public String getPortList();
}

View File

@ -24,8 +24,11 @@ public interface RelayNetworkStatusConsensus extends Descriptor {
/* Return the valid-until time in milliseconds. */
public long getValidUntilMillis();
/* Return a list of the voting-delay times in seconds. */
public List<Long> getVotingDelay();
/* Return the VoteSeconds time in seconds. */
public long getVoteSeconds();
/* Return the DistSeconds time in seconds. */
public long getDistSeconds();
/* Return recommended server versions or null if the consensus doesn't
* contain recommended server versions. */
@ -38,8 +41,9 @@ public interface RelayNetworkStatusConsensus extends Descriptor {
/* Return known relay flags. */
public SortedSet<String> getKnownFlags();
/* Return consensus parameters. */
public SortedMap<String, String> getConsensusParams();
/* Return consensus parameters or null if the consensus doesn't contain
* consensus parameters. */
public SortedMap<String, Integer> getConsensusParams();
/* Return dir-source entries representing the directories of which
* votes are contained in this consensus. */
@ -58,7 +62,8 @@ public interface RelayNetworkStatusConsensus extends Descriptor {
/* Return directory signatures. */
public SortedMap<String, String> getDirectorySignatures();
/* Return bandwidth weights. */
public SortedMap<String, String> getBandwidthWeights();
/* Return bandwidth weights or null if the consensus doesn't contain
* bandwidth weights. */
public SortedMap<String, Integer> getBandwidthWeights();
}

View File

@ -0,0 +1,10 @@
/* Copyright 2011 The Tor Project
* See LICENSE for licensing information */
package org.torproject.descriptor.impl;
public class DescriptorParseException extends Exception {
protected DescriptorParseException(String message) {
super(message);
}
}

View File

@ -6,6 +6,8 @@ import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.text.ParseException;
import java.util.SortedSet;
import java.util.TreeSet;
import org.torproject.descriptor.DirSourceEntry;
public class DirSourceEntryImpl implements DirSourceEntry {
@ -16,30 +18,110 @@ public class DirSourceEntryImpl implements DirSourceEntry {
}
protected DirSourceEntryImpl(byte[] dirSourceEntryBytes)
throws ParseException {
throws DescriptorParseException {
this.dirSourceEntryBytes = dirSourceEntryBytes;
this.initializeKeywords();
this.parseDirSourceEntryBytes();
this.checkKeywords();
}
private SortedSet<String> exactlyOnceKeywords, atMostOnceKeywords;
private void initializeKeywords() {
this.exactlyOnceKeywords = new TreeSet<String>();
this.exactlyOnceKeywords.add("dir-source");
this.exactlyOnceKeywords.add("vote-digest");
this.atMostOnceKeywords = new TreeSet<String>();
this.atMostOnceKeywords.add("contact");
}
private void parsedExactlyOnceKeyword(String keyword)
throws DescriptorParseException {
if (!this.exactlyOnceKeywords.contains(keyword)) {
throw new DescriptorParseException("Duplicate '" + keyword
+ "' line in dir-source.");
}
this.exactlyOnceKeywords.remove(keyword);
}
private void parsedAtMostOnceKeyword(String keyword)
throws DescriptorParseException {
if (!this.atMostOnceKeywords.contains(keyword)) {
throw new DescriptorParseException("Duplicate " + keyword + "line "
+ "in dir-source.");
}
this.atMostOnceKeywords.remove(keyword);
}
private void checkKeywords() throws DescriptorParseException {
if (!this.exactlyOnceKeywords.isEmpty()) {
throw new DescriptorParseException("dir-source does not contain a '"
+ this.exactlyOnceKeywords.first() + "' line.");
}
}
private void parseDirSourceEntryBytes()
throws DescriptorParseException {
try {
BufferedReader br = new BufferedReader(new StringReader(
new String(this.dirSourceEntryBytes)));
String line;
while ((line = br.readLine()) != null) {
if (line.startsWith("dir-source ")) {
String[] parts = line.split(" ");
this.nickname = parts[1];
this.identity = parts[2];
this.ip = parts[4];
this.dirPort = Integer.parseInt(parts[5]);
this.orPort = Integer.parseInt(parts[6]);
} else if (line.startsWith("contact ")) {
this.contactLine = line.substring("contact ".length());
} else if (line.startsWith("vote-digest ")) {
this.voteDigest = line.split(" ")[1];
if (line.startsWith("dir-source")) {
this.parseDirSourceLine(line);
} else if (line.startsWith("contact")) {
this.parseContactLine(line);
} else if (line.startsWith("vote-digest")) {
this.parseVoteDigestLine(line);
} else {
/* TODO Should we really throw an exception here? */
throw new DescriptorParseException("Unknown line '" + line
+ "' in dir-source entry.");
}
}
} catch (IOException e) {
/* TODO This cannot happen, right? */
throw new RuntimeException("Internal error: Ran into an "
+ "IOException while parsing a String in memory. Something's "
+ "really wrong.", e);
}
/* TODO Implement some plausibility tests. */
}
private void parseDirSourceLine(String line)
throws DescriptorParseException {
this.parsedExactlyOnceKeyword("dir-source");
String[] parts = line.split(" ");
String nickname = parts[1];
if (nickname.endsWith("-legacy")) {
nickname = nickname.substring(0, nickname.length()
- "-legacy".length());
this.isLegacy = true;
this.parsedExactlyOnceKeyword("vote-digest");
}
this.nickname = ParseHelper.parseNickname(line, nickname);
this.identity = ParseHelper.parseTwentyByteHexString(line, parts[2]);
this.ip = ParseHelper.parseIpv4Address(line, parts[4]);
this.dirPort = ParseHelper.parsePort(line, parts[5]);
this.orPort = ParseHelper.parsePort(line, parts[6]);
}
private void parseContactLine(String line)
throws DescriptorParseException {
this.parsedAtMostOnceKeyword("contact");
if (line.length() > "contact ".length()) {
this.contactLine = line.substring("contact ".length());
} else {
this.contactLine = "";
}
}
private void parseVoteDigestLine(String line)
throws DescriptorParseException {
this.parsedExactlyOnceKeyword("vote-digest");
String[] parts = line.split(" ");
if (parts.length != 2) {
throw new DescriptorParseException("Invalid line '" + line + "'.");
}
this.voteDigest = ParseHelper.parseTwentyByteHexString(line,
parts[1]);
}
private String nickname;
@ -52,6 +134,11 @@ public class DirSourceEntryImpl implements DirSourceEntry {
return this.identity;
}
private boolean isLegacy;
public boolean isLegacy() {
return this.isLegacy;
}
private String ip;
public String getIp() {
return this.ip;

View File

@ -5,14 +5,10 @@ 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.Arrays;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TimeZone;
import java.util.TreeSet;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;
import org.torproject.descriptor.NetworkStatusEntry;
public class NetworkStatusEntryImpl implements NetworkStatusEntry {
@ -22,60 +18,163 @@ public class NetworkStatusEntryImpl implements NetworkStatusEntry {
return this.statusEntryBytes;
}
private static SimpleDateFormat dateTimeFormat;
static {
dateTimeFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
dateTimeFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
protected NetworkStatusEntryImpl(byte[] statusEntryBytes)
throws DescriptorParseException {
this.statusEntryBytes = statusEntryBytes;
this.initializeKeywords();
this.parseStatusEntryBytes();
}
protected NetworkStatusEntryImpl(byte[] statusEntryBytes)
throws ParseException {
this.statusEntryBytes = statusEntryBytes;
private SortedSet<String> atMostOnceKeywords;
private void initializeKeywords() {
this.atMostOnceKeywords = new TreeSet<String>();
this.atMostOnceKeywords.add("s");
this.atMostOnceKeywords.add("v");
this.atMostOnceKeywords.add("w");
this.atMostOnceKeywords.add("p");
this.atMostOnceKeywords.add("m");
}
private void parsedAtMostOnceKeyword(String keyword)
throws DescriptorParseException {
if (!this.atMostOnceKeywords.contains(keyword)) {
throw new DescriptorParseException("Duplicate '" + keyword
+ "' line in status entry.");
}
this.atMostOnceKeywords.remove(keyword);
}
private void parseStatusEntryBytes() throws DescriptorParseException {
try {
BufferedReader br = new BufferedReader(new StringReader(
new String(this.statusEntryBytes)));
String line;
String line = br.readLine();
if (line == null || !line.startsWith("r ")) {
throw new DescriptorParseException("Status entry must start with "
+ "an r line.");
}
String[] rLineParts = line.split(" ");
this.parseRLine(line, rLineParts);
while ((line = br.readLine()) != null) {
if (line.startsWith("r ")) {
String[] parts = line.split(" ");
if (parts.length < 9) {
throw new RuntimeException("r line '" + line + "' has fewer "
+ "space-separated elements than expected.");
}
this.nickname = parts[1];
this.fingerprint = Hex.encodeHexString(Base64.decodeBase64(
parts[2] + "=")).toLowerCase();
this.descriptor = Hex.encodeHexString(Base64.decodeBase64(
parts[3] + "=")).toLowerCase();
this.publishedMillis = dateTimeFormat.parse(parts[4] + " "
+ parts[5]).getTime();
this.address = parts[6];
this.orPort = Integer.parseInt(parts[7]);
this.dirPort = Integer.parseInt(parts[8]);
} else if (line.equals("s")) {
/* No flags to add. */
} else if (line.startsWith("s ")) {
this.flags.addAll(Arrays.asList(line.substring("s ".length()).
split(" ")));
} else if (line.startsWith("v ") || line.startsWith("opt v")) {
this.version = line.substring(
line.startsWith("v ") ? "v ".length() : "opt v".length());
} else if (line.startsWith("w ")) {
this.bandwidth = line.substring("w ".length());
} else if (line.startsWith("p ")) {
this.ports = line.substring(2);
} else if (line.startsWith("m ")) {
/* TODO Parse m lines in votes. */
String[] parts = !line.startsWith("opt ") ? line.split(" ") :
line.substring("opt ".length()).split(" ");
String keyword = parts[0];
if (keyword.equals("s")) {
this.parseSLine(line, parts);
} else if (keyword.equals("v")) {
this.parseVLine(line, parts);
} else if (keyword.equals("w")) {
this.parseWLine(line, parts);
} else if (keyword.equals("p")) {
this.parsePLine(line, parts);
} else if (keyword.equals("m")) {
this.parseMLine(line, parts);
} else {
throw new RuntimeException("Unknown line '" + line + "' in "
+ "status entry.");
/* TODO Is throwing an exception the right thing to do here?
* This is probably fine for development, but once the library
* is in production use, this seems annoying. */
throw new DescriptorParseException("Unknown line '" + line
+ "' in status entry.");
}
}
} catch (IOException e) {
/* TODO Do something. */
throw new RuntimeException("Internal error: Ran into an "
+ "IOException while parsing a String in memory. Something's "
+ "really wrong.", e);
}
/* TODO Add some plausibility checks, like if we have a nickname
* etc. */
}
private void parseRLine(String line, String[] parts)
throws DescriptorParseException {
if (parts.length < 9) {
throw new RuntimeException("r line '" + line + "' has fewer "
+ "space-separated elements than expected.");
}
this.nickname = ParseHelper.parseNickname(line, parts[1]);
this.fingerprint = ParseHelper.parseTwentyByteBase64String(line,
parts[2]);
this.descriptor = ParseHelper.parseTwentyByteBase64String(line,
parts[3]);
this.publishedMillis = ParseHelper.parseTimestampAtIndex(line, parts,
4, 5);
this.address = ParseHelper.parseIpv4Address(line, parts[6]);
this.orPort = ParseHelper.parsePort(line, parts[7]);
this.dirPort = ParseHelper.parsePort(line, parts[8]);
}
private void parseSLine(String line, String[] parts)
throws DescriptorParseException {
this.parsedAtMostOnceKeyword("s");
this.flags = new TreeSet<String>();
for (int i = 1; i < parts.length; i++) {
this.flags.add(parts[i]);
}
}
private void parseVLine(String line, String[] parts)
throws DescriptorParseException {
this.parsedAtMostOnceKeyword("v");
String noOptLine = line;
if (noOptLine.startsWith("opt ")) {
noOptLine = noOptLine.substring(4);
}
if (noOptLine.length() < 3) {
throw new DescriptorParseException("Invalid line '" + line + "' in "
+ "status entry.");
} else {
this.version = noOptLine.substring(2);
}
}
private void parseWLine(String line, String[] parts)
throws DescriptorParseException {
this.parsedAtMostOnceKeyword("w");
SortedMap<String, Integer> pairs = ParseHelper.parseKeyValuePairs(
line, parts, 1);
if (pairs.isEmpty()) {
throw new DescriptorParseException("Illegal line '" + line + "'.");
}
if (pairs.containsKey("Bandwidth")) {
this.bandwidth = pairs.remove("Bandwidth");
}
if (pairs.containsKey("Measured")) {
this.measured = pairs.remove("Measured");
}
if (!pairs.isEmpty()) {
throw new DescriptorParseException("Unknown key-value pair in "
+ "line '" + line + "'.");
}
}
private void parsePLine(String line, String[] parts)
throws DescriptorParseException {
this.parsedAtMostOnceKeyword("p");
boolean isValid = true;
if (parts.length != 3) {
isValid = false;
} else if (!parts[1].equals("accept") && !parts[1].equals("reject")) {
isValid = false;
} else {
this.defaultPolicy = parts[1];
this.portList = parts[2];
String[] ports = parts[2].split(",", -1);
for (int i = 0; i < ports.length; i++) {
if (ports[i].length() < 1) {
isValid = false;
break;
}
}
}
if (!isValid) {
throw new DescriptorParseException("Illegal line '" + line + "'.");
}
}
private void parseMLine(String line, String[] parts)
throws DescriptorParseException {
this.parsedAtMostOnceKeyword("m");
/* TODO Implement parsing of m lines in votes. Try to find where m
* lines are specified first. */
}
private String nickname;
@ -113,7 +212,7 @@ public class NetworkStatusEntryImpl implements NetworkStatusEntry {
return this.dirPort;
}
private SortedSet<String> flags = new TreeSet<String>();
private SortedSet<String> flags;
public SortedSet<String> getFlags() {
return new TreeSet<String>(this.flags);
}
@ -123,14 +222,24 @@ public class NetworkStatusEntryImpl implements NetworkStatusEntry {
return this.version;
}
private String bandwidth;
public String getBandwidth() {
private long bandwidth = -1L;
public long getBandwidth() {
return this.bandwidth;
}
private String ports;
public String getPorts() {
return this.ports;
private long measured = -1L;
public long getMeasured() {
return this.measured;
}
private String defaultPolicy;
public String getDefaultPolicy() {
return this.defaultPolicy;
}
private String portList;
public String getPortList() {
return this.portList;
}
}

View File

@ -0,0 +1,147 @@
/* Copyright 2011 The Tor Project
* See LICENSE for licensing information */
package org.torproject.descriptor.impl;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.SortedMap;
import java.util.TimeZone;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;
public class ParseHelper {
private static Pattern ipv4Pattern =
Pattern.compile("^[0-9\\.]{7,15}$");
public static String parseIpv4Address(String line, String address)
throws DescriptorParseException {
boolean isValid = true;
if (!ipv4Pattern.matcher(address).matches()) {
isValid = false;
} else {
String[] parts = address.split("\\.", -1);
if (parts.length != 4) {
isValid = false;
} else {
for (int i = 0; i < 4; i++) {
try {
int octetValue = Integer.parseInt(parts[i]);
if (octetValue < 0 || octetValue > 255) {
isValid = false;
}
} catch (NumberFormatException e) {
isValid = false;
}
}
}
}
if (!isValid) {
throw new DescriptorParseException("'" + address + "' in line '"
+ line + "' is not a valid IPv4 address.");
}
return address;
}
public static int parsePort(String line, String portString)
throws DescriptorParseException {
int port = -1;
try {
port = Integer.parseInt(portString);
} catch (NumberFormatException e) {
throw new DescriptorParseException("'" + portString + "' in line '"
+ line + "' is not a valid port number.");
}
if (port < 0 || port > 65535) {
throw new DescriptorParseException("'" + portString + "' in line '"
+ line + "' is not a valid port number.");
}
return port;
}
private static SimpleDateFormat dateTimeFormat = new SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss");
static {
dateTimeFormat.setLenient(false);
dateTimeFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
}
public static long parseTimestampAtIndex(String line, String[] parts,
int dateIndex, int timeIndex) throws DescriptorParseException {
if (dateIndex >= parts.length || timeIndex >= parts.length) {
throw new DescriptorParseException("Line '" + line + "' does not "
+ "contain a timestamp at the expected position.");
}
long result = -1L;
try {
result = dateTimeFormat.parse(
parts[dateIndex] + " " + parts[timeIndex]).getTime();
} catch (ParseException e) {
/* Leave result at -1L. */
}
if (result < 0L || result > 2000000000000L) {
throw new DescriptorParseException("Illegal timestamp format in "
+ "line '" + line + "'.");
}
return result;
}
private static Pattern twentyByteHexPattern =
Pattern.compile("^[0-9A-F]{40}$");
public static String parseTwentyByteHexString(String line,
String hexString) throws DescriptorParseException {
if (!twentyByteHexPattern.matcher(hexString).matches()) {
throw new DescriptorParseException("Illegal hex string in line '"
+ line + "'.");
}
return hexString;
}
public static SortedMap<String, Integer> parseKeyValuePairs(String line,
String[] parts, int startIndex) throws DescriptorParseException {
SortedMap<String, Integer> result = new TreeMap<String, Integer>();
for (int i = startIndex; i < parts.length; i++) {
String pair = parts[i];
String[] pairParts = pair.split("=");
if (pairParts.length != 2) {
throw new DescriptorParseException("Illegal key-value pair in "
+ "line '" + line + "'.");
}
String pairName = pairParts[0];
try {
int pairValue = Integer.parseInt(pairParts[1]);
result.put(pairName, pairValue);
} catch (NumberFormatException e) {
throw new DescriptorParseException("Illegal value in line '"
+ line + "'.");
}
}
return result;
}
private static Pattern nicknamePattern =
Pattern.compile("^[0-9a-zA-Z]{1,19}$");
public static String parseNickname(String line, String nickname)
throws DescriptorParseException {
if (!nicknamePattern.matcher(nickname).matches()) {
throw new DescriptorParseException("Illegal nickname in line '"
+ line + "'.");
}
return nickname;
}
private static Pattern base64Pattern =
Pattern.compile("^[0-9a-zA-Z+/]{27}$");
public static String parseTwentyByteBase64String(String line,
String base64String) throws DescriptorParseException {
if (!base64Pattern.matcher(base64String).matches()) {
throw new DescriptorParseException("'" + base64String
+ "' in line '" + line + "' is not a valid base64-encoded "
+ "20-byte value.");
}
return Hex.encodeHexString(Base64.decodeBase64(base64String + "=")).
toUpperCase();
}
}

View File

@ -13,8 +13,6 @@ import java.util.SortedSet;
import java.util.TimeZone;
import java.util.TreeMap;
import java.util.TreeSet;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import org.torproject.descriptor.Descriptor;
import org.torproject.descriptor.DirSourceEntry;
import org.torproject.descriptor.NetworkStatusEntry;
@ -41,185 +39,322 @@ public class RelayNetworkStatusConsensusImpl
}
byte[] descBytes = new byte[end - start];
System.arraycopy(consensusBytes, start, descBytes, 0, end - start);
RelayNetworkStatusConsensus parsedConsensus =
new RelayNetworkStatusConsensusImpl(descBytes);
parsedConsensuses.add(parsedConsensus);
start = end;
try {
RelayNetworkStatusConsensus parsedConsensus =
new RelayNetworkStatusConsensusImpl(descBytes);
parsedConsensuses.add(parsedConsensus);
} catch (DescriptorParseException e) {
/* TODO Handle this error somehow. */
System.err.println("Failed to parse consensus. Skipping.");
e.printStackTrace();
}
}
return parsedConsensuses;
}
protected RelayNetworkStatusConsensusImpl(byte[] consensusBytes) {
protected RelayNetworkStatusConsensusImpl(byte[] consensusBytes)
throws DescriptorParseException {
this.consensusBytes = consensusBytes;
this.initializeKeywords();
this.parseConsensusBytes();
this.checkConsistency();
/* TODO Find a way to handle parse and consistency-check problems. */
this.checkKeywords();
}
private void parseConsensusBytes() {
private SortedSet<String> exactlyOnceKeywords, atMostOnceKeywords;
private void initializeKeywords() {
this.exactlyOnceKeywords = new TreeSet<String>();
this.exactlyOnceKeywords.add("vote-status");
this.exactlyOnceKeywords.add("consensus-method");
this.exactlyOnceKeywords.add("valid-after");
this.exactlyOnceKeywords.add("fresh-until");
this.exactlyOnceKeywords.add("valid-until");
this.exactlyOnceKeywords.add("voting-delay");
this.exactlyOnceKeywords.add("known-flags");
this.exactlyOnceKeywords.add("directory-footer");
this.atMostOnceKeywords = new TreeSet<String>();
this.atMostOnceKeywords.add("client-versions");
this.atMostOnceKeywords.add("server-versions");
this.atMostOnceKeywords.add("params");
this.atMostOnceKeywords.add("bandwidth-weights");
}
private void parsedExactlyOnceKeyword(String keyword)
throws DescriptorParseException {
if (!this.exactlyOnceKeywords.contains(keyword)) {
throw new DescriptorParseException("Duplicate '" + keyword
+ "' line in consensus.");
}
this.exactlyOnceKeywords.remove(keyword);
}
private void parsedAtMostOnceKeyword(String keyword)
throws DescriptorParseException {
if (!this.atMostOnceKeywords.contains(keyword)) {
throw new DescriptorParseException("Duplicate " + keyword + "line "
+ "in consensus.");
}
this.atMostOnceKeywords.remove(keyword);
}
private void checkKeywords() throws DescriptorParseException {
if (!this.exactlyOnceKeywords.isEmpty()) {
throw new DescriptorParseException("Consensus does not contain a '"
+ this.exactlyOnceKeywords.first() + "' line.");
}
}
private void parseConsensusBytes() throws DescriptorParseException {
try {
BufferedReader br = new BufferedReader(new StringReader(
new String(this.consensusBytes)));
String line;
SimpleDateFormat dateTimeFormat = new SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss");
dateTimeFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
String line = br.readLine();
if (line == null || !line.equals("network-status-version 3")) {
throw new DescriptorParseException("Consensus must start with "
+ "line 'network-status-version 3'.");
}
this.networkStatusVersion = 3;
StringBuilder dirSourceEntryLines = null, statusEntryLines = null;
boolean skipSignature = false;
while ((line = br.readLine()) != null) {
if (line.startsWith("network-status-version ")) {
this.networkStatusVersion = Integer.parseInt(line.substring(
"network-status-version ".length()));
} else if (line.startsWith("vote-status ")) {
if (!line.equals("vote-status consensus")) {
throw new RuntimeException("Line '" + line + "' indicates "
+ "that this string is not a consensus. Aborting "
+ "parsing.");
}
} else if (line.startsWith("consensus-method ")) {
this.consensusMethod = Integer.parseInt(line.substring(
"consensus-method ".length()));
} else if (line.startsWith("valid-after ")) {
this.validAfterMillis = dateTimeFormat.parse(
line.substring("valid-after ".length())).getTime();
} else if (line.startsWith("fresh-until ")) {
this.freshUntilMillis = dateTimeFormat.parse(
line.substring("fresh-until ".length())).getTime();
} else if (line.startsWith("valid-until ")) {
this.validUntilMillis = dateTimeFormat.parse(
line.substring("valid-until ".length())).getTime();
} else if (line.startsWith("voting-delay ")) {
for (String votingDelayString : line.substring(
"voting-delay ".length()).split(" ")) {
this.votingDelay.add(Long.parseLong(votingDelayString));
}
} else if (line.startsWith("client-versions ")) {
this.recommendedClientVersions = new TreeSet<String>(
Arrays.asList(line.split(" ")[1].split(",")));
} else if (line.startsWith("server-versions ")) {
this.recommendedServerVersions = new TreeSet<String>(
Arrays.asList(line.split(" ")[1].split(",")));
} else if (line.startsWith("known-flags ")) {
for (String flag : line.substring("known-flags ".length()).
split(" ")) {
this.knownFlags.add(flag);
}
} else if (line.startsWith("params ")) {
if (line.length() > "params ".length()) {
for (String param :
line.substring("params ".length()).split(" ")) {
String paramName = param.split("=")[0];
String paramValue = param.split("=")[1];
this.consensusParams.put(paramName, paramValue);
}
}
} else if (line.startsWith("dir-source ") ||
line.startsWith("r ") || line.equals("directory-footer")) {
/* TODO Add code for parsing legacy dir sources. */
if (line.length() < 1) {
throw new DescriptorParseException("Empty lines are not "
+ "allowed in a consensus.");
}
String[] parts = line.split(" ");
if (parts.length < 1) {
throw new DescriptorParseException("No keyword found in line '"
+ line + "'.");
}
String keyword = parts[0];
if (keyword.length() < 1) {
throw new DescriptorParseException("Empty keyword in line '"
+ line + "'.");
}
if (keyword.equals("vote-status")) {
this.parseVoteStatusLine(line, parts);
} else if (keyword.equals("consensus-method")) {
this.parseConsensusMethodLine(line, parts);
} else if (keyword.equals("valid-after")) {
this.parseValidAfterLine(line, parts);
} else if (keyword.equals("fresh-until")) {
this.parseFreshUntilLine(line, parts);
} else if (keyword.equals("valid-until")) {
this.parseValidUntilLine(line, parts);
} else if (keyword.equals("voting-delay")) {
this.parseVotingDelayLine(line, parts);
} else if (keyword.equals("client-versions")) {
this.parseClientVersionsLine(line, parts);
} else if (keyword.equals("server-versions")) {
this.parseServerVersionsLine(line, parts);
} else if (keyword.equals("known-flags")) {
this.parseKnownFlagsLine(line, parts);
} else if (keyword.equals("params")) {
this.parseParamsLine(line, parts);
} else if (keyword.equals("dir-source") || keyword.equals("r") ||
keyword.equals("directory-footer")) {
if (dirSourceEntryLines != null) {
DirSourceEntry dirSourceEntry = new DirSourceEntryImpl(
dirSourceEntryLines.toString().getBytes());
this.dirSourceEntries.put(dirSourceEntry.getIdentity(),
dirSourceEntry);
this.parseDirSourceEntryLines(dirSourceEntryLines.toString());
dirSourceEntryLines = null;
}
if (statusEntryLines != null) {
NetworkStatusEntryImpl statusEntry =
new NetworkStatusEntryImpl(
statusEntryLines.toString().getBytes());
this.statusEntries.put(statusEntry.getFingerprint(),
statusEntry);
this.parseStatusEntryLines(statusEntryLines.toString());
statusEntryLines = null;
}
if (line.startsWith("dir-source ")) {
dirSourceEntryLines = new StringBuilder();
dirSourceEntryLines.append(line + "\n");
} else if (line.startsWith("r ")) {
statusEntryLines = new StringBuilder();
statusEntryLines.append(line + "\n");
if (keyword.equals("dir-source")) {
dirSourceEntryLines = new StringBuilder(line + "\n");
} else if (keyword.equals("r")) {
statusEntryLines = new StringBuilder(line + "\n");
} else if (keyword.equals("directory-footer")) {
this.parsedExactlyOnceKeyword("directory-footer");
}
} else if (keyword.equals("contact") ||
keyword.equals("vote-digest")) {
if (dirSourceEntryLines == null) {
throw new DescriptorParseException(keyword + " line with no "
+ "preceding dir-source line.");
}
} else if (line.startsWith("contact ") ||
line.startsWith("vote-digest ")) {
dirSourceEntryLines.append(line + "\n");
} else if (line.startsWith("s ") || line.equals("s") ||
line.startsWith("v ") || line.startsWith("w ") ||
line.startsWith("p ")) {
statusEntryLines.append(line + "\n");
} else if (line.startsWith("bandwidth-weights ")) {
if (line.length() > "bandwidth-weights ".length()) {
for (String weight : line.substring("bandwidth-weights ".
length()).split(" ")) {
String weightName = weight.split("=")[0];
String weightValue = weight.split("=")[1];
this.bandwidthWeights.put(weightName, weightValue);
}
} else if (keyword.equals("s") || keyword.equals("v") ||
keyword.equals("w") || keyword.equals("p")) {
if (statusEntryLines == null) {
throw new DescriptorParseException(keyword + " line with no "
+ "preceding r line.");
}
} else if (line.startsWith("directory-signature ")) {
String[] parts = line.split(" ");
String identity = parts[1];
String signingKeyDigest = parts[2];
this.directorySignatures.put(identity, signingKeyDigest);
statusEntryLines.append(line + "\n");
} else if (keyword.equals("bandwidth-weights")) {
this.parseBandwidthWeightsLine(line, parts);
} else if (keyword.equals("directory-signature")) {
this.parseDirectorySignatureLine(line, parts);
} else if (line.equals("-----BEGIN SIGNATURE-----")) {
skipSignature = true;
} else if (line.equals("-----END SIGNATURE-----")) {
skipSignature = false;
} else if (!skipSignature) {
throw new RuntimeException("Unrecognized line '" + line + "'.");
/* TODO Is throwing an exception the right thing to do here?
* This is probably fine for development, but once the library
* is in production use, this seems annoying. */
throw new DescriptorParseException("Unrecognized line '" + line
+ "'.");
}
}
} catch (IOException e) {
throw new RuntimeException("Internal error: Ran into an "
+ "IOException while parsing a String in memory. Something's "
+ "really wrong.", e);
} catch (ParseException e) {
/* TODO Handle me. */
} catch (NumberFormatException e) {
/* TODO Handle me. In theory, we shouldn't catch runtime
* exceptions, but in this case it keeps the parsing code small. */
} catch (ArrayIndexOutOfBoundsException e) {
/* TODO Handle me. In theory, we shouldn't catch runtime
* exceptions, but in this case it keeps the parsing code small. */
}
}
private void checkConsistency() {
if (this.networkStatusVersion == 0) {
throw new RuntimeException("Consensus doesn't contain a "
+ "'network-status-version' line.");
private void parseVoteStatusLine(String line, String[] parts)
throws DescriptorParseException {
this.parsedExactlyOnceKeyword("vote-status");
if (parts.length != 2 || !parts[1].equals("consensus")) {
throw new DescriptorParseException("Line '" + line + "' indicates "
+ "that this is not a consensus.");
}
if (this.consensusMethod == 0) {
throw new RuntimeException("Consensus doesn't contain a "
+ "'consensus-method' line.");
}
private void parseConsensusMethodLine(String line, String[] parts)
throws DescriptorParseException {
this.parsedExactlyOnceKeyword("consensus-method");
if (parts.length != 2) {
throw new DescriptorParseException("Illegal line '" + line
+ "' in consensus.");
}
if (this.validAfterMillis == 0L) {
throw new RuntimeException("Consensus doesn't contain a "
+ "'valid-after' line.");
try {
this.consensusMethod = Integer.parseInt(parts[1]);
} catch (NumberFormatException e) {
throw new DescriptorParseException("Illegal consensus method "
+ "number in line '" + line + "'.");
}
if (this.freshUntilMillis == 0L) {
throw new RuntimeException("Consensus doesn't contain a "
+ "'fresh-until' line.");
if (this.consensusMethod < 1) {
throw new DescriptorParseException("Illegal consensus method "
+ "number in line '" + line + "'.");
}
if (this.validUntilMillis == 0L) {
throw new RuntimeException("Consensus doesn't contain a "
+ "'valid-until' line.");
}
private void parseValidAfterLine(String line, String[] parts)
throws DescriptorParseException {
this.parsedExactlyOnceKeyword("valid-after");
this.validAfterMillis = ParseHelper.parseTimestampAtIndex(line, parts,
1, 2);
}
private void parseFreshUntilLine(String line, String[] parts)
throws DescriptorParseException {
this.parsedExactlyOnceKeyword("fresh-until");
this.freshUntilMillis = ParseHelper.parseTimestampAtIndex(line, parts,
1, 2);
}
private void parseValidUntilLine(String line, String[] parts)
throws DescriptorParseException {
this.parsedExactlyOnceKeyword("valid-until");
this.validUntilMillis = ParseHelper.parseTimestampAtIndex(line, parts,
1, 2);
}
private void parseVotingDelayLine(String line, String[] parts)
throws DescriptorParseException {
this.parsedExactlyOnceKeyword("voting-delay");
if (parts.length != 3) {
throw new DescriptorParseException("Wrong number of values in line "
+ "'" + line + "'.");
}
if (this.votingDelay.isEmpty()) {
throw new RuntimeException("Consensus doesn't contain a "
+ "'voting-delay' line.");
try {
this.voteSeconds = Long.parseLong(parts[1]);
this.distSeconds = Long.parseLong(parts[2]);
} catch (NumberFormatException e) {
throw new DescriptorParseException("Illegal values in line '" + line
+ "'.");
}
if (this.knownFlags.isEmpty()) {
throw new RuntimeException("Consensus doesn't contain a "
+ "'known-flags' line.");
}
private void parseClientVersionsLine(String line, String[] parts)
throws DescriptorParseException {
this.parsedAtMostOnceKeyword("client-versions");
this.recommendedClientVersions = this.parseClientOrServerVersions(
line, parts);
}
private void parseServerVersionsLine(String line, String[] parts)
throws DescriptorParseException {
this.parsedAtMostOnceKeyword("server-versions");
this.recommendedServerVersions = this.parseClientOrServerVersions(
line, parts);
}
private SortedSet<String> parseClientOrServerVersions(String line,
String[] parts) throws DescriptorParseException {
SortedSet<String> result = new TreeSet<String>();
if (parts.length == 1) {
return result;
} else if (parts.length > 2) {
throw new DescriptorParseException("Illegal versions line '" + line
+ "'.");
}
if (this.dirSourceEntries.isEmpty()) {
throw new RuntimeException("Consensus doesn't contain any "
+ "'dir-source' lines.");
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);
}
if (this.statusEntries.isEmpty()) {
throw new RuntimeException("Consensus doesn't contain any 'r' "
+ "lines.");
return result;
}
private void parseKnownFlagsLine(String line, String[] parts)
throws DescriptorParseException {
this.parsedExactlyOnceKeyword("known-flags");
if (parts.length < 2) {
throw new DescriptorParseException("No known flags in line '" + line
+ "'.");
}
for (int i = 1; i < parts.length; i++) {
this.knownFlags.add(parts[i]);
}
}
private void parseParamsLine(String line, String[] parts)
throws DescriptorParseException {
this.parsedAtMostOnceKeyword("params");
this.consensusParams = ParseHelper.parseKeyValuePairs(line, parts, 1);
}
private void parseDirSourceEntryLines(String string)
throws DescriptorParseException {
DirSourceEntry dirSourceEntry = new DirSourceEntryImpl(
string.getBytes());
this.dirSourceEntries.put(dirSourceEntry.getIdentity(),
dirSourceEntry);
}
private void parseStatusEntryLines(String string)
throws DescriptorParseException {
NetworkStatusEntryImpl statusEntry = new NetworkStatusEntryImpl(
string.getBytes());
this.statusEntries.put(statusEntry.getFingerprint(), statusEntry);
}
private void parseBandwidthWeightsLine(String line, String[] parts)
throws DescriptorParseException {
this.parsedAtMostOnceKeyword("bandwidth-weights");
this.bandwidthWeights = ParseHelper.parseKeyValuePairs(line, parts,
1);
}
private void parseDirectorySignatureLine(String line, String[] parts)
throws DescriptorParseException {
if (parts.length != 3) {
throw new DescriptorParseException("Illegal line '" + line + "'.");
}
String identity = ParseHelper.parseTwentyByteHexString(line,
parts[1]);
String signingKeyDigest = ParseHelper.parseTwentyByteHexString(line,
parts[2]);
this.directorySignatures.put(identity, signingKeyDigest);
}
private byte[] consensusBytes;
@ -252,9 +387,14 @@ public class RelayNetworkStatusConsensusImpl
return this.validUntilMillis;
}
private List<Long> votingDelay = new ArrayList<Long>();
public List<Long> getVotingDelay() {
return new ArrayList<Long>(this.votingDelay);
private long voteSeconds;
public long getVoteSeconds() {
return this.voteSeconds;
}
private long distSeconds;
public long getDistSeconds() {
return this.distSeconds;
}
private SortedSet<String> recommendedClientVersions;
@ -274,10 +414,10 @@ public class RelayNetworkStatusConsensusImpl
return new TreeSet<String>(this.knownFlags);
}
private SortedMap<String, String> consensusParams =
new TreeMap<String, String>();
public SortedMap<String, String> getConsensusParams() {
return new TreeMap<String, String>(this.consensusParams);
private SortedMap<String, Integer> consensusParams;
public SortedMap<String, Integer> getConsensusParams() {
return this.consensusParams == null ? null:
new TreeMap<String, Integer>(this.consensusParams);
}
private SortedMap<String, DirSourceEntry> dirSourceEntries =
@ -304,10 +444,10 @@ public class RelayNetworkStatusConsensusImpl
return new TreeMap<String, String>(this.directorySignatures);
}
private SortedMap<String, String> bandwidthWeights =
new TreeMap<String, String>();
public SortedMap<String, String> getBandwidthWeights() {
return new TreeMap<String, String>(this.bandwidthWeights);
private SortedMap<String, Integer> bandwidthWeights;
public SortedMap<String, Integer> getBandwidthWeights() {
return this.bandwidthWeights == null ? null :
new TreeMap<String, Integer>(this.bandwidthWeights);
}
}

View File

@ -149,11 +149,16 @@ public class RelayNetworkStatusVoteImpl
} else if (line.startsWith("r ") ||
line.equals("directory-footer")) {
if (statusEntryLines != null) {
NetworkStatusEntryImpl statusEntry =
new NetworkStatusEntryImpl(
statusEntryLines.toString().getBytes());
this.statusEntries.put(statusEntry.getFingerprint(),
statusEntry);
try {
NetworkStatusEntryImpl statusEntry =
new NetworkStatusEntryImpl(
statusEntryLines.toString().getBytes());
this.statusEntries.put(statusEntry.getFingerprint(),
statusEntry);
} catch (DescriptorParseException e) {
System.err.println("Could not parse status entry in vote. "
+ "Skipping.");
}
statusEntryLines = null;
}
if (line.startsWith("r ")) {