commit 7a8f1ffd2d95388892efe0f4198ffae0c076aa34
Author: Karsten Loesing <karsten.loesing(a)gmx.net>
Date: Tue Aug 18 09:52:36 2015 +0200
Add "alleged_family" and "indirect_family" fields.
Implements #16750.
---
build.xml | 2 +-
.../torproject/onionoo/docs/DetailsDocument.java | 16 +++
.../org/torproject/onionoo/docs/DetailsStatus.java | 16 +++
.../org/torproject/onionoo/docs/NodeStatus.java | 83 ++++++++++-----
.../torproject/onionoo/server/ResponseBuilder.java | 6 +-
.../onionoo/updater/NodeDetailsStatusUpdater.java | 84 +++++++++++----
.../onionoo/writer/DetailsDocumentWriter.java | 20 ++++
.../onionoo/writer/SummaryDocumentWriter.java | 5 +-
.../torproject/onionoo/docs/NodeStatusTest.java | 112 ++++++++++++++++++++
web/protocol.html | 34 ++++++
10 files changed, 326 insertions(+), 52 deletions(-)
diff --git a/build.xml b/build.xml
index f82b49d..03a97f1 100644
--- a/build.xml
+++ b/build.xml
@@ -1,6 +1,6 @@
<project default="dist" name="onionoo" basedir=".">
- <property name="onionoo.protocol.version" value="2.5"/>
+ <property name="onionoo.protocol.version" value="2.6"/>
<property name="release.version"
value="${onionoo.protocol.version}.0"/>
<property name="javasources" value="src/main/java"/>
diff --git a/src/main/java/org/torproject/onionoo/docs/DetailsDocument.java b/src/main/java/org/torproject/onionoo/docs/DetailsDocument.java
index aa410d8..69d8efe 100644
--- a/src/main/java/org/torproject/onionoo/docs/DetailsDocument.java
+++ b/src/main/java/org/torproject/onionoo/docs/DetailsDocument.java
@@ -289,6 +289,14 @@ public class DetailsDocument extends Document {
return this.family;
}
+ private SortedSet<String> alleged_family;
+ public void setAllegedFamily(SortedSet<String> allegedFamily) {
+ this.alleged_family = allegedFamily;
+ }
+ public SortedSet<String> getAllegedFamily() {
+ return this.alleged_family;
+ }
+
private SortedSet<String> effective_family;
public void setEffectiveFamily(SortedSet<String> effectiveFamily) {
this.effective_family = effectiveFamily;
@@ -297,6 +305,14 @@ public class DetailsDocument extends Document {
return this.effective_family;
}
+ private SortedSet<String> indirect_family;
+ public void setIndirectFamily(SortedSet<String> indirectFamily) {
+ this.indirect_family = indirectFamily;
+ }
+ public SortedSet<String> getIndirectFamily() {
+ return this.indirect_family;
+ }
+
private Float consensus_weight_fraction;
public void setConsensusWeightFraction(Float consensusWeightFraction) {
if (consensusWeightFraction == null ||
diff --git a/src/main/java/org/torproject/onionoo/docs/DetailsStatus.java b/src/main/java/org/torproject/onionoo/docs/DetailsStatus.java
index 0a97fd3..62e621b 100644
--- a/src/main/java/org/torproject/onionoo/docs/DetailsStatus.java
+++ b/src/main/java/org/torproject/onionoo/docs/DetailsStatus.java
@@ -110,6 +110,14 @@ public class DetailsStatus extends Document {
return this.family;
}
+ private SortedSet<String> alleged_family;
+ public void setAllegedFamily(SortedSet<String> allegedFamily) {
+ this.alleged_family = allegedFamily;
+ }
+ public SortedSet<String> getAllegedFamily() {
+ return this.alleged_family;
+ }
+
private SortedSet<String> effective_family;
public void setEffectiveFamily(SortedSet<String> effectiveFamily) {
this.effective_family = effectiveFamily;
@@ -118,6 +126,14 @@ public class DetailsStatus extends Document {
return this.effective_family;
}
+ private SortedSet<String> indirect_family;
+ public void setIndirectFamily(SortedSet<String> indirectFamily) {
+ this.indirect_family = indirectFamily;
+ }
+ public SortedSet<String> getIndirectFamily() {
+ return this.indirect_family;
+ }
+
private Map<String, List<String>> exit_policy_v6_summary;
public void setExitPolicyV6Summary(
Map<String, List<String>> exitPolicyV6Summary) {
diff --git a/src/main/java/org/torproject/onionoo/docs/NodeStatus.java b/src/main/java/org/torproject/onionoo/docs/NodeStatus.java
index 51fc678..6e51190 100644
--- a/src/main/java/org/torproject/onionoo/docs/NodeStatus.java
+++ b/src/main/java/org/torproject/onionoo/docs/NodeStatus.java
@@ -46,13 +46,12 @@ public class NodeStatus extends Document {
return this.contact;
}
- private String[] familyFingerprints;
- public void setFamilyFingerprints(
- SortedSet<String> familyFingerprints) {
- this.familyFingerprints = collectionToStringArray(familyFingerprints);
+ private String[] declaredFamily;
+ public void setDeclaredFamily(SortedSet<String> declaredFamily) {
+ this.declaredFamily = collectionToStringArray(declaredFamily);
}
- public SortedSet<String> getFamilyFingerprints() {
- return stringArrayToSortedSet(this.familyFingerprints);
+ public SortedSet<String> getDeclaredFamily() {
+ return stringArrayToSortedSet(this.declaredFamily);
}
private static String[] collectionToStringArray(
@@ -301,7 +300,8 @@ public class NodeStatus extends Document {
return this.lastRdnsLookup;
}
- /* Computed effective family */
+ /* Computed effective and extended family and derived subsets alleged
+ * and indirect family */
private String[] effectiveFamily;
public void setEffectiveFamily(SortedSet<String> effectiveFamily) {
@@ -311,6 +311,28 @@ public class NodeStatus extends Document {
return stringArrayToSortedSet(this.effectiveFamily);
}
+ private String[] extendedFamily;
+ public void setExtendedFamily(SortedSet<String> extendedFamily) {
+ this.extendedFamily = collectionToStringArray(extendedFamily);
+ }
+ public SortedSet<String> getExtendedFamily() {
+ return stringArrayToSortedSet(this.extendedFamily);
+ }
+
+ public SortedSet<String> getAllegedFamily() {
+ SortedSet<String> allegedFamily = new TreeSet<String>(
+ stringArrayToSortedSet(this.declaredFamily));
+ allegedFamily.removeAll(stringArrayToSortedSet(this.effectiveFamily));
+ return allegedFamily;
+ }
+
+ public SortedSet<String> getIndirectFamily() {
+ SortedSet<String> indirectFamily = new TreeSet<String>(
+ stringArrayToSortedSet(this.extendedFamily));
+ indirectFamily.removeAll(stringArrayToSortedSet(this.effectiveFamily));
+ return indirectFamily;
+ }
+
/* Constructor and (de-)serialization methods: */
public NodeStatus(String fingerprint) {
@@ -412,17 +434,33 @@ public class NodeStatus extends Document {
nodeStatus.setRecommendedVersion(parts[21].equals("true"));
}
if (!parts[22].equals("null")) {
- SortedSet<String> familyFingerprints = new TreeSet<String>();
- for (String familyMember : parts[22].split("[;:]")) {
- if (familyMember.length() > 0) {
- familyFingerprints.add(familyMember);
- }
+ /* The relay's family is encoded in three ':'-separated groups:
+ * 0. declared, not-mutually agreed family members,
+ * 1. effective, mutually agreed family members, and
+ * 2. indirect members that can be reached via others only.
+ * Each group contains zero or more ';'-separated fingerprints. */
+ String[] groups = parts[22].split(":", -1);
+ SortedSet<String> allegedFamily = new TreeSet<String>(),
+ effectiveFamily = new TreeSet<String>(),
+ indirectFamily = new TreeSet<String>();
+ if (groups[0].length() > 0) {
+ allegedFamily.addAll(Arrays.asList(groups[0].split(";")));
+ }
+ if (groups.length > 1 && groups[1].length() > 0) {
+ effectiveFamily.addAll(Arrays.asList(groups[1].split(";")));
}
- nodeStatus.setFamilyFingerprints(familyFingerprints);
- if (parts[22].contains(":")) {
- nodeStatus.setEffectiveFamily(new TreeSet<String>(
- Arrays.asList(parts[22].split(":", 2)[1].split(";"))));
+ if (groups.length > 2 && groups[2].length() > 0) {
+ indirectFamily.addAll(Arrays.asList(groups[2].split(";")));
}
+ SortedSet<String> declaredFamily = new TreeSet<String>();
+ declaredFamily.addAll(allegedFamily);
+ declaredFamily.addAll(effectiveFamily);
+ nodeStatus.setDeclaredFamily(declaredFamily);
+ nodeStatus.setEffectiveFamily(effectiveFamily);
+ SortedSet<String> extendedFamily = new TreeSet<String>();
+ extendedFamily.addAll(effectiveFamily);
+ extendedFamily.addAll(indirectFamily);
+ nodeStatus.setExtendedFamily(extendedFamily);
}
return nodeStatus;
} catch (NumberFormatException e) {
@@ -483,16 +521,9 @@ public class NodeStatus extends Document {
sb.append("\t" + (this.contact != null ? this.contact : ""));
sb.append("\t" + (this.recommendedVersion == null ? "null" :
this.recommendedVersion ? "true" : "false"));
- if (this.effectiveFamily != null && this.effectiveFamily.length > 0) {
- SortedSet<String> mutual = this.getEffectiveFamily();
- SortedSet<String> notMutual = new TreeSet<String>(
- this.getFamilyFingerprints());
- notMutual.removeAll(mutual);
- sb.append("\t" + StringUtils.join(notMutual, ";") + ":"
- + StringUtils.join(mutual, ";"));
- } else {
- sb.append("\t" + StringUtils.join(this.familyFingerprints, ";"));
- }
+ sb.append("\t" + StringUtils.join(this.getAllegedFamily(), ";") + ":"
+ + StringUtils.join(this.getEffectiveFamily(), ";") + ":"
+ + StringUtils.join(this.getIndirectFamily(), ";"));
return sb.toString();
}
}
diff --git a/src/main/java/org/torproject/onionoo/server/ResponseBuilder.java b/src/main/java/org/torproject/onionoo/server/ResponseBuilder.java
index ffd6ddb..b8aafc3 100644
--- a/src/main/java/org/torproject/onionoo/server/ResponseBuilder.java
+++ b/src/main/java/org/torproject/onionoo/server/ResponseBuilder.java
@@ -70,7 +70,7 @@ public class ResponseBuilder {
return this.charsWritten;
}
- private static final String PROTOCOL_VERSION = "2.5";
+ private static final String PROTOCOL_VERSION = "2.6";
private static final String NEXT_MAJOR_VERSION_SCHEDULED = null;
@@ -271,6 +271,10 @@ public class ResponseBuilder {
dd.setEffectiveFamily(detailsDocument.getEffectiveFamily());
} else if (field.equals("measured")) {
dd.setMeasured(detailsDocument.getMeasured());
+ } else if (field.equals("alleged_family")) {
+ dd.setAllegedFamily(detailsDocument.getAllegedFamily());
+ } else if (field.equals("indirect_family")) {
+ dd.setIndirectFamily(detailsDocument.getIndirectFamily());
}
}
/* Don't escape HTML characters, like < and >, contained in
diff --git a/src/main/java/org/torproject/onionoo/updater/NodeDetailsStatusUpdater.java b/src/main/java/org/torproject/onionoo/updater/NodeDetailsStatusUpdater.java
index fdcd419..d28e249 100644
--- a/src/main/java/org/torproject/onionoo/updater/NodeDetailsStatusUpdater.java
+++ b/src/main/java/org/torproject/onionoo/updater/NodeDetailsStatusUpdater.java
@@ -140,7 +140,7 @@ public class NodeDetailsStatusUpdater implements DescriptorListener,
}
}
- private Map<String, SortedSet<String>> familyFingerprints =
+ private Map<String, SortedSet<String>> declaredFamilies =
new HashMap<String, SortedSet<String>>();
private void processRelayServerDescriptor(
@@ -174,14 +174,16 @@ public class NodeDetailsStatusUpdater implements DescriptorListener,
detailsStatus.setPlatform(descriptor.getPlatform());
detailsStatus.setFamily(descriptor.getFamilyEntries());
if (descriptor.getFamilyEntries() != null) {
- SortedSet<String> noPrefixUpperCase = new TreeSet<String>();
+ SortedSet<String> declaredFamily = new TreeSet<String>();
for (String familyMember : descriptor.getFamilyEntries()) {
if (familyMember.startsWith("$") && familyMember.length() >= 41) {
- noPrefixUpperCase.add(
+ declaredFamily.add(
familyMember.substring(1, 41).toUpperCase());
+ } else {
+ declaredFamily.add(familyMember);
}
}
- this.familyFingerprints.put(fingerprint, noPrefixUpperCase);
+ this.declaredFamilies.put(fingerprint, declaredFamily);
}
if (descriptor.getIpv6DefaultPolicy() != null &&
(descriptor.getIpv6DefaultPolicy().equals("accept") ||
@@ -382,8 +384,8 @@ public class NodeDetailsStatusUpdater implements DescriptorListener,
log.info("Looked up cities and ASes");
this.calculatePathSelectionProbabilities();
log.info("Calculated path selection probabilities");
- this.computeEffectiveFamilies();
- log.info("Computed effective families");
+ this.computeEffectiveAndExtendedFamilies();
+ log.info("Computed effective and extended families");
this.finishReverseDomainNameLookups();
log.info("Finished reverse domain name lookups");
this.updateNodeDetailsStatuses();
@@ -479,10 +481,12 @@ public class NodeDetailsStatusUpdater implements DescriptorListener,
}
updatedNodeStatus.setLastRdnsLookup(
nodeStatus.getLastRdnsLookup());
- updatedNodeStatus.setFamilyFingerprints(
- nodeStatus.getFamilyFingerprints());
+ updatedNodeStatus.setDeclaredFamily(
+ nodeStatus.getDeclaredFamily());
updatedNodeStatus.setEffectiveFamily(
nodeStatus.getEffectiveFamily());
+ updatedNodeStatus.setExtendedFamily(
+ nodeStatus.getExtendedFamily());
} else {
updatedNodeStatus = nodeStatus;
this.knownNodes.put(fingerprint, nodeStatus);
@@ -511,13 +515,13 @@ public class NodeDetailsStatusUpdater implements DescriptorListener,
* safe to override whatever is in node statuses. */
for (Map.Entry<String, NodeStatus> e : this.knownNodes.entrySet()) {
String fingerprint = e.getKey();
- if (this.familyFingerprints.containsKey(fingerprint)) {
+ if (this.declaredFamilies.containsKey(fingerprint)) {
NodeStatus nodeStatus = e.getValue();
- nodeStatus.setFamilyFingerprints(
- this.familyFingerprints.get(fingerprint));
+ nodeStatus.setDeclaredFamily(
+ this.declaredFamilies.get(fingerprint));
}
}
- this.familyFingerprints.clear();
+ this.declaredFamilies.clear();
}
/* Step 3: perform lookups and calculate path selection
@@ -664,16 +668,14 @@ public class NodeDetailsStatusUpdater implements DescriptorListener,
}
}
- private void computeEffectiveFamilies() {
+ private void computeEffectiveAndExtendedFamilies() {
SortedMap<String, SortedSet<String>> declaredFamilies =
new TreeMap<String, SortedSet<String>>();
for (String fingerprint : this.currentRelays) {
NodeStatus nodeStatus = this.knownNodes.get(fingerprint);
- if (nodeStatus != null &&
- nodeStatus.getFamilyFingerprints() != null &&
- !nodeStatus.getFamilyFingerprints().isEmpty()) {
- declaredFamilies.put(fingerprint,
- nodeStatus.getFamilyFingerprints());
+ if (nodeStatus != null && nodeStatus.getDeclaredFamily() != null &&
+ !nodeStatus.getDeclaredFamily().isEmpty()) {
+ declaredFamilies.put(fingerprint, nodeStatus.getDeclaredFamily());
}
}
SortedMap<String, SortedSet<String>> effectiveFamilies =
@@ -694,17 +696,55 @@ public class NodeDetailsStatusUpdater implements DescriptorListener,
effectiveFamilies.put(fingerprint, effectiveFamily);
}
}
+ SortedMap<String, SortedSet<String>> extendedFamilies =
+ new TreeMap<String, SortedSet<String>>();
+ SortedSet<String> visited = new TreeSet<String>();
+ for (String fingerprint : effectiveFamilies.keySet()) {
+ if (visited.contains(fingerprint)) {
+ continue;
+ }
+ SortedSet<String> toVisit = new TreeSet<String>();
+ toVisit.add(fingerprint);
+ SortedSet<String> extendedFamily = new TreeSet<String>();
+ while (!toVisit.isEmpty()) {
+ String visiting = toVisit.first();
+ toVisit.remove(visiting);
+ extendedFamily.add(visiting);
+ SortedSet<String> members = effectiveFamilies.get(visiting);
+ if (members != null) {
+ for (String member : members) {
+ if (!toVisit.contains(member) && !visited.contains(member)) {
+ toVisit.add(member);
+ }
+ }
+ }
+ visited.add(visiting);
+ }
+ if (extendedFamily.size() > 1) {
+ for (String member : extendedFamily) {
+ SortedSet<String> extendedFamilyWithoutMember =
+ new TreeSet<String>(extendedFamily);
+ extendedFamilyWithoutMember.remove(member);
+ extendedFamilies.put(member, extendedFamilyWithoutMember);
+ }
+ }
+ }
for (String fingerprint : this.currentRelays) {
NodeStatus nodeStatus = this.knownNodes.get(fingerprint);
if (nodeStatus == null) {
continue;
}
- if (effectiveFamilies.containsKey(fingerprint)) {
+ if (effectiveFamilies.containsKey(fingerprint) ||
+ extendedFamilies.containsKey(fingerprint)) {
nodeStatus.setEffectiveFamily(effectiveFamilies.get(fingerprint));
+ nodeStatus.setExtendedFamily(extendedFamilies.get(fingerprint));
this.updatedNodes.add(fingerprint);
- } else if (nodeStatus.getEffectiveFamily() != null ||
- !nodeStatus.getEffectiveFamily().isEmpty()) {
+ } else if ((nodeStatus.getEffectiveFamily() != null &&
+ !nodeStatus.getEffectiveFamily().isEmpty()) ||
+ (nodeStatus.getIndirectFamily() != null &&
+ !nodeStatus.getIndirectFamily().isEmpty())) {
nodeStatus.setEffectiveFamily(null);
+ nodeStatus.setExtendedFamily(null);
this.updatedNodes.add(fingerprint);
}
}
@@ -774,7 +814,9 @@ public class NodeDetailsStatusUpdater implements DescriptorListener,
nodeStatus.getOrAddresses());
nodeStatus.setExitAddresses(exitAddressesWithoutOrAddresses);
+ detailsStatus.setAllegedFamily(nodeStatus.getAllegedFamily());
detailsStatus.setEffectiveFamily(nodeStatus.getEffectiveFamily());
+ detailsStatus.setIndirectFamily(nodeStatus.getIndirectFamily());
if (this.geoIpLookupResults.containsKey(fingerprint)) {
LookupResult lookupResult = this.geoIpLookupResults.get(
diff --git a/src/main/java/org/torproject/onionoo/writer/DetailsDocumentWriter.java b/src/main/java/org/torproject/onionoo/writer/DetailsDocumentWriter.java
index 6f60958..6747c2c 100644
--- a/src/main/java/org/torproject/onionoo/writer/DetailsDocumentWriter.java
+++ b/src/main/java/org/torproject/onionoo/writer/DetailsDocumentWriter.java
@@ -112,6 +112,18 @@ public class DetailsDocumentWriter implements DocumentWriter {
detailsDocument.setContact(detailsStatus.getContact());
detailsDocument.setPlatform(detailsStatus.getPlatform());
detailsDocument.setFamily(detailsStatus.getFamily());
+ if (detailsStatus.getAllegedFamily() != null &&
+ !detailsStatus.getAllegedFamily().isEmpty()) {
+ SortedSet<String> allegedFamily = new TreeSet<String>();
+ for (String familyMember : detailsStatus.getAllegedFamily()) {
+ if (familyMember.length() >= 40) {
+ allegedFamily.add("$" + familyMember);
+ } else {
+ allegedFamily.add(familyMember);
+ }
+ }
+ detailsDocument.setAllegedFamily(allegedFamily);
+ }
if (detailsStatus.getEffectiveFamily() != null &&
!detailsStatus.getEffectiveFamily().isEmpty()) {
SortedSet<String> effectiveFamily = new TreeSet<String>();
@@ -120,6 +132,14 @@ public class DetailsDocumentWriter implements DocumentWriter {
}
detailsDocument.setEffectiveFamily(effectiveFamily);
}
+ if (detailsStatus.getIndirectFamily() != null &&
+ !detailsStatus.getIndirectFamily().isEmpty()) {
+ SortedSet<String> indirectFamily = new TreeSet<String>();
+ for (String familyMember : detailsStatus.getIndirectFamily()) {
+ indirectFamily.add("$" + familyMember);
+ }
+ detailsDocument.setIndirectFamily(indirectFamily);
+ }
detailsDocument.setExitPolicyV6Summary(
detailsStatus.getExitPolicyV6Summary());
detailsDocument.setHibernating(detailsStatus.getHibernating());
diff --git a/src/main/java/org/torproject/onionoo/writer/SummaryDocumentWriter.java b/src/main/java/org/torproject/onionoo/writer/SummaryDocumentWriter.java
index ddb3003..1be752a 100644
--- a/src/main/java/org/torproject/onionoo/writer/SummaryDocumentWriter.java
+++ b/src/main/java/org/torproject/onionoo/writer/SummaryDocumentWriter.java
@@ -84,13 +84,12 @@ public class SummaryDocumentWriter implements DocumentWriter {
long firstSeenMillis = nodeStatus.getFirstSeenMillis();
String aSNumber = nodeStatus.getASNumber();
String contact = nodeStatus.getContact();
- SortedSet<String> familyFingerprints =
- nodeStatus.getFamilyFingerprints();
+ SortedSet<String> declaredFamily = nodeStatus.getDeclaredFamily();
SortedSet<String> effectiveFamily = nodeStatus.getEffectiveFamily();
SummaryDocument summaryDocument = new SummaryDocument(isRelay,
nickname, fingerprint, addresses, lastSeenMillis, running,
relayFlags, consensusWeight, countryCode, firstSeenMillis,
- aSNumber, contact, familyFingerprints, effectiveFamily);
+ aSNumber, contact, declaredFamily, effectiveFamily);
if (this.documentStore.store(summaryDocument, fingerprint)) {
this.writtenDocuments++;
};
diff --git a/src/test/java/org/torproject/onionoo/docs/NodeStatusTest.java b/src/test/java/org/torproject/onionoo/docs/NodeStatusTest.java
new file mode 100644
index 0000000..e3a6fca
--- /dev/null
+++ b/src/test/java/org/torproject/onionoo/docs/NodeStatusTest.java
@@ -0,0 +1,112 @@
+package org.torproject.onionoo.docs;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.Arrays;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import org.junit.Test;
+
+public class NodeStatusTest {
+
+ private static final String GABELMOO_NODE_STATUS =
+ "r\tgabelmoo\tF2044413DAC2E02E3D6BCF4735A19BCA1DE97281\t"
+ + "131.188.40.189;[2001:638:a000:4140::ffff:189]:443;\t2015-08-13\t"
+ + "08:00:00\t443\t80\tAuthority,HSDir,Running,Stable,V2Dir,Valid\t"
+ + "20\tde\t\t-1\treject\t1-65535\t2015-08-04\t12:00:00\t"
+ + "2015-08-04\t12:00:00\tAS680\t"
+ + "4096r/261c5fbe77285f88fb0c343266c8c2d7c5aa446d sebastian hahn "
+ + "<tor(a)sebastianhahn.net> - 12nbrajag5u3llwetsf7fstcdaz32mu5cn\t"
+ + "true\tnull";
+
+ private void assertFamiliesCanBeDeSerialized(
+ String[] declaredFamilyArray, String[] effectiveFamilyArray,
+ String[] extendedFamilyArray) {
+ SortedSet<String> declaredFamily = new TreeSet<String>(
+ Arrays.asList(declaredFamilyArray));
+ SortedSet<String> effectiveFamily = new TreeSet<String>(
+ Arrays.asList(effectiveFamilyArray));
+ SortedSet<String> extendedFamily = new TreeSet<String>(
+ Arrays.asList(extendedFamilyArray));
+ NodeStatus nodeStatus = NodeStatus.fromString(GABELMOO_NODE_STATUS);
+ nodeStatus.setDeclaredFamily(declaredFamily);
+ nodeStatus.setEffectiveFamily(effectiveFamily);
+ nodeStatus.setExtendedFamily(extendedFamily);
+ String serialized = nodeStatus.toString();
+ NodeStatus deserialized = NodeStatus.fromString(serialized);
+ assertEquals("Declared families don't match", declaredFamily,
+ deserialized.getDeclaredFamily());
+ assertEquals("Effective families don't match", effectiveFamily,
+ deserialized.getEffectiveFamily());
+ assertEquals("Extended families don't match", extendedFamily,
+ deserialized.getExtendedFamily());
+ }
+
+ private final String A = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
+ B = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
+ C = "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC",
+ D = "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD",
+ E = "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE",
+ F = "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
+ N = "nickname";
+
+ @Test
+ public void testFamiliesEmpty() {
+ assertFamiliesCanBeDeSerialized(
+ new String[] {}, new String[] {}, new String[] {});
+ }
+
+ @Test
+ public void testFamiliesOneNotMutual() {
+ assertFamiliesCanBeDeSerialized(
+ new String[] { A }, new String[] {}, new String[] {});
+ }
+
+ @Test
+ public void testFamiliesTwoNotMutual() {
+ assertFamiliesCanBeDeSerialized(
+ new String[] { A, B }, new String[] {}, new String[] {});
+ }
+
+ @Test
+ public void testFamiliesOneNotMutualOneMutual() {
+ assertFamiliesCanBeDeSerialized(
+ new String[] { A, B }, new String[] { B }, new String[] { B });
+ }
+
+ @Test
+ public void testFamiliesOneMutualOneIndirect() {
+ assertFamiliesCanBeDeSerialized(
+ new String[] { A }, new String[] { A }, new String[] { A, B });
+ }
+
+ @Test
+ public void testFamiliesOneNotMutualOneIndirect() {
+ /* This case is special, because B is both in this relay's alleged and
+ * extended family, but it's not in an effective family relationship
+ * with this relay. It's a valid case, because B can be in a mutual
+ * family relationship with A. */
+ assertFamiliesCanBeDeSerialized(
+ new String[] { A, B }, new String[] { A }, new String[] { A, B });
+ }
+
+ @Test
+ public void testFamiliesOneNotMutualOneMutualOneIndirect() {
+ assertFamiliesCanBeDeSerialized(
+ new String[] { A, B }, new String[] { B }, new String[] { B, C });
+ }
+
+ @Test
+ public void testFamiliesTwoNotMutualTwoMutualTwoIndirect() {
+ assertFamiliesCanBeDeSerialized(
+ new String[] { A, B, C, D }, new String[] { C, D },
+ new String[] { C, D, E, F });
+ }
+
+ @Test
+ public void testFamiliesNickname() {
+ assertFamiliesCanBeDeSerialized(
+ new String[] { N }, new String[] {}, new String[] {});
+ }
+}
diff --git a/web/protocol.html b/web/protocol.html
index 6681faf..ab06fe9 100644
--- a/web/protocol.html
+++ b/web/protocol.html
@@ -180,6 +180,9 @@ documents on March 22, 2015.</li>
details documents on July 3, 2015.</li>
<li><strong>2.5</strong>: Added optional "measured" field to details
documents on August 13, 2015.</li>
+<li><strong>2.6</strong>: Added optional "alleged_family" and
+"indirect_family" fields and deprecated optional "family" field in details
+documents on August 25, 2015.</li>
</ul>
</div> <!-- box -->
@@ -1185,6 +1188,7 @@ relay part of their family, so that the effective family of this relay may
be smaller.
Omitted if empty or if descriptor containing this information cannot be
found.
+<font color="blue">Deprecated on August 25, 2015.</font>
</p>
</li>
@@ -1195,6 +1199,8 @@ found.
<p>
Array of $-prefixed fingerprints of relays that are in an effective,
mutual family relationship with this relay.
+These relays are part of this relay's family and they consider this relay
+to be part of their family.
Omitted if empty or if descriptor containing this information cannot be
found.
<font color="blue">Added on July 3, 2015.</font>
@@ -1202,6 +1208,34 @@ found.
</li>
<li>
+<b><font color="blue">alleged_family</font></b>
+<code class="typeof">array of strings</code>
+<span class="required-false">optional</span>
+<p>
+Array of $-prefixed fingerprints of relays that are not in an effective,
+mutual family relationship with this relay.
+These relays are part of this relay's family but they don't consider this
+relay to be part of their family.
+Omitted if empty or if descriptor containing this information cannot be
+found.
+<font color="blue">Added on August 25, 2015.</font>
+</p>
+</li>
+
+<li>
+<b><font color="blue">indirect_family</font></b>
+<code class="typeof">array of strings</code>
+<span class="required-false">optional</span>
+Array of $-prefixed fingerprints of relays that are not in an effective,
+mutual family relationship with this relay but that can be reached by
+following effective, mutual family relationships starting at this relay.
+Omitted if empty or if descriptor containing this information cannot be
+found.
+<font color="blue">Added on August 25, 2015.</font>
+</p>
+</li>
+
+<li>
<b>consensus_weight_fraction</b>
<code class="typeof">number</code>
<span class="required-false">optional</span>