[tor-commits] [metrics-web/master] Support searching for IPv6 OR addresses in ExoneraTor.

karsten at torproject.org karsten at torproject.org
Tue Dec 11 17:07:34 UTC 2012


commit 2aef30a3356bb4a7673e79275c53d4668e493eb7
Author: Karsten Loesing <karsten.loesing at 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>");



More information about the tor-commits mailing list