[tor-commits] [exonerator/master] Provide and use query results via query.json.

karsten at torproject.org karsten at torproject.org
Fri Sep 15 12:18:16 UTC 2017


commit 34e793e7c318d5bd3836c5ae2c22abc4becd8ee7
Author: Karsten Loesing <karsten.loesing at gmx.net>
Date:   Wed Aug 16 10:31:24 2017 +0200

    Provide and use query results via query.json.
    
    This prepares splitting up ExoneraTor into front-end and back-end.
    
    Note that some code duplication between ExoneraTorServlet and
    QueryServlet was deemed acceptable, because ExoneraTorServlet will be
    moved to metrics-web in the medium term anyway.
---
 build.xml                                          |   1 +
 .../torproject/exonerator/ExoneraTorServlet.java   | 383 ++--------------
 .../org/torproject/exonerator/QueryResponse.java   |   3 +
 .../org/torproject/exonerator/QueryServlet.java    | 502 +++++++++++++++++++++
 src/main/webapp/web.xml                            |  11 +
 5 files changed, 554 insertions(+), 346 deletions(-)

diff --git a/build.xml b/build.xml
index 359ae20..eb39e7e 100644
--- a/build.xml
+++ b/build.xml
@@ -108,6 +108,7 @@
       <lib dir="${libs}">
         <include name="commons-codec-1.10.jar"/>
         <include name="commons-lang-2.6.jar"/>
+        <include name="gson-2.4.jar" />
         <include name="postgresql-9.4.1212.jar"/>
       </lib>
       <classes dir="${classes}"/>
diff --git a/src/main/java/org/torproject/exonerator/ExoneraTorServlet.java b/src/main/java/org/torproject/exonerator/ExoneraTorServlet.java
index b7d0497..1fb7073 100644
--- a/src/main/java/org/torproject/exonerator/ExoneraTorServlet.java
+++ b/src/main/java/org/torproject/exonerator/ExoneraTorServlet.java
@@ -3,71 +3,60 @@
 
 package org.torproject.exonerator;
 
-import org.apache.commons.codec.binary.Hex;
+import com.google.gson.Gson;
 import org.apache.commons.lang.StringEscapeUtils;
 
 import java.io.IOException;
+import java.io.InputStreamReader;
 import java.io.PrintWriter;
-import java.sql.CallableStatement;
-import java.sql.Connection;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
+import java.net.MalformedURLException;
+import java.net.URL;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Calendar;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.ResourceBundle;
 import java.util.SortedMap;
-import java.util.SortedSet;
 import java.util.TimeZone;
 import java.util.TreeMap;
-import java.util.TreeSet;
-import java.util.logging.Level;
 import java.util.logging.Logger;
 import java.util.regex.Pattern;
 
-import javax.naming.Context;
-import javax.naming.InitialContext;
-import javax.naming.NamingException;
+import javax.servlet.ServletConfig;
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import javax.sql.DataSource;
 
 public class ExoneraTorServlet extends HttpServlet {
 
   private static final long serialVersionUID = 1370088989739567509L;
 
-  private DataSource ds;
-
   private Logger logger;
 
+  private String exoneraTorHost = "https://exonerator.torproject.org";
+
+  /* Don't accept query responses with versions lower than this. */
+  private static final String firstRecognizedVersion = "1.0";
+
+  /* Don't accept query responses with this version or higher. */
+  private static final String firstUnrecognizedVersion = "2.0";
+
   private List<String> availableLanguages =
       Arrays.asList("de", "en", "fr", "ro", "sv");
 
   private SortedMap<String, String> availableLanguageNames;
 
   @Override
-  public void init() {
+  public void init(ServletConfig config) throws ServletException {
+    super.init(config);
 
     /* Initialize logger. */
     this.logger = Logger.getLogger(ExoneraTorServlet.class.toString());
 
-    /* Look up data source. */
-    try {
-      Context cxt = new InitialContext();
-      this.ds = (DataSource) cxt.lookup("java:comp/env/jdbc/exonerator");
-      this.logger.info("Successfully looked up data source.");
-    } catch (NamingException e) {
-      this.logger.log(Level.WARNING, "Could not look up data source", e);
-    }
-
     this.availableLanguageNames = new TreeMap<>();
     for (String locale : this.availableLanguages) {
       ResourceBundle rb = ResourceBundle.getBundle("ExoneraTor",
@@ -298,50 +287,6 @@ public class ExoneraTorServlet extends HttpServlet {
     return relayIp;
   }
 
-  private String convertIpV4ToHex(String relayIp) {
-    String[] relayIpParts = relayIp.split("\\.");
-    byte[] address24Bytes = new byte[4];
-    for (int i = 0; i < address24Bytes.length; i++) {
-      address24Bytes[i] = (byte) Integer.parseInt(relayIpParts[i]);
-    }
-    String address24 = Hex.encodeHexString(address24Bytes);
-    return address24;
-  }
-
-  private String convertIpV6ToHex(String relayIp) {
-    if (relayIp.startsWith("[") && relayIp.endsWith("]")) {
-      relayIp = relayIp.substring(1, relayIp.length() - 1);
-    }
-    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();
-      }
-    }
-    return address48;
-  }
-
   private String parseTimestampParameter(
       String passedTimestampParameter) {
     String timestampStr = "";
@@ -361,289 +306,35 @@ public class ExoneraTorServlet extends HttpServlet {
     return timestampStr;
   }
 
-  /* Helper methods for querying the database. */
+  /* Helper method for fetching a query response via URL. */
 
   private QueryResponse queryDatabase(String relayIp, String timestampStr) {
-
-    QueryResponse response = null;
-    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
-    dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
-    SimpleDateFormat validAfterTimeFormat = new SimpleDateFormat(
-        "yyyy-MM-dd HH:mm:ss");
-    validAfterTimeFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
-    long timestamp = 0L;
-    if (timestampStr != null && timestampStr.length() > 0) {
-      try {
-        timestamp = dateFormat.parse(timestampStr).getTime();
-      } catch (ParseException e) {
-        /* Already checked in parseTimestamp(). */
-      }
-    }
-
-    /* Only query the database if we received valid user input. */
-    if (!"".equals(relayIp) && !"".equals(timestampStr)) {
-
-      /* Open a database connection that we'll use to handle the whole
-       * request. */
-      long requestedConnection = System.currentTimeMillis();
-      Connection conn = this.connectToDatabase();
-      if (null != conn) {
-
-        response = new QueryResponse();
-        response.queryAddress = relayIp;
-        response.queryDate = timestampStr;
-
-        /* Look up first and last date in the database. */
-        long[] firstAndLastDates = this.queryFirstAndLastDatesFromDatabase(
-            conn);
-        if (null != firstAndLastDates) {
-          response.firstDateInDatabase = dateFormat.format(
-              firstAndLastDates[0]);
-          response.lastDateInDatabase = dateFormat.format(firstAndLastDates[1]);
-
-          /* Consider all consensuses published on or within a day of the given
-           * date. */
-          long timestampFrom = timestamp - 24L * 60L * 60L * 1000L;
-          long timestampTo = timestamp + 2 * 24L * 60L * 60L * 1000L - 1L;
-          String fromValidAfter = validAfterTimeFormat.format(timestampFrom);
-          String toValidAfter = validAfterTimeFormat.format(timestampTo);
-          SortedSet<Long> relevantConsensuses =
-              this.queryKnownConsensusValidAfterTimes(conn, fromValidAfter,
-              toValidAfter);
-          if (null != relevantConsensuses && !relevantConsensuses.isEmpty()) {
-            response.relevantStatuses = true;
-
-            /* Search for status entries with the given IP address as onion
-             * routing address, plus status entries of relays having an exit
-             * list entry with the given IP address as exit address. */
-            List<QueryResponse.Match> matches = this.queryStatusEntries(conn,
-                relayIp, timestamp, validAfterTimeFormat);
-            if (!matches.isEmpty()) {
-              response.matches = matches.toArray(new QueryResponse.Match[0]);
-
-            /* If we didn't find anything, run another query to find out if
-             * there are relays running on other IP addresses in the same /24 or
-             * /48 network and tell the user about it. */
-            } else {
-              if (!relayIp.contains(":")) {
-                String address24 = this.convertIpV4ToHex(relayIp)
-                    .substring(0, 6);
-                if (address24 != null) {
-                  response.nearbyAddresses = this.queryAddressesInSame24(conn,
-                      address24, timestamp).toArray(new String[0]);
-                }
-              } else {
-                String address48 = this.convertIpV6ToHex(relayIp)
-                    .substring(0, 12);
-                if (address48 != null) {
-                  response.nearbyAddresses = this.queryAddressesInSame48(conn,
-                      address48, timestamp).toArray(new String[0]);
-                }
-              }
-            }
-          }
-        }
-
-        /* Close the database connection. */
-        this.closeDatabaseConnection(conn, requestedConnection);
+    QueryResponse response;
+    try (InputStreamReader isr = new InputStreamReader(new URL(
+        this.exoneraTorHost + "/query.json?ip=" + relayIp + "&timestamp="
+        + timestampStr).openStream())) {
+      Gson gson = new Gson();
+      response = gson.fromJson(isr, QueryResponse.class);
+      if (null == response || null == response.version) {
+        logger.warning("Response is either empty or does not contain "
+            + "version information.");
+        response = null;
+      } else if (response.version.compareTo(firstRecognizedVersion) < 0
+          || response.version.compareTo(firstUnrecognizedVersion) >= 0) {
+        logger.warning("Response has either an older or a newer version ("
+            + response.version + ") than we can handle ("
+            + firstRecognizedVersion + " <= x < " + firstUnrecognizedVersion
+            + ").");
+        response = null;
       }
+    } catch (IOException | RuntimeException e) {
+      /* This could be anything, but the effect is that we don't have a query
+       * response to process further. */
+      response = null;
     }
     return response;
   }
 
-  private Connection connectToDatabase() {
-    Connection conn = null;
-    try {
-      conn = this.ds.getConnection();
-    } catch (SQLException e) {
-      this.logger.log(Level.WARNING, "Couldn't connect: " + e.getMessage(), e);
-    }
-    return conn;
-  }
-
-  private long[] queryFirstAndLastDatesFromDatabase(Connection conn) {
-    long[] firstAndLastDates = null;
-    try {
-      Statement statement = conn.createStatement();
-      String query = "SELECT DATE(MIN(validafter)) AS first, "
-          + "DATE(MAX(validafter)) AS last FROM statusentry";
-      ResultSet rs = statement.executeQuery(query);
-      if (rs.next()) {
-        Calendar utcCalendar = Calendar.getInstance(
-            TimeZone.getTimeZone("UTC"));
-        firstAndLastDates = new long[] {
-            rs.getTimestamp(1, utcCalendar).getTime(),
-            rs.getTimestamp(2, utcCalendar).getTime()
-        };
-      }
-      rs.close();
-      statement.close();
-    } catch (SQLException e) {
-      /* Looks like we don't have any consensuses. */
-      firstAndLastDates = null;
-    }
-    return firstAndLastDates;
-  }
-
-  private SortedSet<Long> queryKnownConsensusValidAfterTimes(
-      Connection conn, String fromValidAfter, String toValidAfter) {
-    SortedSet<Long> relevantConsensuses = new TreeSet<>();
-    try {
-      Statement statement = conn.createStatement();
-      String query = "SELECT DISTINCT validafter FROM statusentry "
-          + "WHERE validafter >= '" + fromValidAfter
-          + "' AND validafter <= '" + toValidAfter + "'";
-      ResultSet rs = statement.executeQuery(query);
-      while (rs.next()) {
-        long consensusTime = rs.getTimestamp(1).getTime();
-        relevantConsensuses.add(consensusTime);
-      }
-      rs.close();
-      statement.close();
-    } catch (SQLException e) {
-      /* Looks like we don't have any consensuses in the requested
-       * interval. */
-      relevantConsensuses = null;
-    }
-    return relevantConsensuses;
-  }
-
-  private List<QueryResponse.Match> queryStatusEntries(Connection conn,
-      String relayIp, long timestamp,
-      SimpleDateFormat validAfterTimeFormat) {
-    List<QueryResponse.Match> matches = new ArrayList<>();
-    String addressHex = !relayIp.contains(":")
-        ? this.convertIpV4ToHex(relayIp) : this.convertIpV6ToHex(relayIp);
-    if (addressHex == null) {
-      return null;
-    }
-    String address24Or48Hex = !relayIp.contains(":")
-        ? addressHex.substring(0, 6) : addressHex.substring(0, 12);
-    try {
-      CallableStatement cs;
-      if (!relayIp.contains(":")) {
-        cs = conn.prepareCall("{call search_by_address24_date(?, ?)}");
-      } else {
-        cs = conn.prepareCall("{call search_by_address48_date(?, ?)}");
-      }
-      cs.setString(1, address24Or48Hex);
-      Calendar utcCalendar = Calendar.getInstance(
-          TimeZone.getTimeZone("UTC"));
-      cs.setDate(2, new java.sql.Date(timestamp), utcCalendar);
-      ResultSet rs = cs.executeQuery();
-      while (rs.next()) {
-        byte[] rawstatusentry = rs.getBytes(1);
-        SortedSet<String> addresses = new TreeSet<>();
-        SortedSet<String> addressesHex = new TreeSet<>();
-        String nickname = null;
-        Boolean exit = null;
-        for (String line : new String(rawstatusentry).split("\n")) {
-          if (line.startsWith("r ")) {
-            String[] parts = line.split(" ");
-            nickname = parts[1];
-            addresses.add(parts[6]);
-            addressesHex.add(this.convertIpV4ToHex(parts[6]));
-          } else if (line.startsWith("a ")) {
-            String address = line.substring("a ".length(),
-                line.lastIndexOf(":"));
-            addresses.add(address);
-            String orAddressHex = !address.contains(":")
-                ? this.convertIpV4ToHex(address)
-                : this.convertIpV6ToHex(address);
-            addressesHex.add(orAddressHex);
-          } else if (line.startsWith("p ")) {
-            exit = !line.equals("p reject 1-65535");
-          }
-        }
-        String exitaddress = rs.getString(4);
-        if (exitaddress != null && exitaddress.length() > 0) {
-          addresses.add(exitaddress);
-          addressesHex.add(this.convertIpV4ToHex(exitaddress));
-        }
-        if (!addressesHex.contains(addressHex)) {
-          continue;
-        }
-        long validafter = rs.getTimestamp(2, utcCalendar).getTime();
-        String validAfterString = validAfterTimeFormat.format(validafter);
-        String fingerprint = rs.getString(3).toUpperCase();
-        QueryResponse.Match match = new QueryResponse.Match();
-        match.timestamp = validAfterString;
-        match.addresses = addresses.toArray(new String[0]);
-        match.fingerprint = fingerprint;
-        match.nickname = nickname;
-        match.exit = exit;
-        matches.add(match);
-      }
-      rs.close();
-      cs.close();
-    } catch (SQLException e) {
-      /* Nothing found. */
-      matches.clear();
-    }
-    return matches;
-  }
-
-  private List<String> queryAddressesInSame24(Connection conn,
-      String address24, long timestamp) {
-    List<String> addressesInSameNetwork = new ArrayList<>();
-    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. */
-      addressesInSameNetwork = null;
-    }
-    return addressesInSameNetwork;
-  }
-
-  private List<String> queryAddressesInSame48(Connection conn,
-      String address48, long timestamp) {
-    List<String> addressesInSameNetwork = new ArrayList<>();
-    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. */
-      addressesInSameNetwork = null;
-    }
-    return addressesInSameNetwork;
-  }
-
-  private void closeDatabaseConnection(Connection conn,
-      long requestedConnection) {
-    try {
-      conn.close();
-      this.logger.info("Returned a database connection to the pool "
-          + "after " + (System.currentTimeMillis()
-          - requestedConnection) + " millis.");
-    } catch (SQLException e) {
-      this.logger.log(Level.WARNING, "Couldn't close: " + e.getMessage(), e);
-    }
-    return;
-  }
-
   /* Helper methods for writing the response. */
 
   private void writeHeader(PrintWriter out, ResourceBundle rb, String langStr)
diff --git a/src/main/java/org/torproject/exonerator/QueryResponse.java b/src/main/java/org/torproject/exonerator/QueryResponse.java
index d86efb0..45dd017 100644
--- a/src/main/java/org/torproject/exonerator/QueryResponse.java
+++ b/src/main/java/org/torproject/exonerator/QueryResponse.java
@@ -8,6 +8,9 @@ import com.google.gson.annotations.SerializedName;
 /** Query response from the ExoneraTor database. */
 public class QueryResponse {
 
+  /** Version of this response format. */
+  String version = "1.0";
+
   /** Query IP address passed in the request; never <code>null</code>. */
   @SerializedName("query_address")
   String queryAddress;
diff --git a/src/main/java/org/torproject/exonerator/QueryServlet.java b/src/main/java/org/torproject/exonerator/QueryServlet.java
new file mode 100644
index 0000000..873a53a
--- /dev/null
+++ b/src/main/java/org/torproject/exonerator/QueryServlet.java
@@ -0,0 +1,502 @@
+/* Copyright 2017 The Tor Project
+ * See LICENSE for licensing information */
+
+package org.torproject.exonerator;
+
+import com.google.gson.Gson;
+import org.apache.commons.codec.binary.Hex;
+
+import java.io.IOException;
+import java.sql.CallableStatement;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.List;
+import java.util.SortedSet;
+import java.util.TimeZone;
+import java.util.TreeSet;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+
+import javax.naming.Context;
+import javax.naming.InitialContext;
+import javax.naming.NamingException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.sql.DataSource;
+
+public class QueryServlet extends HttpServlet {
+
+  private static final long serialVersionUID = 7109011659099295183L;
+
+  private Logger logger;
+
+  private DataSource ds;
+
+  @Override
+  public void init() {
+
+    /* Initialize logger. */
+    this.logger = Logger.getLogger(QueryServlet.class.toString());
+
+    /* Look up data source. */
+    try {
+      Context cxt = new InitialContext();
+      this.ds = (DataSource) cxt.lookup("java:comp/env/jdbc/exonerator");
+      this.logger.info("Successfully looked up data source.");
+    } catch (NamingException e) {
+      this.logger.log(Level.WARNING, "Could not look up data source", e);
+    }
+  }
+
+  @Override
+  public void doGet(HttpServletRequest request,
+      HttpServletResponse response) throws IOException,
+      ServletException {
+
+    /* Parse ip parameter. */
+    String ipParameter = request.getParameter("ip");
+    if (null == ipParameter) {
+      response.sendError(HttpServletResponse.SC_BAD_REQUEST,
+          "Missing ip parameter.");
+      return;
+    }
+    String relayIp = this.parseIpParameter(ipParameter);
+    if (null == relayIp) {
+      response.sendError(HttpServletResponse.SC_BAD_REQUEST,
+          "Invalid ip parameter.");
+      return;
+    }
+
+    /* Parse timestamp parameter. */
+    String timestampParameter = request.getParameter("timestamp");
+    if (null == timestampParameter) {
+      response.sendError(HttpServletResponse.SC_BAD_REQUEST,
+          "Missing timestamp parameter.");
+      return;
+    }
+    Long timestamp = this.parseTimestampParameter(timestampParameter);
+    if (null == timestamp) {
+      response.sendError(HttpServletResponse.SC_BAD_REQUEST,
+          "Invalid timestamp parameter.");
+      return;
+    }
+
+    /* Query the database. */
+    QueryResponse queryResponse = this.queryDatabase(relayIp, timestamp);
+    if (null == queryResponse) {
+      response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+          "Database error.");
+      return;
+    }
+
+    /* Format the query response. */
+    Gson gson = new Gson();
+    String formattedResponse = gson.toJson(queryResponse);
+
+    /* Write the response. */
+    response.setContentType("application/json");
+    response.setCharacterEncoding("utf-8");
+    response.getWriter().write(formattedResponse);
+  }
+
+  /* Helper methods for handling the request. */
+
+  private String parseIpParameter(String passedIpParameter) {
+    String relayIp = null;
+    if (passedIpParameter != null && passedIpParameter.length() > 0) {
+      String ipParameter = passedIpParameter.trim();
+      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}\\]?$");
+      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();
+          }
+        }
+      }
+    } else {
+      relayIp = "";
+    }
+    return relayIp;
+  }
+
+  private String convertIpV4ToHex(String relayIp) {
+    String[] relayIpParts = relayIp.split("\\.");
+    byte[] address24Bytes = new byte[4];
+    for (int i = 0; i < address24Bytes.length; i++) {
+      address24Bytes[i] = (byte) Integer.parseInt(relayIpParts[i]);
+    }
+    String address24 = Hex.encodeHexString(address24Bytes);
+    return address24;
+  }
+
+  private String convertIpV6ToHex(String relayIp) {
+    if (relayIp.startsWith("[") && relayIp.endsWith("]")) {
+      relayIp = relayIp.substring(1, relayIp.length() - 1);
+    }
+    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();
+      }
+    }
+    return address48;
+  }
+
+  private Long parseTimestampParameter(
+      String passedTimestampParameter) {
+    Long timestamp = null;
+    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
+    dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+    dateFormat.setLenient(false);
+    if (passedTimestampParameter != null
+        && passedTimestampParameter.length() > 0) {
+      String timestampParameter = passedTimestampParameter.trim();
+      try {
+        timestamp = dateFormat.parse(timestampParameter).getTime();
+      } catch (ParseException e) {
+        timestamp = null;
+      }
+    }
+    return timestamp;
+  }
+
+  /* Helper methods for querying the database. */
+
+  private QueryResponse queryDatabase(String relayIp, long timestamp) {
+
+    QueryResponse response = null;
+    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
+    dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+    SimpleDateFormat validAfterTimeFormat = new SimpleDateFormat(
+        "yyyy-MM-dd HH:mm:ss");
+    validAfterTimeFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+
+    /* Open a database connection that we'll use to handle the whole
+     * request. */
+    long requestedConnection = System.currentTimeMillis();
+    Connection conn = this.connectToDatabase();
+    if (null != conn) {
+
+      response = new QueryResponse();
+      response.queryAddress = relayIp;
+      response.queryDate = dateFormat.format(timestamp);
+
+      /* Look up first and last date in the database. */
+      long[] firstAndLastDates = this.queryFirstAndLastDatesFromDatabase(
+          conn);
+      if (null != firstAndLastDates) {
+        response.firstDateInDatabase = dateFormat.format(
+            firstAndLastDates[0]);
+        response.lastDateInDatabase = dateFormat.format(firstAndLastDates[1]);
+
+        /* Consider all consensuses published on or within a day of the given
+         * date. */
+        long timestampFrom = timestamp - 24L * 60L * 60L * 1000L;
+        long timestampTo = timestamp + 2 * 24L * 60L * 60L * 1000L - 1L;
+        String fromValidAfter = validAfterTimeFormat.format(timestampFrom);
+        String toValidAfter = validAfterTimeFormat.format(timestampTo);
+        SortedSet<Long> relevantConsensuses =
+            this.queryKnownConsensusValidAfterTimes(conn, fromValidAfter,
+                toValidAfter);
+        if (null != relevantConsensuses && !relevantConsensuses.isEmpty()) {
+          response.relevantStatuses = true;
+
+          /* Search for status entries with the given IP address as onion
+           * routing address, plus status entries of relays having an exit
+           * list entry with the given IP address as exit address. */
+          List<QueryResponse.Match> matches = this.queryStatusEntries(conn,
+              relayIp, timestamp, validAfterTimeFormat);
+          if (!matches.isEmpty()) {
+            response.matches = matches.toArray(new QueryResponse.Match[0]);
+
+          /* If we didn't find anything, run another query to find out if
+           * there are relays running on other IP addresses in the same /24 or
+           * /48 network and tell the user about it. */
+          } else {
+            if (!relayIp.contains(":")) {
+              String address24 = this.convertIpV4ToHex(relayIp)
+                  .substring(0, 6);
+              if (address24 != null) {
+                response.nearbyAddresses = this.queryAddressesInSame24(conn,
+                    address24, timestamp).toArray(new String[0]);
+              }
+            } else {
+              String address48 = this.convertIpV6ToHex(relayIp)
+                  .substring(0, 12);
+              if (address48 != null) {
+                response.nearbyAddresses = this.queryAddressesInSame48(conn,
+                    address48, timestamp).toArray(new String[0]);
+              }
+            }
+          }
+        }
+      }
+
+      /* Close the database connection. */
+      this.closeDatabaseConnection(conn, requestedConnection);
+    }
+    return response;
+  }
+
+  private Connection connectToDatabase() {
+    Connection conn = null;
+    try {
+      conn = this.ds.getConnection();
+    } catch (SQLException e) {
+      this.logger.log(Level.WARNING, "Couldn't connect: " + e.getMessage(), e);
+    }
+    return conn;
+  }
+
+  private long[] queryFirstAndLastDatesFromDatabase(Connection conn) {
+    long[] firstAndLastDates = null;
+    try {
+      Statement statement = conn.createStatement();
+      String query = "SELECT DATE(MIN(validafter)) AS first, "
+          + "DATE(MAX(validafter)) AS last FROM statusentry";
+      ResultSet rs = statement.executeQuery(query);
+      if (rs.next()) {
+        Calendar utcCalendar = Calendar.getInstance(
+            TimeZone.getTimeZone("UTC"));
+        firstAndLastDates = new long[] {
+            rs.getTimestamp(1, utcCalendar).getTime(),
+            rs.getTimestamp(2, utcCalendar).getTime()
+        };
+      }
+      rs.close();
+      statement.close();
+    } catch (SQLException e) {
+      /* Looks like we don't have any consensuses. */
+      firstAndLastDates = null;
+    }
+    return firstAndLastDates;
+  }
+
+  private SortedSet<Long> queryKnownConsensusValidAfterTimes(
+      Connection conn, String fromValidAfter, String toValidAfter) {
+    SortedSet<Long> relevantConsensuses = new TreeSet<>();
+    try {
+      Statement statement = conn.createStatement();
+      String query = "SELECT DISTINCT validafter FROM statusentry "
+          + "WHERE validafter >= '" + fromValidAfter
+          + "' AND validafter <= '" + toValidAfter + "'";
+      ResultSet rs = statement.executeQuery(query);
+      while (rs.next()) {
+        long consensusTime = rs.getTimestamp(1).getTime();
+        relevantConsensuses.add(consensusTime);
+      }
+      rs.close();
+      statement.close();
+    } catch (SQLException e) {
+      /* Looks like we don't have any consensuses in the requested
+       * interval. */
+      relevantConsensuses = null;
+    }
+    return relevantConsensuses;
+  }
+
+  private List<QueryResponse.Match> queryStatusEntries(Connection conn,
+      String relayIp, long timestamp,
+      SimpleDateFormat validAfterTimeFormat) {
+    List<QueryResponse.Match> matches = new ArrayList<>();
+    String addressHex = !relayIp.contains(":")
+        ? this.convertIpV4ToHex(relayIp) : this.convertIpV6ToHex(relayIp);
+    if (addressHex == null) {
+      return null;
+    }
+    String address24Or48Hex = !relayIp.contains(":")
+        ? addressHex.substring(0, 6) : addressHex.substring(0, 12);
+    try {
+      CallableStatement cs;
+      if (!relayIp.contains(":")) {
+        cs = conn.prepareCall("{call search_by_address24_date(?, ?)}");
+      } else {
+        cs = conn.prepareCall("{call search_by_address48_date(?, ?)}");
+      }
+      cs.setString(1, address24Or48Hex);
+      Calendar utcCalendar = Calendar.getInstance(
+          TimeZone.getTimeZone("UTC"));
+      cs.setDate(2, new java.sql.Date(timestamp), utcCalendar);
+      ResultSet rs = cs.executeQuery();
+      while (rs.next()) {
+        byte[] rawstatusentry = rs.getBytes(1);
+        SortedSet<String> addresses = new TreeSet<>();
+        SortedSet<String> addressesHex = new TreeSet<>();
+        String nickname = null;
+        Boolean exit = null;
+        for (String line : new String(rawstatusentry).split("\n")) {
+          if (line.startsWith("r ")) {
+            String[] parts = line.split(" ");
+            nickname = parts[1];
+            addresses.add(parts[6]);
+            addressesHex.add(this.convertIpV4ToHex(parts[6]));
+          } else if (line.startsWith("a ")) {
+            String address = line.substring("a ".length(),
+                line.lastIndexOf(":"));
+            addresses.add(address);
+            String orAddressHex = !address.contains(":")
+                ? this.convertIpV4ToHex(address)
+                : this.convertIpV6ToHex(address);
+            addressesHex.add(orAddressHex);
+          } else if (line.startsWith("p ")) {
+            exit = !line.equals("p reject 1-65535");
+          }
+        }
+        String exitaddress = rs.getString(4);
+        if (exitaddress != null && exitaddress.length() > 0) {
+          addresses.add(exitaddress);
+          addressesHex.add(this.convertIpV4ToHex(exitaddress));
+        }
+        if (!addressesHex.contains(addressHex)) {
+          continue;
+        }
+        long validafter = rs.getTimestamp(2, utcCalendar).getTime();
+        String validAfterString = validAfterTimeFormat.format(validafter);
+        String fingerprint = rs.getString(3).toUpperCase();
+        QueryResponse.Match match = new QueryResponse.Match();
+        match.timestamp = validAfterString;
+        match.addresses = addresses.toArray(new String[0]);
+        match.fingerprint = fingerprint;
+        match.nickname = nickname;
+        match.exit = exit;
+        matches.add(match);
+      }
+      rs.close();
+      cs.close();
+    } catch (SQLException e) {
+      /* Nothing found. */
+      matches.clear();
+    }
+    return matches;
+  }
+
+  private List<String> queryAddressesInSame24(Connection conn,
+      String address24, long timestamp) {
+    List<String> addressesInSameNetwork = new ArrayList<>();
+    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. */
+      addressesInSameNetwork = null;
+    }
+    return addressesInSameNetwork;
+  }
+
+  private List<String> queryAddressesInSame48(Connection conn,
+      String address48, long timestamp) {
+    List<String> addressesInSameNetwork = new ArrayList<>();
+    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. */
+      addressesInSameNetwork = null;
+    }
+    return addressesInSameNetwork;
+  }
+
+  private void closeDatabaseConnection(Connection conn,
+      long requestedConnection) {
+    try {
+      conn.close();
+      this.logger.info("Returned a database connection to the pool "
+          + "after " + (System.currentTimeMillis()
+          - requestedConnection) + " millis.");
+    } catch (SQLException e) {
+      this.logger.log(Level.WARNING, "Couldn't close: " + e.getMessage(), e);
+    }
+    return;
+  }
+
+}
+
diff --git a/src/main/webapp/web.xml b/src/main/webapp/web.xml
index ce12489..d838059 100644
--- a/src/main/webapp/web.xml
+++ b/src/main/webapp/web.xml
@@ -17,6 +17,17 @@
     <url-pattern>/index.html</url-pattern>
   </servlet-mapping>
 
+  <servlet>
+    <servlet-name>Query</servlet-name>
+    <servlet-class>
+      org.torproject.exonerator.QueryServlet
+    </servlet-class>
+  </servlet>
+  <servlet-mapping>
+    <servlet-name>Query</servlet-name>
+    <url-pattern>/query.json</url-pattern>
+  </servlet-mapping>
+
   <welcome-file-list>
     <welcome-file>index.html</welcome-file>
   </welcome-file-list>





More information about the tor-commits mailing list