commit 2aef30a3356bb4a7673e79275c53d4668e493eb7 Author: Karsten Loesing karsten.loesing@gmx.net Date: Tue Dec 11 14:19:52 2012 +0100
Support searching for IPv6 OR addresses in ExoneraTor. --- db/exonerator.sql | 60 ++++-- .../ernie/cron/ExoneraTorDatabaseImporter.java | 104 +++++++--- .../torproject/ernie/web/ExoneraTorServlet.java | 210 +++++++++++++++----- 3 files changed, 280 insertions(+), 94 deletions(-)
diff --git a/db/exonerator.sql b/db/exonerator.sql index b3c7c62..6851e91 100755 --- a/db/exonerator.sql +++ b/db/exonerator.sql @@ -31,7 +31,8 @@ CREATE TABLE consensus ( -- a relay as running at a certain point in time. Only relays with the -- Running flag shall be inserted into this table. If a relay advertises -- more than one IP address, there is a distinct entry for each address in --- this table. +-- this table. If a relay advertises more than one TCP port on the same +-- IP address, there is only a single entry in this table. CREATE TABLE statusentry (
-- The valid-after time of the consensus that contains this entry. @@ -45,11 +46,17 @@ CREATE TABLE statusentry ( -- descriptor published by the relay. descriptor CHARACTER(40) NOT NULL,
- -- The most significant 3 bytes of the relay's onion routing address in - -- hex notation. This column contains the /24 network of the IPv4 or - -- IPv6 address. The purpose is to quickly reduce query results which - -- works surprisingly well. - oraddress24 CHARACTER(6) NOT NULL, + -- The most significant 3 bytes of the relay's onion routing IPv4 + -- address in lower-case hex notation, or null if the relay's onion + -- routing address in this status entry is IPv6. The purpose is to + -- quickly reduce query results for relays in the same /24 network. + oraddress24 CHARACTER(6), + + -- The most significant 6 bytes of the relay's onion routing IPv6 + -- address in lower-case hex notation, or null if the relay's onion + -- routing address in this status entry is IPv4. The purpose is to + -- quickly reduce query results for relays in the same /48 network. + oraddress48 CHARACTER(12),
-- The relay's onion routing address. Can be an IPv4 or an IPv6 -- address. If a relay advertises more than one address, there are @@ -77,6 +84,12 @@ CREATE INDEX statusentry_oraddress_validafterdate CREATE INDEX statusentry_oraddress24_validafterdate ON statusentry (oraddress24, DATE(validafter));
+-- The index on the most significant 6 bytes of the relay's onion routing +-- address and on the valid-after date is used to speed up queries for +-- other relays in the same /48 network. +CREATE INDEX statusentry_oraddress48_validafterdate + ON statusentry (oraddress48, DATE(validafter)); + -- The exitlistentry table stores the results of the active testing, -- DNS-based exit list for exit nodes. An entry in this table means that -- a relay was scanned at a given time and found to be exiting to the @@ -88,10 +101,11 @@ CREATE TABLE exitlistentry ( -- The 40-character lower-case hex string identifying the relay. fingerprint CHARACTER(40) NOT NULL,
- -- The most significant 3 bytes of the relay's exit address in hex - -- notation. This column contains the /24 network of the IPv4 or IPv6 - -- address. The purpose is to quickly reduce query results. - exitaddress24 CHARACTER(6) NOT NULL, + -- The most significant 3 bytes of the relay's exit IPv4 address in + -- lower-case hex notation, or null if the relay's exit address in this + -- entry is IPv6. The purpose is to quickly reduce query results for + -- relays exiting from the same /24 network. + exitaddress24 CHARACTER(6),
-- The IP address that the relay uses for exiting to the Internet. If -- the relay uses more than one IP address, there are multiple entries @@ -163,6 +177,7 @@ CREATE OR REPLACE FUNCTION insert_statusentry ( insert_fingerprint CHARACTER(40), insert_descriptor CHARACTER(40), insert_oraddress24 CHARACTER(6), + insert_oraddress48 CHARACTER(12), insert_oraddress TEXT, insert_rawstatusentry BYTEA) RETURNS INTEGER AS $$ @@ -176,10 +191,10 @@ CREATE OR REPLACE FUNCTION insert_statusentry ( AND oraddress = insert_oraddress::INET) = 0 THEN -- Insert the status entry. INSERT INTO statusentry (validafter, fingerprint, descriptor, - oraddress24, oraddress, rawstatusentry) + oraddress24, oraddress48, oraddress, rawstatusentry) VALUES (insert_validafter, insert_fingerprint, - insert_descriptor, insert_oraddress24, insert_oraddress::INET, - insert_rawstatusentry); + insert_descriptor, insert_oraddress24, insert_oraddress48, + insert_oraddress::INET, insert_rawstatusentry); -- Return 1 for a successfully inserted status entry. RETURN 1; ELSE @@ -305,9 +320,8 @@ CREATE OR REPLACE FUNCTION search_statusentries_by_address_date ( ORDER BY 3, 4, 6; $$ LANGUAGE SQL;
--- Look up all IP adddresses in the /24 network of a given address to --- suggest other addresses the user may be looking for. --- TODO Revisit this function when enabling IPv6. +-- Look up all IPv4 OR and exit addresses in the /24 network of a given +-- address to suggest other addresses the user may be looking for. CREATE OR REPLACE FUNCTION search_addresses_in_same_24 ( select_address24 CHARACTER(6), select_date DATE) @@ -326,3 +340,17 @@ CREATE OR REPLACE FUNCTION search_addresses_in_same_24 ( ORDER BY 1; $$ LANGUAGE SQL;
+-- Look up all IPv6 OR addresses in the /48 network of a given address to +-- suggest other addresses the user may be looking for. +CREATE OR REPLACE FUNCTION search_addresses_in_same_48 ( + select_address48 CHARACTER(12), + select_date DATE) + RETURNS TABLE(address TEXT) AS $$ + SELECT HOST(oraddress) + FROM statusentry + WHERE oraddress48 = $1 + AND DATE(validafter) >= $2 - 1 + AND DATE(validafter) <= $2 + 1 + ORDER BY 1; +$$ LANGUAGE SQL; + diff --git a/src/org/torproject/ernie/cron/ExoneraTorDatabaseImporter.java b/src/org/torproject/ernie/cron/ExoneraTorDatabaseImporter.java old mode 100755 new mode 100644 index c23b4eb..2e6916a --- a/src/org/torproject/ernie/cron/ExoneraTorDatabaseImporter.java +++ b/src/org/torproject/ernie/cron/ExoneraTorDatabaseImporter.java @@ -18,6 +18,7 @@ import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.sql.Timestamp; +import java.sql.Types; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Calendar; @@ -86,7 +87,7 @@ public class ExoneraTorDatabaseImporter { insertDescriptorStatement = connection.prepareCall( "{call insert_descriptor(?, ?)}"); insertStatusentryStatement = connection.prepareCall( - "{call insert_statusentry(?, ?, ?, ?, ?, ?)}"); + "{call insert_statusentry(?, ?, ?, ?, ?, ?, ?)}"); insertConsensusStatement = connection.prepareCall( "{call insert_consensus(?, ?)}"); insertExitlistentryStatement = connection.prepareCall( @@ -325,8 +326,8 @@ public class ExoneraTorDatabaseImporter { try { BufferedReader br = new BufferedReader(new StringReader(new String( bytes, "US-ASCII"))); - String line, fingerprint = null, descriptor = null, - orAddress24 = null, orAddress = null; + String line, fingerprint = null, descriptor = null; + Set<String> orAddresses = new HashSet<String>(); long validAfterMillis = -1L; StringBuilder rawStatusentryBuilder = null; boolean isRunning = false; @@ -353,7 +354,8 @@ public class ExoneraTorDatabaseImporter { byte[] rawStatusentry = rawStatusentryBuilder.toString(). getBytes(); importStatusentry(validAfterMillis, fingerprint, descriptor, - orAddress24, orAddress, rawStatusentry); + orAddresses, rawStatusentry); + orAddresses = new HashSet<String>(); } if (line.equals("directory-footer")) { return; @@ -369,26 +371,17 @@ public class ExoneraTorDatabaseImporter { + "=")).toLowerCase(); descriptor = Hex.encodeHexString(Base64.decodeBase64(parts[3] + "=")).toLowerCase(); - orAddress = parts[6]; - /* TODO Extend the following code for IPv6 once Tor supports - * it. */ - String[] orAddressParts = orAddress.split("\."); - byte[] orAddress24Bytes = new byte[3]; - orAddress24Bytes[0] = (byte) Integer.parseInt( - orAddressParts[0]); - orAddress24Bytes[1] = (byte) Integer.parseInt( - orAddressParts[1]); - orAddress24Bytes[2] = (byte) Integer.parseInt( - orAddressParts[2]); - orAddress24 = Hex.encodeHexString(orAddress24Bytes); + orAddresses.add(parts[6]); + } else if (line.startsWith("a ")) { + rawStatusentryBuilder.append(line + "\n"); + orAddresses.add(line.substring("a ".length(), + line.lastIndexOf(":"))); } else if (line.startsWith("s ") || line.equals("s")) { rawStatusentryBuilder.append(line + "\n"); isRunning = line.contains(" Running"); } else if (rawStatusentryBuilder != null) { rawStatusentryBuilder.append(line + "\n"); } - /* TODO Extend this code to parse additional addresses once that's - * implemented in Tor. */ } } catch (IOException e) { System.out.println("Could not parse consensus. Skipping."); @@ -400,20 +393,71 @@ public class ExoneraTorDatabaseImporter { private static Calendar calendarUTC = Calendar.getInstance( TimeZone.getTimeZone("UTC"));
- /* Import a single status entry into the database. */ + /* Import a status entry with one or more OR addresses into the + * database. */ private static void importStatusentry(long validAfterMillis, - String fingerprint, String descriptor, String orAddress24, - String orAddress, byte[] rawStatusentry) { + String fingerprint, String descriptor, Set<String> orAddresses, + byte[] rawStatusentry) { try { - insertStatusentryStatement.clearParameters(); - insertStatusentryStatement.setTimestamp(1, - new Timestamp(validAfterMillis), calendarUTC); - insertStatusentryStatement.setString(2, fingerprint); - insertStatusentryStatement.setString(3, descriptor); - insertStatusentryStatement.setString(4, orAddress24); - insertStatusentryStatement.setString(5, orAddress); - insertStatusentryStatement.setBytes(6, rawStatusentry); - insertStatusentryStatement.execute(); + for (String orAddress : orAddresses) { + insertStatusentryStatement.clearParameters(); + insertStatusentryStatement.setTimestamp(1, + new Timestamp(validAfterMillis), calendarUTC); + insertStatusentryStatement.setString(2, fingerprint); + insertStatusentryStatement.setString(3, descriptor); + if (!orAddress.contains(":")) { + String[] addressParts = orAddress.split("\."); + byte[] address24Bytes = new byte[3]; + address24Bytes[0] = (byte) Integer.parseInt(addressParts[0]); + address24Bytes[1] = (byte) Integer.parseInt(addressParts[1]); + address24Bytes[2] = (byte) Integer.parseInt(addressParts[2]); + String orAddress24 = Hex.encodeHexString(address24Bytes); + insertStatusentryStatement.setString(4, orAddress24); + insertStatusentryStatement.setNull(5, Types.VARCHAR); + insertStatusentryStatement.setString(6, orAddress); + } else { + StringBuilder addressHex = new StringBuilder(); + int start = orAddress.startsWith("[::") ? 2 : 1; + int end = orAddress.length() + - (orAddress.endsWith("::]") ? 2 : 1); + String[] parts = orAddress.substring(start, end).split(":", -1); + for (int i = 0; i < parts.length; i++) { + String part = parts[i]; + if (part.length() == 0) { + addressHex.append("x"); + } else if (part.length() <= 4) { + addressHex.append(String.format("%4s", part)); + } else { + addressHex = null; + break; + } + } + String orAddress48 = null; + if (addressHex != null) { + String addressHexString = addressHex.toString(); + addressHexString = addressHexString.replaceFirst("x", + String.format("%" + (33 - addressHexString.length()) + + "s", "0")); + if (!addressHexString.contains("x") && + addressHexString.length() == 32) { + orAddress48 = addressHexString.replaceAll(" ", "0"). + toLowerCase().substring(0, 12); + } + } + if (orAddress48 != null) { + insertStatusentryStatement.setNull(4, Types.VARCHAR); + insertStatusentryStatement.setString(5, orAddress48); + insertStatusentryStatement.setString(6, + orAddress.replaceAll("[\[\]]", "")); + } else { + System.err.println("Could not import status entry with IPv6 " + + "address '" + orAddress + "'. Exiting."); + System.exit(1); + } + } + insertStatusentryStatement.setBytes(7, rawStatusentry); + insertStatusentryStatement.execute(); + } } catch (SQLException e) { System.out.println("Could not import status entry. Exiting."); System.exit(1); diff --git a/src/org/torproject/ernie/web/ExoneraTorServlet.java b/src/org/torproject/ernie/web/ExoneraTorServlet.java index d353c7b..687cf37 100644 --- a/src/org/torproject/ernie/web/ExoneraTorServlet.java +++ b/src/org/torproject/ernie/web/ExoneraTorServlet.java @@ -13,7 +13,9 @@ import java.sql.SQLException; import java.sql.Statement; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.SortedMap; import java.util.SortedSet; @@ -229,22 +231,59 @@ public class ExoneraTorServlet extends HttpServlet { + "on this IP address?</h3>");
/* Parse IP parameter. */ - /* TODO Extend the parsing code to accept IPv6 addresses, too. */ - Pattern ipAddressPattern = Pattern.compile( + Pattern ipv4AddressPattern = Pattern.compile( "^([01]?\d\d?|2[0-4]\d|25[0-5])\." + "([01]?\d\d?|2[0-4]\d|25[0-5])\." + "([01]?\d\d?|2[0-4]\d|25[0-5])\." + "([01]?\d\d?|2[0-4]\d|25[0-5])$"); + Pattern ipv6AddressPattern = Pattern.compile( + "^\[?[0-9a-fA-F:]{3,39}\]?$"); String ipParameter = request.getParameter("ip"); String relayIP = "", ipWarning = ""; if (ipParameter != null && ipParameter.length() > 0) { - Matcher ipParameterMatcher = ipAddressPattern.matcher(ipParameter); - if (ipParameterMatcher.matches()) { + if (ipv4AddressPattern.matcher(ipParameter).matches()) { String[] ipParts = ipParameter.split("\."); relayIP = Integer.parseInt(ipParts[0]) + "." + Integer.parseInt(ipParts[1]) + "." + Integer.parseInt(ipParts[2]) + "." + Integer.parseInt(ipParts[3]); + } else if (ipv6AddressPattern.matcher(ipParameter).matches()) { + if (ipParameter.startsWith("[") && ipParameter.endsWith("]")) { + ipParameter = ipParameter.substring(1, + ipParameter.length() - 1); + } + StringBuilder addressHex = new StringBuilder(); + int start = ipParameter.startsWith("::") ? 1 : 0; + int end = ipParameter.length() + - (ipParameter.endsWith("::") ? 1 : 0); + String[] parts = ipParameter.substring(start, end).split(":", -1); + for (int i = 0; i < parts.length; i++) { + String part = parts[i]; + if (part.length() == 0) { + addressHex.append("x"); + } else if (part.length() <= 4) { + addressHex.append(String.format("%4s", part)); + } else { + addressHex = null; + break; + } + } + if (addressHex != null) { + String addressHexString = addressHex.toString(); + addressHexString = addressHexString.replaceFirst("x", + String.format("%" + (33 - addressHexString.length()) + "s", + "0")); + if (!addressHexString.contains("x") && + addressHexString.length() == 32) { + relayIP = ipParameter.toLowerCase(); + } + } + if (relayIP.length() < 1) { + ipWarning = """ + (ipParameter.length() > 40 ? + StringEscapeUtils.escapeHtml(ipParameter.substring(0, 40)) + + "[...]" : StringEscapeUtils.escapeHtml(ipParameter)) + + "" is not a valid IP address."; + } } else { ipWarning = """ + (ipParameter.length() > 20 ? StringEscapeUtils.escapeHtml(ipParameter.substring(0, 20)) @@ -309,7 +348,7 @@ public class ExoneraTorServlet extends HttpServlet { String targetAddrWarning = ""; if (targetAddrParameter != null && targetAddrParameter.length() > 0) { Matcher targetAddrParameterMatcher = - ipAddressPattern.matcher(targetAddrParameter); + ipv4AddressPattern.matcher(targetAddrParameter); if (targetAddrParameterMatcher.matches()) { String[] targetAddrParts = targetAddrParameter.split("\."); targetIP = Integer.parseInt(targetAddrParts[0]) + "." @@ -370,19 +409,21 @@ public class ExoneraTorServlet extends HttpServlet { + " <tr>\n" + " <td align="right">IP address in question:" + "</td>\n" - + " <td><input type="text" name="ip"" + + " <td><input type="text" name="ip" size="30"" + (relayIP.length() > 0 ? " value="" + relayIP + """ : "") + ">" + (ipWarning.length() > 0 ? "<br><font color="red">" + ipWarning + "</font>" : "") + "</td>\n" - + " <td><i>(Ex.: 1.2.3.4)</i></td>\n" + + " <td><i>(Ex.: 86.59.21.38 or " + + "2001:858:2:2:aabb:0:563b:1526)</i></td>\n" + " </tr>\n" + " <tr>\n" + " <td align="right">Date or timestamp, in " + "UTC:</td>\n" + " <td><input type="text" name="timestamp"" + + " size="30"" + (timestampStr.length() > 0 ? " value="" + timestampStr + """ : "") + ">" @@ -523,27 +564,35 @@ public class ExoneraTorServlet extends HttpServlet { } relevantDescriptors.get(descriptor).add(validafter); String fingerprint = rs.getString(4); - boolean orAddressMatches = rs.getString(5).equals(relayIP); String exitaddress = rs.getString(6); - String rLine = new String(rawstatusentry); - rLine = rLine.substring(0, rLine.indexOf("\n")); - String[] parts = rLine.split(" "); - String htmlString = "r " + parts[1] + " " + parts[2] + " " - + "<a href="serverdesc?desc-id=" + descriptor + "" " - + "target="_blank">" + parts[3] + "</a> " + parts[4] - + " " + parts[5] + " " + (orAddressMatches ? "<b>" : "") - + parts[6] + (orAddressMatches ? "</b>" : "") + " " + parts[7] - + " " + parts[8] + "\n"; + StringBuilder html = new StringBuilder(); + for (String line : new String(rawstatusentry).split("\n")) { + if (line.startsWith("r ")) { + String[] parts = line.split(" "); + boolean orAddressMatches = parts[6].equals(relayIP); + html.append("r " + parts[1] + " " + parts[2] + " " + + "<a href="serverdesc?desc-id=" + descriptor + "" " + + "target="_blank">" + parts[3] + "</a> " + parts[4] + + " " + parts[5] + " " + (orAddressMatches ? "<b>" : "") + + parts[6] + (orAddressMatches ? "</b>" : "") + " " + + parts[7] + " " + parts[8] + "\n"); + } else if (line.startsWith("a ") && + line.toLowerCase().contains(relayIP)) { + String address = line.substring("a ".length(), + line.lastIndexOf(":")); + String port = line.substring(line.lastIndexOf(":")); + html.append("a <b>" + address + "</b>" + port + "\n"); + } + } if (exitaddress != null && exitaddress.length() > 0) { long scanned = rs.getTimestamp(7).getTime(); - htmlString += " [ExitAddress <b>" + exitaddress - + "</b> " + validAfterTimeFormat.format(scanned) - + "]\n"; + html.append(" [ExitAddress <b>" + exitaddress + + "</b> " + validAfterTimeFormat.format(scanned) + "]\n"); } if (!statusEntries.containsKey(validafter)) { statusEntries.put(validafter, new TreeMap<String, String>()); } - statusEntries.get(validafter).put(fingerprint, htmlString); + statusEntries.get(validafter).put(fingerprint, html.toString()); } rs.close(); cs.close(); @@ -584,35 +633,93 @@ public class ExoneraTorServlet extends HttpServlet { dateFormat.format(timestampTooOld), dateFormat.format(timestampTooNew)); /* Run another query to find out if there are relays running on - * other IP addresses in the same /24 network and tell the user - * about it. */ - SortedSet<String> addressesInSameNetwork = new TreeSet<String>(); - String[] relayIPParts = relayIP.split("\."); - byte[] address24Bytes = new byte[3]; - address24Bytes[0] = (byte) Integer.parseInt(relayIPParts[0]); - address24Bytes[1] = (byte) Integer.parseInt(relayIPParts[1]); - address24Bytes[2] = (byte) Integer.parseInt(relayIPParts[2]); - String address24 = Hex.encodeHexString(address24Bytes); - try { - CallableStatement cs = conn.prepareCall( - "{call search_addresses_in_same_24 (?, ?)}"); - cs.setString(1, address24); - cs.setDate(2, new java.sql.Date(timestamp)); - ResultSet rs = cs.executeQuery(); - while (rs.next()) { - String address = rs.getString(1); - addressesInSameNetwork.add(address); + * other IP addresses in the same /24 or /48 network and tell the + * user about it. */ + List<String> addressesInSameNetwork = new ArrayList<String>(); + if (!relayIP.contains(":")) { + String[] relayIPParts = relayIP.split("\."); + byte[] address24Bytes = new byte[3]; + address24Bytes[0] = (byte) Integer.parseInt(relayIPParts[0]); + address24Bytes[1] = (byte) Integer.parseInt(relayIPParts[1]); + address24Bytes[2] = (byte) Integer.parseInt(relayIPParts[2]); + String address24 = Hex.encodeHexString(address24Bytes); + try { + CallableStatement cs = conn.prepareCall( + "{call search_addresses_in_same_24 (?, ?)}"); + cs.setString(1, address24); + cs.setDate(2, new java.sql.Date(timestamp)); + ResultSet rs = cs.executeQuery(); + while (rs.next()) { + String address = rs.getString(1); + if (!addressesInSameNetwork.contains(address)) { + addressesInSameNetwork.add(address); + } + } + rs.close(); + cs.close(); + } catch (SQLException e) { + /* No other addresses in the same /24 found. */ + } + } else { + StringBuilder addressHex = new StringBuilder(); + int start = relayIP.startsWith("::") ? 1 : 0; + int end = relayIP.length() - (relayIP.endsWith("::") ? 1 : 0); + String[] parts = relayIP.substring(start, end).split(":", -1); + for (int i = 0; i < parts.length; i++) { + String part = parts[i]; + if (part.length() == 0) { + addressHex.append("x"); + } else if (part.length() <= 4) { + addressHex.append(String.format("%4s", part)); + } else { + addressHex = null; + break; + } + } + String address48 = null; + if (addressHex != null) { + String addressHexString = addressHex.toString(); + addressHexString = addressHexString.replaceFirst("x", + String.format("%" + (33 - addressHexString.length()) + + "s", "0")); + if (!addressHexString.contains("x") && + addressHexString.length() == 32) { + address48 = addressHexString.replaceAll(" ", "0"). + toLowerCase().substring(0, 12); + } + } + if (address48 != null) { + try { + CallableStatement cs = conn.prepareCall( + "{call search_addresses_in_same_48 (?, ?)}"); + cs.setString(1, address48); + cs.setDate(2, new java.sql.Date(timestamp)); + ResultSet rs = cs.executeQuery(); + while (rs.next()) { + String address = rs.getString(1); + if (!addressesInSameNetwork.contains(address)) { + addressesInSameNetwork.add(address); + } + } + rs.close(); + cs.close(); + } catch (SQLException e) { + /* No other addresses in the same /48 found. */ + } } - rs.close(); - cs.close(); - } catch (SQLException e) { - /* No other addresses in the same /24 found. */ } if (!addressesInSameNetwork.isEmpty()) { - out.print(" <p>The following other IP addresses of Tor " - + "relays in the same /24 network were found in relay and/or " - + "exit lists around the time that could be related to IP " - + "address " + relayIP + ":</p>\n"); + if (!relayIP.contains(":")) { + out.print(" <p>The following other IP addresses of Tor " + + "relays in the same /24 network were found in relay " + + "and/or exit lists around the time that could be related " + + "to IP address " + relayIP + ":</p>\n"); + } else { + out.print(" <p>The following other IP addresses of Tor " + + "relays in the same /48 network were found in relay " + + "lists around the time that could be related to IP " + + "address " + relayIP + ":</p>\n"); + } out.print(" <ul>\n"); for (String s : addressesInSameNetwork) { out.print(" <li>" + s + "</li>\n"); @@ -670,7 +777,8 @@ public class ExoneraTorServlet extends HttpServlet { if (timestampIsDate) { out.print("a relay list published on " + timestampStr); } else { - out.print("the most recent relay list preceding " + timestampStr); + out.print("the most recent relay list preceding " + + timestampStr); } out.print(". A possible reason for the relay being missing in a " + "relay list might be that some of the directory " @@ -720,6 +828,12 @@ public class ExoneraTorServlet extends HttpServlet { } }
+ /* Looking up targets for IPv6 is not supported yet. */ + if (relayIP.contains(":")) { + writeFooter(out); + return; + } + /* Second part: target */ out.println("<br><a name="exit"></a><h3>Was this relay configured " + "to permit exiting to a given target?</h3>");
tor-commits@lists.torproject.org