[tor-commits] [onionoo/master] Add new contact parameter to the servlet.

karsten at torproject.org karsten at torproject.org
Fri Jul 19 15:34:11 UTC 2013


commit fd810e6c6b5ee46977142901105e35290806dcf0
Author: Karsten Loesing <karsten.loesing at gmx.net>
Date:   Wed Jul 17 09:55:49 2013 +0200

    Add new contact parameter to the servlet.
    
    Implements #5255.
---
 src/org/torproject/onionoo/DetailsDataWriter.java  |   23 ++++-
 src/org/torproject/onionoo/NodeDataWriter.java     |    4 +-
 src/org/torproject/onionoo/NodeStatus.java         |   72 +++++++++----
 src/org/torproject/onionoo/ResourceServlet.java    |   53 +++++++++-
 .../torproject/onionoo/ResourceServletTest.java    |  107 ++++++++++++++------
 web/index.html                                     |   11 ++
 6 files changed, 212 insertions(+), 58 deletions(-)

diff --git a/src/org/torproject/onionoo/DetailsDataWriter.java b/src/org/torproject/onionoo/DetailsDataWriter.java
index aa511c2..51ca2e9 100644
--- a/src/org/torproject/onionoo/DetailsDataWriter.java
+++ b/src/org/torproject/onionoo/DetailsDataWriter.java
@@ -426,6 +426,10 @@ public class DetailsDataWriter implements DescriptorListener {
     return StringEscapeUtils.escapeJavaScript(s).replaceAll("\\\\'", "'");
   }
 
+  private static String unescapeJSON(String s) {
+    return StringEscapeUtils.unescapeJavaScript(s.replaceAll("'", "\\'"));
+  }
+
   private void updateRelayDetailsFiles(
       SortedSet<String> remainingDetailsFiles) {
     SimpleDateFormat dateTimeFormat = new SimpleDateFormat(
@@ -588,12 +592,29 @@ public class DetailsDataWriter implements DescriptorListener {
         sb.append("]");
       }
 
-      /* Append descriptor-specific part from details status file. */
+      /* Append descriptor-specific part from details status file, and
+       * update contact in node status. */
       DetailsStatus detailsStatus = this.documentStore.retrieve(
           DetailsStatus.class, false, fingerprint);
       if (detailsStatus != null &&
           detailsStatus.documentString.length() > 0) {
         sb.append(",\n" + detailsStatus.documentString);
+        String contact = null;
+        Scanner s = new Scanner(detailsStatus.documentString);
+        while (s.hasNextLine()) {
+          String line = s.nextLine();
+          if (!line.startsWith("\"contact\":")) {
+            continue;
+          }
+          int start = "\"contact\":\"".length(), end = line.length() - 1;
+          if (line.endsWith(",")) {
+            end--;
+          }
+          contact = unescapeJSON(line.substring(start, end));
+          break;
+        }
+        s.close();
+        entry.setContact(contact);
       }
 
       /* Finish details string. */
diff --git a/src/org/torproject/onionoo/NodeDataWriter.java b/src/org/torproject/onionoo/NodeDataWriter.java
index 723dd16..b1e9b5c 100644
--- a/src/org/torproject/onionoo/NodeDataWriter.java
+++ b/src/org/torproject/onionoo/NodeDataWriter.java
@@ -82,7 +82,7 @@ public class NodeDataWriter implements DescriptorListener {
           fingerprint, address, orAddressesAndPorts, null,
           validAfterMillis, orPort, dirPort, relayFlags, consensusWeight,
           null, null, -1L, defaultPolicy, portList, validAfterMillis,
-          validAfterMillis, null);
+          validAfterMillis, null, null);
       if (this.knownNodes.containsKey(fingerprint)) {
         this.knownNodes.get(fingerprint).update(newNodeStatus);
       } else {
@@ -112,7 +112,7 @@ public class NodeDataWriter implements DescriptorListener {
       NodeStatus newNodeStatus = new NodeStatus(false, nickname,
           fingerprint, address, orAddressesAndPorts, null,
           publishedMillis, orPort, dirPort, relayFlags, -1L, "??", null,
-          -1L, null, null, publishedMillis, -1L, null);
+          -1L, null, null, publishedMillis, -1L, null, null);
       if (this.knownNodes.containsKey(fingerprint)) {
         this.knownNodes.get(fingerprint).update(newNodeStatus);
       } else {
diff --git a/src/org/torproject/onionoo/NodeStatus.java b/src/org/torproject/onionoo/NodeStatus.java
index 4f207a0..e549336 100644
--- a/src/org/torproject/onionoo/NodeStatus.java
+++ b/src/org/torproject/onionoo/NodeStatus.java
@@ -55,13 +55,14 @@ public class NodeStatus extends Document {
   private String defaultPolicy;
   private String portList;
   private SortedMap<Long, Set<String>> lastAddresses;
+  private String contact;
   public NodeStatus(boolean isRelay, String nickname, String fingerprint,
       String address, SortedSet<String> orAddressesAndPorts,
       SortedSet<String> exitAddresses, long lastSeenMillis, int orPort,
       int dirPort, SortedSet<String> relayFlags, long consensusWeight,
       String countryCode, String hostName, long lastRdnsLookup,
       String defaultPolicy, String portList, long firstSeenMillis,
-      long lastChangedAddresses, String aSNumber) {
+      long lastChangedAddresses, String aSNumber, String contact) {
     this.isRelay = isRelay;
     this.nickname = nickname;
     this.fingerprint = fingerprint;
@@ -106,13 +107,14 @@ public class NodeStatus extends Document {
     addresses.addAll(orAddressesAndPorts);
     this.lastAddresses.put(lastChangedAddresses, addresses);
     this.aSNumber = aSNumber;
+    this.contact = contact;
   }
 
   public static NodeStatus fromString(String documentString) {
     boolean isRelay = false;
     String nickname = null, fingerprint = null, address = null,
         countryCode = null, hostName = null, defaultPolicy = null,
-        portList = null, aSNumber = null;
+        portList = null, aSNumber = null, contact = null;
     SortedSet<String> orAddressesAndPorts = null, exitAddresses = null,
         relayFlags = null;
     long lastSeenMillis = -1L, consensusWeight = -1L,
@@ -123,7 +125,8 @@ public class NodeStatus extends Document {
       SimpleDateFormat dateTimeFormat = new SimpleDateFormat(
           "yyyy-MM-dd HH:mm:ss");
       dateTimeFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
-      String[] parts = documentString.trim().split(" ");
+      String separator = documentString.contains("\t") ? "\t" : " ";
+      String[] parts = documentString.trim().split(separator);
       isRelay = parts[0].equals("r");
       if (parts.length < 9) {
         System.err.println("Too few space-separated values in line '"
@@ -193,6 +196,9 @@ public class NodeStatus extends Document {
       if (parts.length > 19) {
         aSNumber = parts[19];
       }
+      if (parts.length > 20) {
+        contact = parts[20];
+      }
     } catch (NumberFormatException e) {
       System.err.println("Number format exception while parsing node "
           + "status line '" + documentString + "': " + e.getMessage()
@@ -215,7 +221,7 @@ public class NodeStatus extends Document {
         fingerprint, address, orAddressesAndPorts, exitAddresses,
         lastSeenMillis, orPort, dirPort, relayFlags, consensusWeight,
         countryCode, hostName, lastRdnsLookup, defaultPolicy, portList,
-        firstSeenMillis, lastChangedAddresses, aSNumber);
+        firstSeenMillis, lastChangedAddresses, aSNumber, contact);
     return newNodeStatus;
   }
 
@@ -234,6 +240,7 @@ public class NodeStatus extends Document {
       this.defaultPolicy = newNodeStatus.defaultPolicy;
       this.portList = newNodeStatus.portList;
       this.aSNumber = newNodeStatus.aSNumber;
+      this.contact = newNodeStatus.contact;
     }
     if (this.isRelay && newNodeStatus.isRelay) {
       this.lastAddresses.putAll(newNodeStatus.lastAddresses);
@@ -244,13 +251,13 @@ public class NodeStatus extends Document {
 
   public String toString() {
     SimpleDateFormat dateTimeFormat = new SimpleDateFormat(
-        "yyyy-MM-dd HH:mm:ss");
+        "yyyy-MM-dd\tHH:mm:ss");
     dateTimeFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
     StringBuilder sb = new StringBuilder();
     sb.append(this.isRelay ? "r" : "b");
-    sb.append(" " + this.nickname);
-    sb.append(" " + this.fingerprint);
-    sb.append(" " + this.address + ";");
+    sb.append("\t" + this.nickname);
+    sb.append("\t" + this.fingerprint);
+    sb.append("\t" + this.address + ";");
     int written = 0;
     for (String orAddressAndPort : this.orAddressesAndPorts) {
       sb.append((written++ > 0 ? "+" : "") + orAddressAndPort);
@@ -263,32 +270,34 @@ public class NodeStatus extends Document {
             + exitAddress);
       }
     }
-    sb.append(" " + dateTimeFormat.format(this.lastSeenMillis));
-    sb.append(" " + this.orPort);
-    sb.append(" " + this.dirPort + " ");
+    sb.append("\t" + dateTimeFormat.format(this.lastSeenMillis));
+    sb.append("\t" + this.orPort);
+    sb.append("\t" + this.dirPort + "\t");
     written = 0;
     for (String relayFlag : this.relayFlags) {
       sb.append((written++ > 0 ? "," : "") + relayFlag);
     }
     if (this.isRelay) {
-      sb.append(" " + String.valueOf(this.consensusWeight));
-      sb.append(" " + (this.countryCode != null ? this.countryCode : "??"));
-      sb.append(" " + (this.hostName != null ? this.hostName : "null"));
-      sb.append(" " + String.valueOf(this.lastRdnsLookup));
-      sb.append(" " + (this.defaultPolicy != null ? this.defaultPolicy
+      sb.append("\t" + String.valueOf(this.consensusWeight));
+      sb.append("\t"
+          + (this.countryCode != null ? this.countryCode : "??"));
+      sb.append("\t" + (this.hostName != null ? this.hostName : "null"));
+      sb.append("\t" + String.valueOf(this.lastRdnsLookup));
+      sb.append("\t" + (this.defaultPolicy != null ? this.defaultPolicy
           : "null"));
-      sb.append(" " + (this.portList != null ? this.portList : "null"));
+      sb.append("\t" + (this.portList != null ? this.portList : "null"));
     } else {
-      sb.append(" -1 ?? null -1 null null");
+      sb.append("\t-1\t??\tnull\t-1\tnull\tnull");
     }
-    sb.append(" " + dateTimeFormat.format(this.firstSeenMillis));
+    sb.append("\t" + dateTimeFormat.format(this.firstSeenMillis));
     if (this.isRelay) {
-      sb.append(" " + dateTimeFormat.format(
+      sb.append("\t" + dateTimeFormat.format(
           this.getLastChangedOrAddress()));
-      sb.append(" " + (this.aSNumber != null ? this.aSNumber : "null"));
+      sb.append("\t" + (this.aSNumber != null ? this.aSNumber : "null"));
     } else {
-      sb.append(" null null null");
+      sb.append("\tnull\tnull\tnull");
     }
+    sb.append("\t" + (this.contact != null ? this.contact : ""));
     return sb.toString();
   }
 
@@ -480,5 +489,24 @@ public class NodeStatus extends Document {
     }
     return lastChangedAddressesMillis;
   }
+  public void setContact(String contact) {
+    if (contact == null) {
+      this.contact = null;
+    } else {
+      contact = contact.toLowerCase();
+      StringBuilder sb = new StringBuilder();
+      for (char c : contact.toCharArray()) {
+        if (c >= 32 && c < 127) {
+          sb.append(c);
+        } else {
+          sb.append(" ");
+        }
+      }
+      this.contact = sb.toString();
+    }
+  }
+  public String getContact() {
+    return this.contact;
+  }
 }
 
diff --git a/src/org/torproject/onionoo/ResourceServlet.java b/src/org/torproject/onionoo/ResourceServlet.java
index 110b123..47d4a93 100644
--- a/src/org/torproject/onionoo/ResourceServlet.java
+++ b/src/org/torproject/onionoo/ResourceServlet.java
@@ -59,7 +59,8 @@ public class ResourceServlet extends HttpServlet {
   private Map<String, String> relayFingerprintSummaryLines = null,
       bridgeFingerprintSummaryLines = null;
   private Map<String, Set<String>> relaysByCountryCode = null,
-      relaysByASNumber = null, relaysByFlag = null;
+      relaysByASNumber = null, relaysByFlag = null,
+      relaysByContact = null;
   private SortedMap<Integer, Set<String>> relaysByFirstSeenDays = null,
       bridgesByFirstSeenDays = null, relaysByLastSeenDays = null,
       bridgesByLastSeenDays = null;
@@ -90,7 +91,8 @@ public class ResourceServlet extends HttpServlet {
       Map<String, Set<String>>
           relaysByCountryCode = new HashMap<String, Set<String>>(),
           relaysByASNumber = new HashMap<String, Set<String>>(),
-          relaysByFlag = new HashMap<String, Set<String>>();
+          relaysByFlag = new HashMap<String, Set<String>>(),
+          relaysByContact = new HashMap<String, Set<String>>();
       SortedMap<Integer, Set<String>>
           relaysByFirstSeenDays = new TreeMap<Integer, Set<String>>(),
           bridgesByFirstSeenDays = new TreeMap<Integer, Set<String>>(),
@@ -182,6 +184,12 @@ public class ResourceServlet extends HttpServlet {
         relaysByLastSeenDays.get(daysSinceLastSeen).add(fingerprint);
         relaysByLastSeenDays.get(daysSinceLastSeen).add(
             hashedFingerprint);
+        String contact = entry.getContact();
+        if (!relaysByContact.containsKey(contact)) {
+          relaysByContact.put(contact, new HashSet<String>());
+        }
+        relaysByContact.get(contact).add(fingerprint);
+        relaysByContact.get(contact).add(hashedFingerprint);
       }
       Collections.sort(orderRelaysByConsensusWeight);
       relaysByConsensusWeight = new ArrayList<String>();
@@ -224,6 +232,7 @@ public class ResourceServlet extends HttpServlet {
       this.relaysByCountryCode = relaysByCountryCode;
       this.relaysByASNumber = relaysByASNumber;
       this.relaysByFlag = relaysByFlag;
+      this.relaysByContact = relaysByContact;
       this.relaysByFirstSeenDays = relaysByFirstSeenDays;
       this.relaysByLastSeenDays = relaysByLastSeenDays;
       this.bridgesByFirstSeenDays = bridgesByFirstSeenDays;
@@ -374,7 +383,7 @@ public class ResourceServlet extends HttpServlet {
      * parameters. */
     Set<String> knownParameters = new HashSet<String>(Arrays.asList((
         "type,running,search,lookup,country,as,flag,first_seen_days,"
-        + "last_seen_days,order,limit,offset").split(",")));
+        + "last_seen_days,contact,order,limit,offset").split(",")));
     for (String parameterKey : parameterMap.keySet()) {
       if (!knownParameters.contains(parameterKey)) {
         response.sendError(HttpServletResponse.SC_BAD_REQUEST);
@@ -485,6 +494,15 @@ public class ResourceServlet extends HttpServlet {
       this.filterNodesByDays(filteredBridges, this.bridgesByLastSeenDays,
           days);
     }
+    if (parameterMap.containsKey("contact")) {
+      String[] contactParts = this.parseContactParameter(
+          parameterMap.get("contact"));
+      if (contactParts == null) {
+        response.sendError(HttpServletResponse.SC_BAD_REQUEST);
+        return;
+      }
+      this.filterByContact(filteredRelays, filteredBridges, contactParts);
+    }
 
     /* Re-order and limit results. */
     List<String> orderedRelays = new ArrayList<String>();
@@ -667,6 +685,15 @@ public class ResourceServlet extends HttpServlet {
     return new int[] { x, y };
   }
 
+  private String[] parseContactParameter(String parameter) {
+    for (char c : parameter.toCharArray()) {
+      if (c < 32 || c >= 127) {
+        return null;
+      }
+    }
+    return parameter.split(" ");
+  }
+
   private void filterByType(Map<String, String> filteredRelays,
       Map<String, String> filteredBridges, boolean relaysRequested) {
     if (relaysRequested) {
@@ -874,6 +901,26 @@ public class ResourceServlet extends HttpServlet {
     }
   }
 
+  private void filterByContact(Map<String, String> filteredRelays,
+      Map<String, String> filteredBridges, String[] contactParts) {
+    Set<String> removeRelays = new HashSet<String>();
+    for (Map.Entry<String, Set<String>> e :
+        this.relaysByContact.entrySet()) {
+      String contact = e.getKey();
+      for (String contactPart : contactParts) {
+        if (contact == null ||
+            !contact.contains(contactPart.toLowerCase())) {
+          removeRelays.addAll(e.getValue());
+          break;
+        }
+      }
+    }
+    for (String fingerprint : removeRelays) {
+      filteredRelays.remove(fingerprint);
+    }
+    filteredBridges.clear();
+  }
+
   private void writeRelays(List<String> relays, PrintWriter pw,
       String resourceType) {
     pw.write("{\"relays_published\":\"" + this.relaysPublishedString
diff --git a/test/org/torproject/onionoo/ResourceServletTest.java b/test/org/torproject/onionoo/ResourceServletTest.java
index 5be2fba..2d131b4 100644
--- a/test/org/torproject/onionoo/ResourceServletTest.java
+++ b/test/org/torproject/onionoo/ResourceServletTest.java
@@ -110,36 +110,41 @@ public class ResourceServletTest {
         Map<String, String[]> parameterMap) {
       this.tempOutDir = tempOutDir;
       this.relays = new TreeMap<String, String>();
-      this.relays.put("000C5F55", "r TorkaZ "
-          + "000C5F55BD4814B917CC474BD537F1A3B33CCE2A "
-          + "62.216.201.221;;62.216.201.222+62.216.201.223 "
-          + "2013-04-19 05:00:00 9001 0 Running,Valid 20 de null -1 "
-          + "reject 1-65535 2013-04-18 05:00:00 2013-04-19 05:00:00 "
-          + "AS8767");
-      this.relays.put("001C13B3", "r Ferrari458 "
-          + "001C13B3A55A71B977CA65EC85539D79C653A3FC "
-          + "68.38.171.200;[2001:4f8:3:2e::51]:9001; "
-          + "2013-04-24 12:00:00 9001 9030 "
-          + "Fast,Named,Running,V2Dir,Valid 1140 us "
-          + "c-68-38-171-200.hsd1.pa.comcast.net 1366805763009 reject "
-          + "1-65535 2013-02-12 16:00:00 2013-02-26 18:00:00 AS7922");
-      this.relays.put("0025C136", "r TimMayTribute "
-          + "0025C136C1F3A9EEFE2AE3F918F03BFA21B5070B 89.69.68.246;; "
-          + "2013-04-22 20:00:00 9001 9030 "
-          + "Fast,Running,Unnamed,V2Dir,Valid 63 a1 null -1 reject "
-          + "1-65535 2013-04-16 18:00:00 2013-04-16 18:00:00 AS6830");
-      this.bridges.put("0000831B", "b ec2bridgercc7f31fe "
-          + "0000831B236DFF73D409AD17B40E2A728A53994F 10.199.7.176;; "
-          + "2013-04-21 18:07:03 443 0 Valid -1 ?? null -1 null null "
-          + "2013-04-20 15:37:04 null null null");
-      this.bridges.put("0002D9BD", "b Unnamed "
-          + "0002D9BDBBC230BD9C78FF502A16E0033EF87E0C 10.0.52.84;; "
-          + "2013-04-20 17:37:04 443 0 Valid -1 ?? null -1 null "
-          + "null 2013-04-14 07:07:05 null null null");
-      this.bridges.put("0010D49C", "b gummy "
-          + "1FEDE50ED8DBA1DD9F9165F78C8131E4A44AB756 10.63.169.98;; "
-          + "2013-04-24 01:07:04 9001 0 Running,Valid -1 ?? null -1 null "
-          + "null 2013-01-16 21:07:04 null null null");
+      this.relays.put("000C5F55", "r\tTorkaZ\t"
+          + "000C5F55BD4814B917CC474BD537F1A3B33CCE2A\t"
+          + "62.216.201.221;;62.216.201.222+62.216.201.223\t"
+          + "2013-04-19\t05:00:00\t9001\t0\tRunning,Valid\t20\tde\tnull\t"
+          + "-1\treject\t1-65535\t2013-04-18\t05:00:00\t"
+          + "2013-04-19\t05:00:00\tAS8767\ttorkaz <klaus dot zufall at "
+          + "gmx dot de> <fb-token:np5_g_83jmf=>");
+      this.relays.put("001C13B3", "r\tFerrari458\t"
+          + "001C13B3A55A71B977CA65EC85539D79C653A3FC\t"
+          + "68.38.171.200;[2001:4f8:3:2e::51]:9001;\t"
+          + "2013-04-24\t12:00:00\t9001\t9030\t"
+          + "Fast,Named,Running,V2Dir,Valid\t1140\tus\t"
+          + "c-68-38-171-200.hsd1.pa.comcast.net\t1366805763009\treject\t"
+          + "1-65535\t2013-02-12\t16:00:00\t2013-02-26\t18:00:00\t"
+          + "AS7922\t");
+      this.relays.put("0025C136", "r\tTimMayTribute\t"
+          + "0025C136C1F3A9EEFE2AE3F918F03BFA21B5070B\t89.69.68.246;;\t"
+          + "2013-04-22\t20:00:00\t9001\t9030\t"
+          + "Fast,Running,Unnamed,V2Dir,Valid\t63\ta1\tnull\t-1\treject\t"
+          + "1-65535\t2013-04-16\t18:00:00\t2013-04-16\t18:00:00\t"
+          + "AS6830\t1024D/51E2A1C7 steven j. murdoch "
+          + "<tor+steven.murdoch at cl.cam.ac.uk> <fb-token:5sr_k_zs2wm=>");
+      this.bridges.put("0000831B", "b\tec2bridgercc7f31fe\t"
+          + "0000831B236DFF73D409AD17B40E2A728A53994F\t10.199.7.176;;\t"
+          + "2013-04-21\t18:07:03\t443\t0\tValid\t-1\t??\tnull\t-1\t"
+          + "null\tnull\t2013-04-20\t15:37:04\tnull\tnull\tnull\tnull");
+      this.bridges.put("0002D9BD", "b\tUnnamed\t"
+          + "0002D9BDBBC230BD9C78FF502A16E0033EF87E0C\t10.0.52.84;;\t"
+          + "2013-04-20\t17:37:04\t443\t0\tValid\t-1\t??\tnull\t-1\t"
+          + "null\tnull\t2013-04-14\t07:07:05\tnull\tnull\tnull\tnull");
+      this.bridges.put("0010D49C", "b\tgummy\t"
+          + "1FEDE50ED8DBA1DD9F9165F78C8131E4A44AB756\t10.63.169.98;;\t"
+          + "2013-04-24\t01:07:04\t9001\t0\tRunning,Valid\t-1\t??\tnull\t"
+          + "-1\tnull\tnull\t2013-01-16\t21:07:04\tnull\tnull\tnull\t"
+          + "null");
       this.request = new TestingHttpServletRequestWrapper(requestURI,
           parameterMap);
       this.response = new TestingHttpServletResponseWrapper();
@@ -987,6 +992,48 @@ public class ResourceServletTest {
   }
 
   @Test()
+  public void testContactSteven() {
+    ResourceServletTestHelper.assertSummaryDocument(this.tempOutDir,
+        "/summary?contact=Steven", 1, null, 0, null);
+  }
+
+  @Test()
+  public void testContactStevenMurdoch() {
+    ResourceServletTestHelper.assertSummaryDocument(this.tempOutDir,
+        "/summary?contact=Steven Murdoch", 1, null, 0, null);
+  }
+
+  @Test()
+  public void testContactMurdochSteven() {
+    ResourceServletTestHelper.assertSummaryDocument(this.tempOutDir,
+        "/summary?contact=Murdoch Steven", 1, null, 0, null);
+  }
+
+  @Test()
+  public void testContactStevenDotMurdoch() {
+    ResourceServletTestHelper.assertSummaryDocument(this.tempOutDir,
+        "/summary?contact=Steven.Murdoch", 1, null, 0, null);
+  }
+
+  @Test()
+  public void testContactFbTokenFive() {
+    ResourceServletTestHelper.assertSummaryDocument(this.tempOutDir,
+        "/summary?contact=<fb-token:5sR_K_zs2wM=>", 1, null, 0, null);
+  }
+
+  @Test()
+  public void testContactFbToken() {
+    ResourceServletTestHelper.assertSummaryDocument(this.tempOutDir,
+        "/summary?contact=<fb-token:", 2, null, 0, null);
+  }
+
+  @Test()
+  public void testContactDash() {
+    ResourceServletTestHelper.assertSummaryDocument(this.tempOutDir,
+        "/summary?contact=-", 2, null, 0, null);
+  }
+
+  @Test()
   public void testOrderConsensusWeightAscending() {
     ResourceServletTestHelper.assertSummaryDocument(this.tempOutDir,
         "/summary?order=consensus_weight", 3,
diff --git a/web/index.html b/web/index.html
index a3bcae8..2655975 100755
--- a/web/index.html
+++ b/web/index.html
@@ -687,6 +687,17 @@ Note that relays and bridges that haven't been running in the past week
 are never included in results, so that setting x to 8 or higher will
 always lead to an empty result set.
 </td></tr>
+<tr><td><b><font color="blue">contact</font></b></td><td>Return only
+relays with the parameter value
+matching (part of) the contact line.
+If the parameter value contains spaces, only relays are returned which
+contain all space-separated parts in their contact line.
+Only printable ASCII characters are permitted in the parameter value,
+some of which need to be percent-encoded (# as %23, % as %25, & as
+%26, + as %2B, and / as %2F).
+Comparisons are case-insensitive.
+<font color="blue">Added on July 19, 2013.</font>
+</td></tr>
 </table>
 <p>Relay and/or bridge documents in the response can be ordered and
 limited by providing further parameters.



More information about the tor-commits mailing list