[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