[tor-commits] [metrics-web/master] Make ExoneraTor Beta the new default.

karsten at torproject.org karsten at torproject.org
Thu Mar 8 13:31:24 UTC 2012


commit becc1f24af8d149c6c4a9e5223b7f0f4d4731695
Author: Karsten Loesing <karsten.loesing at gmx.net>
Date:   Thu Mar 8 14:29:47 2012 +0100

    Make ExoneraTor Beta the new default.
    
    Two major changes:
    
    - Results include IP addresses from exit lists, too.
    
    - It's not required anymore to specify an exact timestamp, but a date is
      enough.
---
 etc/web.xml                                        |   11 -
 .../ernie/web/ExoneraTorBetaServlet.java           | 1015 --------------------
 .../torproject/ernie/web/ExoneraTorServlet.java    |  552 +++++++----
 3 files changed, 359 insertions(+), 1219 deletions(-)

diff --git a/etc/web.xml b/etc/web.xml
index fea3df6..e377968 100644
--- a/etc/web.xml
+++ b/etc/web.xml
@@ -266,17 +266,6 @@
   </servlet-mapping>
 
   <servlet>
-    <servlet-name>ExoneraTorBeta</servlet-name>
-    <servlet-class>
-      org.torproject.ernie.web.ExoneraTorBetaServlet
-    </servlet-class>
-  </servlet>
-  <servlet-mapping>
-    <servlet-name>ExoneraTorBeta</servlet-name>
-    <url-pattern>/exonerator-beta.html</url-pattern>
-  </servlet-mapping>
-
-  <servlet>
     <servlet-name>ServerDescriptor</servlet-name>
     <servlet-class>
       org.torproject.ernie.web.ServerDescriptorServlet
diff --git a/src/org/torproject/ernie/web/ExoneraTorBetaServlet.java b/src/org/torproject/ernie/web/ExoneraTorBetaServlet.java
deleted file mode 100644
index f81d522..0000000
--- a/src/org/torproject/ernie/web/ExoneraTorBetaServlet.java
+++ /dev/null
@@ -1,1015 +0,0 @@
-package org.torproject.ernie.web;
-
-import java.io.*;
-import java.math.*;
-import java.sql.*;
-import java.text.*;
-import java.util.*;
-import java.util.logging.*;
-import java.util.regex.*;
-
-import javax.naming.*;
-import javax.servlet.*;
-import javax.servlet.http.*;
-import javax.sql.*;
-
-import org.apache.commons.codec.binary.*;
-import org.apache.commons.lang.*;
-
-public class ExoneraTorBetaServlet extends HttpServlet {
-
-  private DataSource ds;
-
-  private Logger logger;
-
-  public void init() {
-
-    /* Initialize logger. */
-    this.logger = Logger.getLogger(
-        ExoneraTorBetaServlet.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);
-    }
-  }
-
-  private void writeHeader(PrintWriter out) throws IOException {
-    out.println("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 "
-          + "Transitional//EN\">\n"
-        + "<html>\n"
-        + "  <head>\n"
-        + "    <title>Tor Metrics Portal: ExoneraTor</title>\n"
-        + "    <meta http-equiv=\"content-type\" content=\"text/html; "
-          + "charset=ISO-8859-1\">\n"
-        + "    <link href=\"/css/stylesheet-ltr.css\" type=\"text/css\" "
-          + "rel=\"stylesheet\">\n"
-        + "    <link href=\"/images/favicon.ico\" "
-          + "type=\"image/x-icon\" rel=\"shortcut icon\">\n"
-        + "  </head>\n"
-        + "  <body>\n"
-        + "    <div class=\"center\">\n"
-        + "      <table class=\"banner\" border=\"0\" cellpadding=\"0\" "
-          + "cellspacing=\"0\" summary=\"\">\n"
-        + "        <tr>\n"
-        + "          <td class=\"banner-left\"><a "
-          + "href=\"/index.html\"><img src=\"/images/top-left.png\" "
-          + "alt=\"Click to go to home page\" width=\"193\" "
-          + "height=\"79\"></a></td>\n"
-        + "          <td class=\"banner-middle\">\n"
-        + "            <a href=\"/\">Home</a>\n"
-        + "            <a href=\"graphs.html\">Graphs</a>\n"
-        + "            <a href=\"research.html\">Research</a>\n"
-        + "            <a href=\"status.html\">Status</a>\n"
-        + "            <br>\n"
-        + "            <font size=\"2\">\n"
-        + "              <a class=\"current\">ExoneraTor</a>\n"
-        + "              <a href=\"relay-search.html\">Relay Search</a>\n"
-        + "              <a href=\"consensus-health.html\">Consensus "
-          + "Health</a>\n"
-        + "            </font>\n"
-        + "          </td>\n"
-        + "          <td class=\"banner-right\"></td>\n"
-        + "        </tr>\n"
-        + "      </table>\n"
-        + "      <div class=\"main-column\" style=\"margin:5; "
-          + "Padding:0;\">\n"
-        + "        <h2>ExoneraTor</h2>\n"
-        + "        <h3>or: a website that tells you whether some IP "
-          + "address was a Tor relay</h3>\n"
-        + "        <p>ExoneraTor tells you whether there was a Tor relay "
-          + "running on a given IP address at a given time. ExoneraTor "
-          + "can further find out whether this relay permitted exiting "
-          + "to a given server and/or TCP port. ExoneraTor learns about "
-          + "these facts from parsing the public relay lists and relay "
-          + "descriptors that are collected from the Tor directory "
-          + "authorities and the exit lists collected by TorDNSEL.</p>\n"
-        + "        <br>\n"
-        + "        <p>This is a <b>BETA</b> version of ExoneraTor.  "
-          + "Beware of bugs.  The stable version of ExoneraTor is still "
-          + "available <a href=\"exonerator.html\">here</a>.  The "
-          + "visible changes in this BETA version are:</p>\n"
-        + "        <ul>\n"
-        + "        <li>Results now include IP addresses from exit "
-          + "lists, too.</li>\n"
-        + "        <li>It's not required anymore to specify an exact "
-          + "timestamp, but now a date is enough.</li>\n"
-        + "        </ul><br>\n"
-        + "        <p><font color=\"red\"><b>Notice:</b> Note that the "
-          + "information you are providing below may be leaked to anyone "
-          + "who can read the network traffic between you and this web "
-          + "server or who has access to this web server. If you need to "
-          + "keep the IP addresses and incident times confidential, you "
-          + "should download the <a href=\"tools.html#exonerator\">Java "
-          + "or Python version of ExoneraTor</a> and run it on your "
-          + "local machine.</font></p>\n"
-        + "        <br>\n");
-  }
-
-  private void writeFooter(PrintWriter out) throws IOException {
-    out.println("        <br>\n"
-        + "      </div>\n"
-        + "    </div>\n"
-        + "    <div class=\"bottom\" id=\"bottom\">\n"
-        + "      <p>This material is supported in part by the National "
-          + "Science Foundation under Grant No. CNS-0959138. Any "
-          + "opinions, finding, and conclusions or recommendations "
-          + "expressed in this material are those of the author(s) and "
-          + "do not necessarily reflect the views of the National "
-          + "Science Foundation.</p>\n"
-        + "      <p>\"Tor\" and the \"Onion Logo\" are <a "
-          + "href=\"https://www.torproject.org/docs/trademark-faq.html.en"
-          + "\">registered trademarks</a> of The Tor Project, Inc.</p>\n"
-        + "      <p>Data on this site is freely available under a <a "
-          + "href=\"http://creativecommons.org/publicdomain/zero/1.0/\">"
-          + "CC0 no copyright declaration</a>: To the extent possible "
-          + "under law, the Tor Project has waived all copyright and "
-          + "related or neighboring rights in the data. Graphs are "
-          + "licensed under a <a "
-          + "href=\"http://creativecommons.org/licenses/by/3.0/us/\">"
-          + "Creative Commons Attribution 3.0 United States "
-          + "License</a>.</p>\n"
-        + "    </div>\n"
-        + "  </body>\n"
-        + "</html>");
-    out.close();
-  }
-
-  public void doGet(HttpServletRequest request,
-      HttpServletResponse response) throws IOException,
-      ServletException {
-
-    /* Start writing response. */
-    PrintWriter out = response.getWriter();
-    writeHeader(out);
-
-    /* Open a database connection that we'll use to handle the whole
-     * request. */
-    Connection conn = null;
-    long requestedConnection = System.currentTimeMillis();
-    try {
-      conn = this.ds.getConnection();
-    } catch (SQLException e) {
-      out.println("<p><font color=\"red\"><b>Warning: </b></font>Unable "
-          + "to connect to the database. If this problem persists, "
-          + "please <a href=\"mailto:tor-assistants at torproject.org\">let "
-          + "us know</a>!</p>\n");
-      writeFooter(out);
-      return;
-    }
-
-    /* Look up first and last consensus in the database. */
-    long firstValidAfter = -1L, lastValidAfter = -1L;
-    try {
-      Statement statement = conn.createStatement();
-      String query = "SELECT MIN(validafter) AS first, "
-          + "MAX(validafter) AS last FROM consensus";
-      ResultSet rs = statement.executeQuery(query);
-      if (rs.next()) {
-        firstValidAfter = rs.getTimestamp(1).getTime();
-        lastValidAfter = rs.getTimestamp(2).getTime();
-      }
-      rs.close();
-      statement.close();
-    } catch (SQLException e) {
-      /* Looks like we don't have any consensuses. */
-    }
-    if (firstValidAfter < 0L || lastValidAfter < 0L) {
-      out.println("<p><font color=\"red\"><b>Warning: </b></font>This "
-          + "server doesn't have any relay lists available. If this "
-          + "problem persists, please "
-          + "<a href=\"mailto:tor-assistants at torproject.org\">let us "
-          + "know</a>!</p>\n");
-      writeFooter(out);
-      try {
-        conn.close();
-        this.logger.info("Returned a database connection to the pool "
-            + "after " + (System.currentTimeMillis()
-            - requestedConnection) + " millis.");
-      } catch (SQLException e) {
-      }
-      return;
-    }
-
-    out.println("<a name=\"relay\"></a><h3>Was there a Tor relay running "
-        + "on this IP address?</h3>");
-
-    /* Parse IP parameter. */
-    /* TODO Extend the parsing code to accept IPv6 addresses, too. */
-    Pattern ipAddressPattern = 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])$");
-    String ipParameter = request.getParameter("ip");
-    String relayIP = "", ipWarning = "";
-    if (ipParameter != null && ipParameter.length() > 0) {
-      Matcher ipParameterMatcher = ipAddressPattern.matcher(ipParameter);
-      if (ipParameterMatcher.matches()) {
-        String[] ipParts = ipParameter.split("\\.");
-        relayIP = Integer.parseInt(ipParts[0]) + "."
-            + Integer.parseInt(ipParts[1]) + "."
-            + Integer.parseInt(ipParts[2]) + "."
-            + Integer.parseInt(ipParts[3]);
-      } else {
-        ipWarning = "\"" + (ipParameter.length() > 20 ?
-            StringEscapeUtils.escapeHtml(ipParameter.substring(0, 20))
-            + "[...]" : StringEscapeUtils.escapeHtml(ipParameter))
-            + "\" is not a valid IP address.";
-      }
-    }
-
-    /* Parse timestamp parameter. */
-    String timestampParameter = request.getParameter("timestamp");
-    long timestamp = 0L;
-    boolean timestampIsDate = false;
-    String timestampStr = "", timestampWarning = "";
-    SimpleDateFormat shortDateTimeFormat = new SimpleDateFormat(
-        "yyyy-MM-dd HH:mm");
-    shortDateTimeFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
-    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
-    dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
-    if (timestampParameter != null && timestampParameter.length() > 0) {
-      try {
-        if (timestampParameter.split(" ").length == 1) {
-          timestamp = dateFormat.parse(timestampParameter).getTime();
-          timestampStr = dateFormat.format(timestamp);
-          timestampIsDate = true;
-        } else {
-          timestamp = shortDateTimeFormat.parse(timestampParameter).
-              getTime();
-          timestampStr = shortDateTimeFormat.format(timestamp);
-        }
-        if (timestamp < firstValidAfter || timestamp > lastValidAfter) {
-          timestampWarning = "Please pick a date or timestamp between \""
-              + shortDateTimeFormat.format(firstValidAfter) + "\" and \""
-              + shortDateTimeFormat.format(lastValidAfter) + "\".";
-          timestamp = 0L;
-        }
-      } catch (ParseException e) {
-        /* We have no way to handle this exception, other than leaving
-           timestampStr at "". */
-        timestampWarning = "\"" + (timestampParameter.length() > 20 ?
-            StringEscapeUtils.escapeHtml(timestampParameter.
-            substring(0, 20)) + "[...]" :
-            StringEscapeUtils.escapeHtml(timestampParameter))
-            + "\" is not a valid date or timestamp.";
-      }
-    }
-
-    /* If either IP address or timestamp is provided, the other one must
-     * be provided, too. */
-    if (relayIP.length() < 1 && timestampStr.length() > 0 &&
-        ipWarning.length() < 1) {
-      ipWarning = "Please provide an IP address.";
-    }
-    if (relayIP.length() > 0 && timestamp < 1 &&
-        timestampWarning.length() < 1) {
-      timestampWarning = "Please provide a date or timestamp.";
-    }
-
-    /* Parse target IP parameter. */
-    String targetIP = "", targetPort = "", target = "";
-    String[] targetIPParts = null;
-    String targetAddrParameter = request.getParameter("targetaddr");
-    String targetAddrWarning = "";
-    if (targetAddrParameter != null && targetAddrParameter.length() > 0) {
-      Matcher targetAddrParameterMatcher =
-          ipAddressPattern.matcher(targetAddrParameter);
-      if (targetAddrParameterMatcher.matches()) {
-        String[] targetAddrParts = targetAddrParameter.split("\\.");
-        targetIP = Integer.parseInt(targetAddrParts[0]) + "."
-            + Integer.parseInt(targetAddrParts[1]) + "."
-            + Integer.parseInt(targetAddrParts[2]) + "."
-            + Integer.parseInt(targetAddrParts[3]);
-        target = targetIP;
-        targetIPParts = targetIP.split("\\.");
-      } else {
-        targetAddrWarning = "\"" + (targetAddrParameter.length() > 20 ?
-            StringEscapeUtils.escapeHtml(targetAddrParameter.substring(
-            0, 20)) + "[...]" : StringEscapeUtils.escapeHtml(
-            targetAddrParameter)) + "\" is not a valid IP address.";
-      }
-    }
-
-    /* Parse target port parameter. */
-    String targetPortParameter = request.getParameter("targetport");
-    String targetPortWarning = "";
-    if (targetPortParameter != null && targetPortParameter.length() > 0) {
-      Pattern targetPortPattern = Pattern.compile("\\d+");
-      if (targetPortParameter.length() < 5 &&
-          targetPortPattern.matcher(targetPortParameter).matches() &&
-          !targetPortParameter.equals("0") &&
-          Integer.parseInt(targetPortParameter) < 65536) {
-        targetPort = targetPortParameter;
-        if (target != null) {
-          target += ":" + targetPort;
-        } else {
-          target = targetPort;
-        }
-      } else {
-        targetPortWarning = "\"" + (targetPortParameter.length() > 8 ?
-            StringEscapeUtils.escapeHtml(targetPortParameter.
-            substring(0, 8)) + "[...]" :
-            StringEscapeUtils.escapeHtml(targetPortParameter))
-            + "\" is not a valid TCP port.";
-      }
-    }
-
-    /* If target port is provided, a target address must be provided,
-     * too. */
-    /* TODO Relax this requirement. */
-    if (targetPort.length() > 0 && targetIP.length() < 1 &&
-        targetAddrWarning.length() < 1) {
-      targetAddrWarning = "Please provide an IP address.";
-    }
-
-    /* Write form with IP address and timestamp. */
-    out.println("        <form action=\"#relay\">\n"
-        + "          <input type=\"hidden\" name=\"targetaddr\" "
-        + (targetIP.length() > 0 ? " value=\"" + targetIP + "\"" : "")
-        + ">\n"
-        + "          <input type=\"hidden\" name=\"targetPort\""
-        + (targetPort.length() > 0 ? " value=\"" + targetPort + "\"" : "")
-        + ">\n"
-        + "          <table>\n"
-        + "            <tr>\n"
-        + "              <td align=\"right\">IP address in question:"
-          + "</td>\n"
-        + "              <td><input type=\"text\" name=\"ip\""
-          + (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"
-        + "            </tr>\n"
-        + "            <tr>\n"
-        + "              <td align=\"right\">Date or timestamp, in "
-          + "UTC:</td>\n"
-        + "              <td><input type=\"text\" name=\"timestamp\""
-          + (timestampStr.length() > 0 ? " value=\"" + timestampStr + "\""
-            : "")
-          + ">"
-          + (timestampWarning.length() > 0 ? "<br><font color=\"red\">"
-              + timestampWarning + "</font>" : "")
-        + "</td>\n"
-        + "              <td><i>(Ex.: 2010-01-01 or 2010-01-01 12:00)"
-          + "</i></td>\n"
-        + "            </tr>\n"
-        + "            <tr>\n"
-        + "              <td></td>\n"
-        + "              <td>\n"
-        + "                <input type=\"submit\">\n"
-        + "                <input type=\"reset\">\n"
-        + "              </td>\n"
-        + "              <td></td>\n"
-        + "            </tr>\n"
-        + "          </table>\n"
-        + "        </form>\n");
-
-    if (relayIP.length() < 1 || timestamp < 1) {
-      writeFooter(out);
-      try {
-        conn.close();
-        this.logger.info("Returned a database connection to the pool "
-            + "after " + (System.currentTimeMillis()
-            - requestedConnection) + " millis.");
-      } catch (SQLException e) {
-      }
-      return;
-    }
-
-    out.printf("<p>Looking up IP address %s in the relay lists "
-        + "published ", relayIP);
-    long timestampFrom, timestampTo;
-    if (timestampIsDate) {
-      /* If we only have a date, consider all consensuses published on the
-       * given date, plus the ones published 3 hours before the given date
-       * and until 23:59:59. */
-      timestampFrom = timestamp - 3L * 60L * 60L * 1000L;
-      timestampTo = timestamp + (24L * 60L * 60L - 1L) * 1000L;
-      out.printf("on %s", timestampStr);
-    } else {
-      /* If we have an exact timestamp, consider the consensuses published
-       * in the 3 hours preceding the UTC timestamp. */
-      timestampFrom = timestamp - 3L * 60L * 60L * 1000L;
-      timestampTo = timestamp;
-      out.printf("between %s and %s UTC",
-        shortDateTimeFormat.format(timestampFrom),
-        shortDateTimeFormat.format(timestampTo));
-    }
-    /* If we don't find any relays in the given time interval, also look
-     * at consensuses published 12 hours before and 12 hours after the
-     * interval, in case the user got the "UTC" bit wrong. */
-    long timestampTooOld = timestampFrom - 12L * 60L * 60L * 1000L;
-    long timestampTooNew = timestampTo + 12L * 60L * 60L * 1000L;
-    out.print(" as well as in the relevant exit lists. Clients could "
-        + "have selected any of these relays to build circuits. "
-        + "You may follow the links to relay lists and relay descriptors "
-        + "to grep for the lines printed below and confirm that results "
-        + "are correct.<br>");
-    SimpleDateFormat validAfterTimeFormat = new SimpleDateFormat(
-        "yyyy-MM-dd HH:mm:ss");
-    validAfterTimeFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
-    String fromValidAfter = validAfterTimeFormat.format(timestampTooOld);
-    String toValidAfter = validAfterTimeFormat.format(timestampTooNew);
-    SortedSet<Long> tooOldConsensuses = new TreeSet<Long>();
-    SortedSet<Long> relevantConsensuses = new TreeSet<Long>();
-    SortedSet<Long> tooNewConsensuses = new TreeSet<Long>();
-    try {
-      Statement statement = conn.createStatement();
-      String query = "SELECT validafter FROM consensus "
-          + "WHERE validafter >= '" + fromValidAfter
-          + "' AND validafter <= '" + toValidAfter + "'";
-      ResultSet rs = statement.executeQuery(query);
-      while (rs.next()) {
-        long consensusTime = rs.getTimestamp(1).getTime();
-        if (consensusTime < timestampFrom) {
-          tooOldConsensuses.add(consensusTime);
-        } else if (consensusTime > timestampTo) {
-          tooNewConsensuses.add(consensusTime);
-        } else {
-          relevantConsensuses.add(consensusTime);
-        }
-      }
-      rs.close();
-      statement.close();
-    } catch (SQLException e) {
-      /* Looks like we don't have any consensuses in the requested
-       * interval. */
-    }
-    SortedSet<Long> allConsensuses = new TreeSet<Long>();
-    allConsensuses.addAll(tooOldConsensuses);
-    allConsensuses.addAll(relevantConsensuses);
-    allConsensuses.addAll(tooNewConsensuses);
-    if (allConsensuses.isEmpty()) {
-      out.println("        <p>No relay lists found!</p>\n"
-          + "        <p>Result is INDECISIVE!</p>\n"
-          + "        <p>We cannot make any statement whether there was "
-          + "a Tor relay running on IP address " + relayIP
-          + (timestampIsDate ? " on " : " at ") + timestampStr + "! We "
-          + "did not find any relevant relay lists at the given time. If "
-          + "you think this is an error on our side, please "
-          + "<a href=\"mailto:tor-assistants at torproject.org\">contact "
-          + "us</a>!</p>\n");
-      writeFooter(out);
-      try {
-        conn.close();
-        this.logger.info("Returned a database connection to the pool "
-            + "after " + (System.currentTimeMillis()
-            - requestedConnection) + " millis.");
-      } catch (SQLException e) {
-      }
-      return;
-    }
-
-    /* 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. */
-    SortedMap<Long, SortedMap<String, String>> statusEntries =
-        new TreeMap<Long, SortedMap<String, String>>();
-    SortedSet<Long> positiveConsensusesNoTarget = new TreeSet<Long>();
-    SortedMap<String, Set<Long>> relevantDescriptors =
-        new TreeMap<String, Set<Long>>();
-    try {
-      CallableStatement cs = conn.prepareCall(
-          "{call search_statusentries_by_address_date(?, ?)}");
-      cs.setString(1, relayIP);
-      cs.setDate(2, new java.sql.Date(timestamp));
-      ResultSet rs = cs.executeQuery();
-      while (rs.next()) {
-        byte[] rawstatusentry = rs.getBytes(1);
-        String descriptor = rs.getString(2);
-        long validafter = rs.getTimestamp(3).getTime();
-        positiveConsensusesNoTarget.add(validafter);
-        if (!relevantDescriptors.containsKey(descriptor)) {
-          relevantDescriptors.put(descriptor, new HashSet<Long>());
-        }
-        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";
-        if (exitaddress != null && exitaddress.length() > 0) {
-          long scanned = rs.getTimestamp(7).getTime();
-          htmlString += "  [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);
-      }
-      rs.close();
-      cs.close();
-    } catch (SQLException e) {
-      /* Nothing found. */
-    }
-
-    /* Print out what we found. */
-    SimpleDateFormat validAfterUrlFormat = new SimpleDateFormat(
-        "yyyy-MM-dd-HH-mm-ss");
-    validAfterUrlFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
-    out.print("<pre><code>");
-    for (long consensus : allConsensuses) {
-      if (relevantConsensuses.contains(consensus)) {
-        String validAfterDatetime = validAfterTimeFormat.format(
-            consensus);
-        String validAfterString = validAfterUrlFormat.format(consensus);
-        out.print("valid-after <b>"
-            + "<a href=\"consensus?valid-after="
-            + validAfterString + "\" target=\"_blank\">"
-            + validAfterDatetime + "</b></a>\n");
-        if (statusEntries.containsKey(consensus)) {
-          for (String htmlString :
-              statusEntries.get(consensus).values()) {
-            out.print(htmlString);
-          }
-        }
-        out.print("\n");
-      }
-    }
-    out.print("</code></pre>");
-    if (relevantDescriptors.isEmpty()) {
-      out.printf("        <p>None found!</p>\n"
-          + "        <p>Result is NEGATIVE with high certainty!</p>\n"
-          + "        <p>We did not find IP "
-          + "address " + relayIP + " in any of the relay or exit lists "
-          + "that were published between %s and %s.</p>\n",
-          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()) {
-          Map<String, String> resultEntry = new HashMap<String, String>();
-          String address = rs.getString(1);
-          addressesInSameNetwork.add(address);
-        }
-        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");
-        out.print("        <ul>\n");
-        for (String s : addressesInSameNetwork) {
-          out.print("        <li>" + s + "</li>\n");
-        }
-        out.print("        </ul>\n");
-      }
-      writeFooter(out);
-      try {
-        conn.close();
-        this.logger.info("Returned a database connection to the pool "
-            + "after " + (System.currentTimeMillis()
-            - requestedConnection) + " millis.");
-      } catch (SQLException e) {
-      }
-      return;
-    }
-
-    /* Print out result. */
-    boolean inMostRelevantConsensuses = false,
-        inOtherRelevantConsensus = false,
-        inTooOldConsensuses = false,
-        inTooNewConsensuses = false;
-    for (long match : positiveConsensusesNoTarget) {
-      if (timestampIsDate &&
-          dateFormat.format(match).equals(timestampStr)) {
-        inMostRelevantConsensuses = true;
-      } else if (!timestampIsDate &&
-          match == relevantConsensuses.last()) {
-        inMostRelevantConsensuses = true;
-      } else if (relevantConsensuses.contains(match)) {
-        inOtherRelevantConsensus = true;
-      } else if (tooOldConsensuses.contains(match)) {
-        inTooOldConsensuses = true;
-      } else if (tooNewConsensuses.contains(match)) {
-        inTooNewConsensuses = true;
-      }
-    }
-    if (inMostRelevantConsensuses) {
-      out.print("        <p>Result is POSITIVE with high certainty!"
-            + "</p>\n"
-          + "        <p>We found one or more relays on IP address "
-          + relayIP + " in ");
-      if (timestampIsDate) {
-        out.print("relay list published on " + timestampStr);
-      } else {
-        out.print("the most recent relay list preceding " + timestampStr);
-      }
-      out.print(" that clients were likely to know.</p>\n");
-    } else {
-      if (inOtherRelevantConsensus) {
-        out.println("        <p>Result is POSITIVE "
-            + "with moderate certainty!</p>\n");
-        out.println("<p>We found one or more relays on IP address "
-            + relayIP + ", but not in ");
-        if (timestampIsDate) {
-          out.print("a relay list published on " + timestampStr);
-        } else {
-          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 "
-            + "authorities had difficulties connecting to the relay. "
-            + "However, clients might still have used the relay.</p>\n");
-      } else {
-        out.println("        <p>Result is NEGATIVE "
-            + "with high certainty!</p>\n");
-        out.println("        <p>We did not find any relay on IP address "
-            + relayIP
-            + " in the relay lists 3 hours preceding " + timestampStr
-            + ".</p>\n");
-        if (inTooOldConsensuses || inTooNewConsensuses) {
-          if (inTooOldConsensuses && !inTooNewConsensuses) {
-            out.println("        <p>Note that we found a matching relay "
-                + "in relay lists that were published between 15 and 3 "
-                + "hours before " + timestampStr + ".</p>\n");
-          } else if (!inTooOldConsensuses && inTooNewConsensuses) {
-            out.println("        <p>Note that we found a matching relay "
-                + "in relay lists that were published up to 12 hours "
-                + "after " + timestampStr + ".</p>\n");
-          } else {
-            out.println("        <p>Note that we found a matching relay "
-                + "in relay lists that were published between 15 and 3 "
-                + "hours before and in relay lists that were published "
-                + "up to 12 hours after " + timestampStr + ".</p>\n");
-          }
-          if (timestampIsDate) {
-            out.println("<p>Be sure to try out the previous/next day or "
-                + "provide an exact timestamp in UTC.</p>");
-          } else {
-            out.println("<p>Make sure that the timestamp you "
-                + "provided is correctly converted to the UTC "
-                + "timezone.</p>");
-          }
-        }
-        /* We didn't find any descriptor.  No need to look up targets. */
-        writeFooter(out);
-        try {
-          conn.close();
-          this.logger.info("Returned a database connection to the pool "
-              + "after " + (System.currentTimeMillis()
-              - requestedConnection) + " millis.");
-        } catch (SQLException e) {
-        }
-        return;
-      }
-    }
-
-    /* Second part: target */
-    out.println("<br><a name=\"exit\"></a><h3>Was this relay configured "
-        + "to permit exiting to a given target?</h3>");
-
-    out.println("        <form action=\"#exit\">\n"
-        + "              <input type=\"hidden\" name=\"timestamp\"\n"
-        + "                         value=\"" + timestampStr + "\">\n"
-        + "              <input type=\"hidden\" name=\"ip\" "
-          + "value=\"" + relayIP + "\">\n"
-        + "          <table>\n"
-        + "            <tr>\n"
-        + "              <td align=\"right\">Target address:</td>\n"
-        + "              <td><input type=\"text\" name=\"targetaddr\""
-          + (targetIP.length() > 0 ? " value=\"" + targetIP + "\"" : "")
-          + "\">"
-          + (targetAddrWarning.length() > 0 ? "<br><font color=\"red\">"
-              + targetAddrWarning + "</font>" : "")
-        + "</td>\n"
-        + "              <td><i>(Ex.: 4.3.2.1)</i></td>\n"
-        + "            </tr>\n"
-        + "            <tr>\n"
-        + "              <td align=\"right\">Target port:</td>\n"
-        + "              <td><input type=\"text\" name=\"targetport\""
-          + (targetPort.length() > 0 ? " value=\"" + targetPort + "\""
-            : "")
-          + ">"
-          + (targetPortWarning.length() > 0 ? "<br><font color=\"red\">"
-              + targetPortWarning + "</font>" : "")
-        + "</td>\n"
-        + "              <td><i>(Ex.: 80)</i></td>\n"
-        + "            </tr>\n"
-        + "            <tr>\n"
-        + "              <td></td>\n"
-        + "              <td>\n"
-        + "                <input type=\"submit\">\n"
-        + "                <input type=\"reset\">\n"
-        + "              </td>\n"
-        + "              <td></td>\n"
-        + "            </tr>\n"
-        + "          </table>\n"
-        + "        </form>\n");
-
-    if (targetIP.length() < 1) {
-      writeFooter(out);
-      try {
-        conn.close();
-        this.logger.info("Returned a database connection to the pool "
-            + "after " + (System.currentTimeMillis()
-            - requestedConnection) + " millis.");
-      } catch (SQLException e) {
-      }
-      return;
-    }
-
-    /* Parse router descriptors to check exit policies. */
-    out.println("<p>Searching the relay descriptors published by the "
-        + "relay on IP address " + relayIP + " to find out whether this "
-        + "relay permitted exiting to " + target + ". You may follow the "
-        + "links above to the relay descriptors and grep them for the "
-        + "lines printed below to confirm that results are correct.</p>");
-    SortedSet<Long> positiveConsensuses = new TreeSet<Long>();
-    Set<String> missingDescriptors = new HashSet<String>();
-    Set<String> descriptors = relevantDescriptors.keySet();
-    for (String descriptor : descriptors) {
-      byte[] rawDescriptor = null;
-      try {
-        String query = "SELECT rawdescriptor FROM descriptor "
-            + "WHERE descriptor = '" + descriptor + "'";
-        Statement statement = conn.createStatement();
-        ResultSet rs = statement.executeQuery(query);
-        if (rs.next()) {
-          rawDescriptor = rs.getBytes(1);
-        }
-        rs.close();
-        statement.close();
-      } catch (SQLException e) {
-        /* Consider this descriptors as 'missing'. */
-        continue;
-      }
-      if (rawDescriptor != null && rawDescriptor.length > 0) {
-        missingDescriptors.remove(descriptor);
-        String rawDescriptorString = new String(rawDescriptor,
-            "US-ASCII");
-        try {
-          BufferedReader br = new BufferedReader(
-              new StringReader(rawDescriptorString));
-          String line = null, routerLine = null, publishedLine = null;
-          StringBuilder acceptRejectLines = new StringBuilder();
-          boolean foundMatch = false;
-          while ((line = br.readLine()) != null) {
-            if (line.startsWith("router ")) {
-              routerLine = line;
-            } else if (line.startsWith("published ")) {
-              publishedLine = line;
-            } else if (line.startsWith("reject ") ||
-                line.startsWith("accept ")) {
-              if (foundMatch) {
-                out.println(line);
-                continue;
-              }
-              boolean ruleAccept = line.split(" ")[0].equals("accept");
-              String ruleAddress = line.split(" ")[1].split(":")[0];
-              if (!ruleAddress.equals("*")) {
-                if (!ruleAddress.contains("/") &&
-                    !ruleAddress.equals(targetIP)) {
-                  /* IP address does not match. */
-                  acceptRejectLines.append(line + "\n");
-                  continue;
-                }
-                String[] ruleIPParts = ruleAddress.split("/")[0].
-                    split("\\.");
-                int ruleNetwork = ruleAddress.contains("/") ?
-                    Integer.parseInt(ruleAddress.split("/")[1]) : 32;
-                for (int i = 0; i < 4; i++) {
-                  if (ruleNetwork == 0) {
-                    break;
-                  } else if (ruleNetwork >= 8) {
-                    if (ruleIPParts[i].equals(targetIPParts[i])) {
-                      ruleNetwork -= 8;
-                    } else {
-                      break;
-                    }
-                  } else {
-                    int mask = 255 ^ 255 >>> ruleNetwork;
-                    if ((Integer.parseInt(ruleIPParts[i]) & mask) ==
-                        (Integer.parseInt(targetIPParts[i]) & mask)) {
-                      ruleNetwork = 0;
-                    }
-                    break;
-                  }
-                }
-                if (ruleNetwork > 0) {
-                  /* IP address does not match. */
-                  acceptRejectLines.append(line + "\n");
-                  continue;
-                }
-              }
-              String rulePort = line.split(" ")[1].split(":")[1];
-              if (targetPort.length() < 1 && !ruleAccept &&
-                  !rulePort.equals("*")) {
-                /* With no port given, we only consider reject :* rules as
-                   matching. */
-                acceptRejectLines.append(line + "\n");
-                continue;
-              }
-              if (targetPort.length() > 0 && !rulePort.equals("*") &&
-                  rulePort.contains("-")) {
-                int fromPort = Integer.parseInt(rulePort.split("-")[0]);
-                int toPort = Integer.parseInt(rulePort.split("-")[1]);
-                int targetPortInt = Integer.parseInt(targetPort);
-                if (targetPortInt < fromPort ||
-                    targetPortInt > toPort) {
-                  /* Port not contained in interval. */
-                  continue;
-                }
-              }
-              if (targetPort.length() > 0) {
-                if (!rulePort.equals("*") &&
-                    !rulePort.contains("-") &&
-                    !targetPort.equals(rulePort)) {
-                  /* Ports do not match. */
-                  acceptRejectLines.append(line + "\n");
-                  continue;
-                }
-              }
-              boolean relevantMatch = false;
-              for (long match : relevantDescriptors.get(descriptor)) {
-                if (relevantConsensuses.contains(match)) {
-                  relevantMatch = true;
-                }
-              }
-              if (relevantMatch) {
-                String[] routerParts = routerLine.split(" ");
-                out.println("<pre><code>" + routerParts[0] + " "
-                    + routerParts[1] + " <b>" + routerParts[2] + "</b> "
-                    + routerParts[3] + " " + routerParts[4] + " "
-                    + routerParts[5]);
-                String[] publishedParts = publishedLine.split(" ");
-                out.println(publishedParts[0] + " <b>"
-                    + publishedParts[1] + " " + publishedParts[2]
-                    + "</b>");
-                out.print(acceptRejectLines.toString());
-                out.println("<b>" + line + "</b>");
-                foundMatch = true;
-              }
-              if (ruleAccept) {
-                positiveConsensuses.addAll(
-                    relevantDescriptors.get(descriptor));
-              }
-            }
-          }
-          br.close();
-          if (foundMatch) {
-            out.println("</code></pre>");
-          }
-        } catch (IOException e) {
-          /* Could not read descriptor string. */
-          continue;
-        }
-      }
-    }
-
-    /* Print out result. */
-    inMostRelevantConsensuses = false;
-    inOtherRelevantConsensus = false;
-    inTooOldConsensuses = false;
-    inTooNewConsensuses = false;
-    for (long match : positiveConsensuses) {
-      if (timestampIsDate &&
-          dateFormat.format(match).equals(timestampStr)) {
-        inMostRelevantConsensuses = true;
-      } else if (!timestampIsDate && match == relevantConsensuses.last()) {
-        inMostRelevantConsensuses = true;
-      } else if (relevantConsensuses.contains(match)) {
-        inOtherRelevantConsensus = true;
-      } else if (tooOldConsensuses.contains(match)) {
-        inTooOldConsensuses = true;
-      } else if (tooNewConsensuses.contains(match)) {
-        inTooNewConsensuses = true;
-      }
-    }
-    if (inMostRelevantConsensuses) {
-      out.print("        <p>Result is POSITIVE with high certainty!"
-            + "</p>\n"
-          + "        <p>We found one or more relays on IP address "
-          + relayIP + " permitting exit to " + target + " in ");
-      if (timestampIsDate) {
-        out.print("relay list published on " + timestampStr);
-      } else {
-        out.print("the most recent relay list preceding " + timestampStr);
-      }
-      out.print(" that clients were likely to know.</p>\n");
-      writeFooter(out);
-      try {
-        conn.close();
-        this.logger.info("Returned a database connection to the pool "
-            + "after " + (System.currentTimeMillis()
-            - requestedConnection) + " millis.");
-      } catch (SQLException e) {
-      }
-      return;
-    }
-    boolean resultIndecisive = target.length() > 0
-        && !missingDescriptors.isEmpty();
-    if (resultIndecisive) {
-      out.println("        <p>Result is INDECISIVE!</p>\n"
-          + "        <p>At least one referenced descriptor could not be "
-          + "found. This is a rare case, but one that (apparently) "
-          + "happens. We cannot make any good statement about exit "
-          + "relays without these descriptors. The following descriptors "
-          + "are missing:</p>");
-      for (String desc : missingDescriptors)
-        out.println("        <p>" + desc + "</p>\n");
-    }
-    if (inOtherRelevantConsensus) {
-      if (!resultIndecisive) {
-        out.println("        <p>Result is POSITIVE "
-            + "with moderate certainty!</p>\n");
-      }
-      out.println("<p>We found one or more relays on IP address "
-          + relayIP + " permitting exit to " + target + ", but not in ");
-      if (timestampIsDate) {
-        out.print("a relay list published on " + timestampStr);
-      } else {
-        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 authorities "
-          + "had difficulties connecting to the relay. However, clients "
-          + "might still have used the relay.</p>\n");
-    } else {
-      if (!resultIndecisive) {
-        out.println("        <p>Result is NEGATIVE "
-            + "with high certainty!</p>\n");
-      }
-      out.println("        <p>We did not find any relay on IP address "
-          + relayIP + " permitting exit to " + target
-          + " in the relay list 3 hours preceding " + timestampStr
-          + ".</p>\n");
-      if (inTooOldConsensuses || inTooNewConsensuses) {
-        if (inTooOldConsensuses && !inTooNewConsensuses) {
-          out.println("        <p>Note that we found a matching relay in "
-              + "relay lists that were published between 15 and 3 "
-              + "hours before " + timestampStr + ".</p>\n");
-        } else if (!inTooOldConsensuses && inTooNewConsensuses) {
-          out.println("        <p>Note that we found a matching relay in "
-              + "relay lists that were published up to 12 hours after "
-              + timestampStr + ".</p>\n");
-        } else {
-          out.println("        <p>Note that we found a matching relay in "
-              + "relay lists that were published between 15 and 3 "
-              + "hours before and in relay lists that were published up "
-              + "to 12 hours after " + timestampStr + ".</p>\n");
-        }
-        if (timestampIsDate) {
-          out.println("<p>Be sure to try out the previous/next day or "
-              + "provide an exact timestamp in UTC.</p>");
-        } else {
-          out.println("<p>Make sure that the timestamp you provided is "
-              + "correctly converted to the UTC timezone.</p>");
-        }
-      }
-    }
-    if (target != null) {
-      if (positiveConsensuses.isEmpty() &&
-          !positiveConsensusesNoTarget.isEmpty()) {
-        out.println("        <p>Note that although the found relay(s) did "
-            + "not permit exiting to " + target + ", there have been one "
-            + "or more relays running at the given time.</p>");
-      }
-    }
-    try {
-      conn.close();
-      this.logger.info("Returned a database connection to the pool "
-          + "after " + (System.currentTimeMillis()
-          - requestedConnection) + " millis.");
-    } catch (SQLException e) {
-    }
-    writeFooter(out);
-  }
-}
-
diff --git a/src/org/torproject/ernie/web/ExoneraTorServlet.java b/src/org/torproject/ernie/web/ExoneraTorServlet.java
index aa46367..d2d33f4 100644
--- a/src/org/torproject/ernie/web/ExoneraTorServlet.java
+++ b/src/org/torproject/ernie/web/ExoneraTorServlet.java
@@ -30,7 +30,7 @@ public class ExoneraTorServlet extends HttpServlet {
     /* Look up data source. */
     try {
       Context cxt = new InitialContext();
-      this.ds = (DataSource) cxt.lookup("java:comp/env/jdbc/tordir");
+      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);
@@ -86,7 +86,7 @@ public class ExoneraTorServlet extends HttpServlet {
           + "to a given server and/or TCP port. ExoneraTor learns about "
           + "these facts from parsing the public relay lists and relay "
           + "descriptors that are collected from the Tor directory "
-          + "authorities.</p>\n"
+          + "authorities and the exit lists collected by TorDNSEL.</p>\n"
         + "        <br>\n"
         + "        <p><font color=\"red\"><b>Notice:</b> Note that the "
           + "information you are providing below may be leaked to anyone "
@@ -136,11 +136,24 @@ public class ExoneraTorServlet extends HttpServlet {
     PrintWriter out = response.getWriter();
     writeHeader(out);
 
+    /* Open a database connection that we'll use to handle the whole
+     * request. */
+    Connection conn = null;
+    long requestedConnection = System.currentTimeMillis();
+    try {
+      conn = this.ds.getConnection();
+    } catch (SQLException e) {
+      out.println("<p><font color=\"red\"><b>Warning: </b></font>Unable "
+          + "to connect to the database. If this problem persists, "
+          + "please <a href=\"mailto:tor-assistants at torproject.org\">let "
+          + "us know</a>!</p>\n");
+      writeFooter(out);
+      return;
+    }
+
     /* Look up first and last consensus in the database. */
     long firstValidAfter = -1L, lastValidAfter = -1L;
     try {
-      long requestedConnection = System.currentTimeMillis();
-      Connection conn = this.ds.getConnection();
       Statement statement = conn.createStatement();
       String query = "SELECT MIN(validafter) AS first, "
           + "MAX(validafter) AS last FROM consensus";
@@ -151,10 +164,6 @@ public class ExoneraTorServlet extends HttpServlet {
       }
       rs.close();
       statement.close();
-      conn.close();
-      this.logger.info("Returned a database connection to the pool after "
-          + (System.currentTimeMillis() - requestedConnection)
-          + " millis.");
     } catch (SQLException e) {
       /* Looks like we don't have any consensuses. */
     }
@@ -165,6 +174,13 @@ public class ExoneraTorServlet extends HttpServlet {
           + "<a href=\"mailto:tor-assistants at torproject.org\">let us "
           + "know</a>!</p>\n");
       writeFooter(out);
+      try {
+        conn.close();
+        this.logger.info("Returned a database connection to the pool "
+            + "after " + (System.currentTimeMillis()
+            - requestedConnection) + " millis.");
+      } catch (SQLException e) {
+      }
       return;
     }
 
@@ -172,6 +188,7 @@ 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(
         "^([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." +
         "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." +
@@ -198,19 +215,29 @@ public class ExoneraTorServlet extends HttpServlet {
     /* Parse timestamp parameter. */
     String timestampParameter = request.getParameter("timestamp");
     long timestamp = 0L;
+    boolean timestampIsDate = false;
     String timestampStr = "", timestampWarning = "";
     SimpleDateFormat shortDateTimeFormat = new SimpleDateFormat(
         "yyyy-MM-dd HH:mm");
     shortDateTimeFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
+    dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
     if (timestampParameter != null && timestampParameter.length() > 0) {
       try {
-        timestamp = shortDateTimeFormat.parse(timestampParameter).
-            getTime();
-        timestampStr = shortDateTimeFormat.format(timestamp);
+        if (timestampParameter.split(" ").length == 1) {
+          timestamp = dateFormat.parse(timestampParameter).getTime();
+          timestampStr = dateFormat.format(timestamp);
+          timestampIsDate = true;
+        } else {
+          timestamp = shortDateTimeFormat.parse(timestampParameter).
+              getTime();
+          timestampStr = shortDateTimeFormat.format(timestamp);
+        }
         if (timestamp < firstValidAfter || timestamp > lastValidAfter) {
-          timestampWarning = "Please pick a value between \""
+          timestampWarning = "Please pick a date or timestamp between \""
               + shortDateTimeFormat.format(firstValidAfter) + "\" and \""
               + shortDateTimeFormat.format(lastValidAfter) + "\".";
+          timestamp = 0L;
         }
       } catch (ParseException e) {
         /* We have no way to handle this exception, other than leaving
@@ -219,7 +246,7 @@ public class ExoneraTorServlet extends HttpServlet {
             StringEscapeUtils.escapeHtml(timestampParameter.
             substring(0, 20)) + "[...]" :
             StringEscapeUtils.escapeHtml(timestampParameter))
-            + "\" is not a valid timestamp.";
+            + "\" is not a valid date or timestamp.";
       }
     }
 
@@ -229,9 +256,9 @@ public class ExoneraTorServlet extends HttpServlet {
         ipWarning.length() < 1) {
       ipWarning = "Please provide an IP address.";
     }
-    if (relayIP.length() > 0 && timestampStr.length() < 1 &&
+    if (relayIP.length() > 0 && timestamp < 1 &&
         timestampWarning.length() < 1) {
-      timestampWarning = "Please provide a timestamp.";
+      timestampWarning = "Please provide a date or timestamp.";
     }
 
     /* Parse target IP parameter. */
@@ -284,13 +311,14 @@ public class ExoneraTorServlet extends HttpServlet {
 
     /* If target port is provided, a target address must be provided,
      * too. */
+    /* TODO Relax this requirement. */
     if (targetPort.length() > 0 && targetIP.length() < 1 &&
         targetAddrWarning.length() < 1) {
       targetAddrWarning = "Please provide an IP address.";
     }
 
     /* Write form with IP address and timestamp. */
-    out.println("        <form action=\"exonerator.html#relay\">\n"
+    out.println("        <form action=\"#relay\">\n"
         + "          <input type=\"hidden\" name=\"targetaddr\" "
         + (targetIP.length() > 0 ? " value=\"" + targetIP + "\"" : "")
         + ">\n"
@@ -311,7 +339,8 @@ public class ExoneraTorServlet extends HttpServlet {
         + "              <td><i>(Ex.: 1.2.3.4)</i></td>\n"
         + "            </tr>\n"
         + "            <tr>\n"
-        + "              <td align=\"right\">Timestamp, in UTC:</td>\n"
+        + "              <td align=\"right\">Date or timestamp, in "
+          + "UTC:</td>\n"
         + "              <td><input type=\"text\" name=\"timestamp\""
           + (timestampStr.length() > 0 ? " value=\"" + timestampStr + "\""
             : "")
@@ -319,7 +348,8 @@ public class ExoneraTorServlet extends HttpServlet {
           + (timestampWarning.length() > 0 ? "<br><font color=\"red\">"
               + timestampWarning + "</font>" : "")
         + "</td>\n"
-        + "              <td><i>(Ex.: 2010-01-01 12:00)</i></td>\n"
+        + "              <td><i>(Ex.: 2010-01-01 or 2010-01-01 12:00)"
+          + "</i></td>\n"
         + "            </tr>\n"
         + "            <tr>\n"
         + "              <td></td>\n"
@@ -332,191 +362,278 @@ public class ExoneraTorServlet extends HttpServlet {
         + "          </table>\n"
         + "        </form>\n");
 
-    if (relayIP.length() < 1 || timestampStr.length() < 1) {
+    if (relayIP.length() < 1 || timestamp < 1) {
       writeFooter(out);
+      try {
+        conn.close();
+        this.logger.info("Returned a database connection to the pool "
+            + "after " + (System.currentTimeMillis()
+            - requestedConnection) + " millis.");
+      } catch (SQLException e) {
+      }
       return;
     }
 
-    /* Look up relevant consensuses. */
-    long timestampTooOld = timestamp - 15L * 60L * 60L * 1000L;
-    long timestampFrom = timestamp - 3L * 60L * 60L * 1000L;
-    long timestampTooNew = timestamp + 12L * 60L * 60L * 1000L;
-    out.printf("<p>Looking up IP address %s in the relay lists published "
-        + "between %s and %s. "
-        + "Clients could have used any of these relay lists to "
-        + "select relays for their paths and build circuits using them. "
+    out.printf("<p>Looking up IP address %s in the relay lists "
+        + "published ", relayIP);
+    long timestampFrom, timestampTo;
+    if (timestampIsDate) {
+      /* If we only have a date, consider all consensuses published on the
+       * given date, plus the ones published 3 hours before the given date
+       * and until 23:59:59. */
+      timestampFrom = timestamp - 3L * 60L * 60L * 1000L;
+      timestampTo = timestamp + (24L * 60L * 60L - 1L) * 1000L;
+      out.printf("on %s", timestampStr);
+    } else {
+      /* If we have an exact timestamp, consider the consensuses published
+       * in the 3 hours preceding the UTC timestamp. */
+      timestampFrom = timestamp - 3L * 60L * 60L * 1000L;
+      timestampTo = timestamp;
+      out.printf("between %s and %s UTC",
+        shortDateTimeFormat.format(timestampFrom),
+        shortDateTimeFormat.format(timestampTo));
+    }
+    /* If we don't find any relays in the given time interval, also look
+     * at consensuses published 12 hours before and 12 hours after the
+     * interval, in case the user got the "UTC" bit wrong. */
+    long timestampTooOld = timestampFrom - 12L * 60L * 60L * 1000L;
+    long timestampTooNew = timestampTo + 12L * 60L * 60L * 1000L;
+    out.print(" as well as in the relevant exit lists. Clients could "
+        + "have selected any of these relays to build circuits. "
         + "You may follow the links to relay lists and relay descriptors "
         + "to grep for the lines printed below and confirm that results "
-        + "are correct.<br>", relayIP,
-        shortDateTimeFormat.format(timestampFrom), timestampStr);
+        + "are correct.<br>");
     SimpleDateFormat validAfterTimeFormat = new SimpleDateFormat(
         "yyyy-MM-dd HH:mm:ss");
     validAfterTimeFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
     String fromValidAfter = validAfterTimeFormat.format(timestampTooOld);
     String toValidAfter = validAfterTimeFormat.format(timestampTooNew);
-    SortedMap<Long, String> tooOldConsensuses =
-        new TreeMap<Long, String>();
-    SortedMap<Long, String> relevantConsensuses =
-        new TreeMap<Long, String>();
-    SortedMap<Long, String> tooNewConsensuses =
-        new TreeMap<Long, String>();
+    SortedSet<Long> tooOldConsensuses = new TreeSet<Long>();
+    SortedSet<Long> relevantConsensuses = new TreeSet<Long>();
+    SortedSet<Long> tooNewConsensuses = new TreeSet<Long>();
     try {
-      long requestedConnection = System.currentTimeMillis();
-      Connection conn = this.ds.getConnection();
       Statement statement = conn.createStatement();
-      String query = "SELECT validafter, rawdesc FROM consensus "
+      String query = "SELECT validafter FROM consensus "
           + "WHERE validafter >= '" + fromValidAfter
           + "' AND validafter <= '" + toValidAfter + "'";
       ResultSet rs = statement.executeQuery(query);
       while (rs.next()) {
         long consensusTime = rs.getTimestamp(1).getTime();
-        String rawConsensusString = new String(rs.getBytes(2), "US-ASCII");
         if (consensusTime < timestampFrom) {
-          tooOldConsensuses.put(consensusTime, rawConsensusString);
-        } else if (consensusTime > timestamp) {
-          tooNewConsensuses.put(consensusTime, rawConsensusString);
+          tooOldConsensuses.add(consensusTime);
+        } else if (consensusTime > timestampTo) {
+          tooNewConsensuses.add(consensusTime);
         } else {
-          relevantConsensuses.put(consensusTime, rawConsensusString);
+          relevantConsensuses.add(consensusTime);
         }
       }
       rs.close();
       statement.close();
-      conn.close();
-      this.logger.info("Returned a database connection to the pool after "
-          + (System.currentTimeMillis() - requestedConnection)
-          + " millis.");
     } catch (SQLException e) {
       /* Looks like we don't have any consensuses in the requested
-         interval. */
+       * interval. */
     }
-    SortedMap<Long, String> allConsensuses = new TreeMap<Long, String>();
-    allConsensuses.putAll(tooOldConsensuses);
-    allConsensuses.putAll(relevantConsensuses);
-    allConsensuses.putAll(tooNewConsensuses);
+    SortedSet<Long> allConsensuses = new TreeSet<Long>();
+    allConsensuses.addAll(tooOldConsensuses);
+    allConsensuses.addAll(relevantConsensuses);
+    allConsensuses.addAll(tooNewConsensuses);
     if (allConsensuses.isEmpty()) {
       out.println("        <p>No relay lists found!</p>\n"
           + "        <p>Result is INDECISIVE!</p>\n"
           + "        <p>We cannot make any statement whether there was "
-          + "a Tor relay running on IP address "
-          + relayIP + " at " + timestampStr + "! We "
-          + "did not find any relevant relay lists preceding the given "
-          + "time. If you think this is an error on our side, please "
+          + "a Tor relay running on IP address " + relayIP
+          + (timestampIsDate ? " on " : " at ") + timestampStr + "! We "
+          + "did not find any relevant relay lists at the given time. If "
+          + "you think this is an error on our side, please "
           + "<a href=\"mailto:tor-assistants at torproject.org\">contact "
           + "us</a>!</p>\n");
       writeFooter(out);
+      try {
+        conn.close();
+        this.logger.info("Returned a database connection to the pool "
+            + "after " + (System.currentTimeMillis()
+            - requestedConnection) + " millis.");
+      } catch (SQLException e) {
+      }
       return;
     }
 
-    /* Parse consensuses to find descriptors belonging to the IP
-       address. */
+    /* 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. */
+    SortedMap<Long, SortedMap<String, String>> statusEntries =
+        new TreeMap<Long, SortedMap<String, String>>();
     SortedSet<Long> positiveConsensusesNoTarget = new TreeSet<Long>();
-    Set<String> addressesInSameNetwork = new HashSet<String>();
     SortedMap<String, Set<Long>> relevantDescriptors =
         new TreeMap<String, Set<Long>>();
+    try {
+      CallableStatement cs = conn.prepareCall(
+          "{call search_statusentries_by_address_date(?, ?)}");
+      cs.setString(1, relayIP);
+      cs.setDate(2, new java.sql.Date(timestamp));
+      ResultSet rs = cs.executeQuery();
+      while (rs.next()) {
+        byte[] rawstatusentry = rs.getBytes(1);
+        String descriptor = rs.getString(2);
+        long validafter = rs.getTimestamp(3).getTime();
+        positiveConsensusesNoTarget.add(validafter);
+        if (!relevantDescriptors.containsKey(descriptor)) {
+          relevantDescriptors.put(descriptor, new HashSet<Long>());
+        }
+        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";
+        if (exitaddress != null && exitaddress.length() > 0) {
+          long scanned = rs.getTimestamp(7).getTime();
+          htmlString += "  [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);
+      }
+      rs.close();
+      cs.close();
+    } catch (SQLException e) {
+      /* Nothing found. */
+    }
+
+    /* Print out what we found. */
     SimpleDateFormat validAfterUrlFormat = new SimpleDateFormat(
         "yyyy-MM-dd-HH-mm-ss");
     validAfterUrlFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
-    for (Map.Entry<Long, String> e : allConsensuses.entrySet()) {
-      long consensus = e.getKey();
-      if (relevantConsensuses.containsKey(consensus)) {
-        long validAfterTime = -1L;
+    out.print("<pre><code>");
+    for (long consensus : allConsensuses) {
+      if (relevantConsensuses.contains(consensus)) {
         String validAfterDatetime = validAfterTimeFormat.format(
             consensus);
         String validAfterString = validAfterUrlFormat.format(consensus);
-        out.println("        <br><tt>valid-after <b>"
+        out.print("valid-after <b>"
             + "<a href=\"consensus?valid-after="
             + validAfterString + "\" target=\"_blank\">"
-            + validAfterDatetime + "</b></a></tt><br>");
-      }
-      String rawConsensusString = e.getValue();
-      BufferedReader br = new BufferedReader(new StringReader(
-          rawConsensusString));
-      String line = null;
-      while ((line = br.readLine()) != null) {
-        if (!line.startsWith("r ")) {
-          continue;
-        }
-        String[] parts = line.split(" ");
-        String address = parts[6];
-        if (address.equals(relayIP)) {
-          String hex = String.format("%040x", new BigInteger(1,
-              Base64.decodeBase64(parts[3] + "==")));
-          if (!relevantDescriptors.containsKey(hex)) {
-            relevantDescriptors.put(hex, new HashSet<Long>());
-          }
-          relevantDescriptors.get(hex).add(consensus);
-          positiveConsensusesNoTarget.add(consensus);
-          if (relevantConsensuses.containsKey(consensus)) {
-            out.println("    <tt>r " + parts[1] + " " + parts[2] + " "
-                + "<a href=\"serverdesc?desc-id=" + hex + "\" "
-                + "target=\"_blank\">" + parts[3] + "</a> " + parts[4]
-                + " " + parts[5] + " <b>" + parts[6] + "</b> " + parts[7]
-                + " " + parts[8] + "</tt><br>");
-          }
-        } else {
-          if (relayIP.startsWith(address.substring(0,
-              address.lastIndexOf(".")))) {
-            addressesInSameNetwork.add(address);
+            + validAfterDatetime + "</b></a>\n");
+        if (statusEntries.containsKey(consensus)) {
+          for (String htmlString :
+              statusEntries.get(consensus).values()) {
+            out.print(htmlString);
           }
         }
+        out.print("\n");
       }
-      br.close();
     }
+    out.print("</code></pre>");
     if (relevantDescriptors.isEmpty()) {
       out.printf("        <p>None found!</p>\n"
-          + "        <p>Result is NEGATIVE with moderate certainty!</p>\n"
+          + "        <p>Result is NEGATIVE with high certainty!</p>\n"
           + "        <p>We did not find IP "
-          + "address " + relayIP + " in any of the relay lists that were "
-          + "published between %s and %s.\n\nA possible "
-          + "reason for false negatives is that the relay is using a "
-          + "different IP address when generating a descriptor than for "
-          + "exiting to the Internet. We hope to provide better checks "
-          + "for this case in the future.</p>\n",
-          shortDateTimeFormat.format(timestampTooOld),
-          shortDateTimeFormat.format(timestampTooNew));
+          + "address " + relayIP + " in any of the relay or exit lists "
+          + "that were published between %s and %s.</p>\n",
+          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()) {
+          Map<String, String> resultEntry = new HashMap<String, String>();
+          String address = rs.getString(1);
+          addressesInSameNetwork.add(address);
+        }
+        rs.close();
+        cs.close();
+      } catch (SQLException e) {
+        /* No other addresses in the same /24 found. */
+      }
       if (!addressesInSameNetwork.isEmpty()) {
-        out.println("        <p>The following other IP addresses of Tor "
-            + "relays were found in the mentioned relay lists that "
-            + "are in the same /24 network and that could be related to "
-            + "IP address " + relayIP + ":</p>\n");
+        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");
+        out.print("        <ul>\n");
         for (String s : addressesInSameNetwork) {
-          out.println("        <p>" + s + "</p>\n");
+          out.print("        <li>" + s + "</li>\n");
         }
+        out.print("        </ul>\n");
       }
       writeFooter(out);
+      try {
+        conn.close();
+        this.logger.info("Returned a database connection to the pool "
+            + "after " + (System.currentTimeMillis()
+            - requestedConnection) + " millis.");
+      } catch (SQLException e) {
+      }
       return;
     }
 
     /* Print out result. */
-    Set<Long> matches = positiveConsensusesNoTarget;
-    if (matches.contains(relevantConsensuses.lastKey())) {
-      out.println("        <p>Result is POSITIVE with high certainty!"
+    boolean inMostRelevantConsensuses = false,
+        inOtherRelevantConsensus = false,
+        inTooOldConsensuses = false,
+        inTooNewConsensuses = false;
+    for (long match : positiveConsensusesNoTarget) {
+      if (timestampIsDate &&
+          dateFormat.format(match).equals(timestampStr)) {
+        inMostRelevantConsensuses = true;
+      } else if (!timestampIsDate &&
+          match == relevantConsensuses.last()) {
+        inMostRelevantConsensuses = true;
+      } else if (relevantConsensuses.contains(match)) {
+        inOtherRelevantConsensus = true;
+      } else if (tooOldConsensuses.contains(match)) {
+        inTooOldConsensuses = true;
+      } else if (tooNewConsensuses.contains(match)) {
+        inTooNewConsensuses = true;
+      }
+    }
+    if (inMostRelevantConsensuses) {
+      out.print("        <p>Result is POSITIVE with high certainty!"
             + "</p>\n"
           + "        <p>We found one or more relays on IP address "
-          + relayIP
-          + " in the most recent relay list preceding " + timestampStr
-          + " that clients were likely to know.</p>\n");
-    } else {
-      boolean inOtherRelevantConsensus = false,
-          inTooOldConsensuses = false,
-          inTooNewConsensuses = false;
-      for (long match : matches) {
-        if (relevantConsensuses.containsKey(match)) {
-          inOtherRelevantConsensus = true;
-        } else if (tooOldConsensuses.containsKey(match)) {
-          inTooOldConsensuses = true;
-        } else if (tooNewConsensuses.containsKey(match)) {
-          inTooNewConsensuses = true;
-        }
+          + relayIP + " in ");
+      if (timestampIsDate) {
+        out.print("relay list published on " + timestampStr);
+      } else {
+        out.print("the most recent relay list preceding " + timestampStr);
       }
+      out.print(" that clients were likely to know.</p>\n");
+    } else {
       if (inOtherRelevantConsensus) {
         out.println("        <p>Result is POSITIVE "
             + "with moderate certainty!</p>\n");
         out.println("<p>We found one or more relays on IP address "
-            + relayIP + ", but not in the relay list immediately "
-            + "preceding " + timestampStr + ". A possible reason for the "
-            + "relay being missing in the last relay list preceding the "
-            + "given time might be that some of the directory "
+            + relayIP + ", but not in ");
+        if (timestampIsDate) {
+          out.print("a relay list published on " + timestampStr);
+        } else {
+          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 "
             + "authorities had difficulties connecting to the relay. "
             + "However, clients might still have used the relay.</p>\n");
       } else {
@@ -529,22 +646,36 @@ public class ExoneraTorServlet extends HttpServlet {
         if (inTooOldConsensuses || inTooNewConsensuses) {
           if (inTooOldConsensuses && !inTooNewConsensuses) {
             out.println("        <p>Note that we found a matching relay "
-                + "in relay lists that were published between 5 and 3 "
+                + "in relay lists that were published between 15 and 3 "
                 + "hours before " + timestampStr + ".</p>\n");
           } else if (!inTooOldConsensuses && inTooNewConsensuses) {
             out.println("        <p>Note that we found a matching relay "
-                + "in relay lists that were published up to 2 hours "
+                + "in relay lists that were published up to 12 hours "
                 + "after " + timestampStr + ".</p>\n");
           } else {
             out.println("        <p>Note that we found a matching relay "
-                + "in relay lists that were published between 5 and 3 "
+                + "in relay lists that were published between 15 and 3 "
                 + "hours before and in relay lists that were published "
-                + "up to 2 hours after " + timestampStr + ".</p>\n");
+                + "up to 12 hours after " + timestampStr + ".</p>\n");
+          }
+          if (timestampIsDate) {
+            out.println("<p>Be sure to try out the previous/next day or "
+                + "provide an exact timestamp in UTC.</p>");
+          } else {
+            out.println("<p>Make sure that the timestamp you "
+                + "provided is correctly converted to the UTC "
+                + "timezone.</p>");
           }
-          out.println("<p>Make sure that the timestamp you provided is "
-              + "in the correct timezone: UTC (or GMT).</p>");
         }
+        /* We didn't find any descriptor.  No need to look up targets. */
         writeFooter(out);
+        try {
+          conn.close();
+          this.logger.info("Returned a database connection to the pool "
+              + "after " + (System.currentTimeMillis()
+              - requestedConnection) + " millis.");
+        } catch (SQLException e) {
+        }
         return;
       }
     }
@@ -553,7 +684,7 @@ public class ExoneraTorServlet extends HttpServlet {
     out.println("<br><a name=\"exit\"></a><h3>Was this relay configured "
         + "to permit exiting to a given target?</h3>");
 
-    out.println("        <form action=\"exonerator.html#exit\">\n"
+    out.println("        <form action=\"#exit\">\n"
         + "              <input type=\"hidden\" name=\"timestamp\"\n"
         + "                         value=\"" + timestampStr + "\">\n"
         + "              <input type=\"hidden\" name=\"ip\" "
@@ -593,6 +724,13 @@ public class ExoneraTorServlet extends HttpServlet {
 
     if (targetIP.length() < 1) {
       writeFooter(out);
+      try {
+        conn.close();
+        this.logger.info("Returned a database connection to the pool "
+            + "after " + (System.currentTimeMillis()
+            - requestedConnection) + " millis.");
+      } catch (SQLException e) {
+      }
       return;
     }
 
@@ -608,28 +746,23 @@ public class ExoneraTorServlet extends HttpServlet {
     for (String descriptor : descriptors) {
       byte[] rawDescriptor = null;
       try {
-        long requestedConnection = System.currentTimeMillis();
-        Connection conn = this.ds.getConnection();
-        Statement statement = conn.createStatement();
-        String query = "SELECT rawdesc FROM descriptor "
+        String query = "SELECT rawdescriptor FROM descriptor "
             + "WHERE descriptor = '" + descriptor + "'";
+        Statement statement = conn.createStatement();
         ResultSet rs = statement.executeQuery(query);
         if (rs.next()) {
           rawDescriptor = rs.getBytes(1);
         }
         rs.close();
         statement.close();
-        conn.close();
-        this.logger.info("Returned a database connection to the pool "
-            + "after " + (System.currentTimeMillis()
-            - requestedConnection) + " millis.");
       } catch (SQLException e) {
         /* Consider this descriptors as 'missing'. */
         continue;
       }
       if (rawDescriptor != null && rawDescriptor.length > 0) {
         missingDescriptors.remove(descriptor);
-        String rawDescriptorString = new String(rawDescriptor, "US-ASCII");
+        String rawDescriptorString = new String(rawDescriptor,
+            "US-ASCII");
         try {
           BufferedReader br = new BufferedReader(
               new StringReader(rawDescriptorString));
@@ -644,7 +777,7 @@ public class ExoneraTorServlet extends HttpServlet {
             } else if (line.startsWith("reject ") ||
                 line.startsWith("accept ")) {
               if (foundMatch) {
-                out.println("<tt> " + line + "</tt><br>");
+                out.println(line);
                 continue;
               }
               boolean ruleAccept = line.split(" ")[0].equals("accept");
@@ -653,7 +786,7 @@ public class ExoneraTorServlet extends HttpServlet {
                 if (!ruleAddress.contains("/") &&
                     !ruleAddress.equals(targetIP)) {
                   /* IP address does not match. */
-                  acceptRejectLines.append("<tt> " + line + "</tt><br>\n");
+                  acceptRejectLines.append(line + "\n");
                   continue;
                 }
                 String[] ruleIPParts = ruleAddress.split("/")[0].
@@ -680,7 +813,7 @@ public class ExoneraTorServlet extends HttpServlet {
                 }
                 if (ruleNetwork > 0) {
                   /* IP address does not match. */
-                  acceptRejectLines.append("<tt> " + line + "</tt><br>\n");
+                  acceptRejectLines.append(line + "\n");
                   continue;
                 }
               }
@@ -689,7 +822,7 @@ public class ExoneraTorServlet extends HttpServlet {
                   !rulePort.equals("*")) {
                 /* With no port given, we only consider reject :* rules as
                    matching. */
-                acceptRejectLines.append("<tt> " + line + "</tt><br>\n");
+                acceptRejectLines.append(line + "\n");
                 continue;
               }
               if (targetPort.length() > 0 && !rulePort.equals("*") &&
@@ -708,28 +841,28 @@ public class ExoneraTorServlet extends HttpServlet {
                     !rulePort.contains("-") &&
                     !targetPort.equals(rulePort)) {
                   /* Ports do not match. */
-                  acceptRejectLines.append("<tt> " + line + "</tt><br>\n");
+                  acceptRejectLines.append(line + "\n");
                   continue;
                 }
               }
               boolean relevantMatch = false;
               for (long match : relevantDescriptors.get(descriptor)) {
-                if (relevantConsensuses.containsKey(match)) {
+                if (relevantConsensuses.contains(match)) {
                   relevantMatch = true;
                 }
               }
               if (relevantMatch) {
                 String[] routerParts = routerLine.split(" ");
-                out.println("<br><tt>" + routerParts[0] + " "
+                out.println("<pre><code>" + routerParts[0] + " "
                     + routerParts[1] + " <b>" + routerParts[2] + "</b> "
                     + routerParts[3] + " " + routerParts[4] + " "
-                    + routerParts[5] + "</tt><br>");
+                    + routerParts[5]);
                 String[] publishedParts = publishedLine.split(" ");
-                out.println("<tt>" + publishedParts[0] + " <b>"
+                out.println(publishedParts[0] + " <b>"
                     + publishedParts[1] + " " + publishedParts[2]
-                    + "</b></tt><br>");
-                out.println(acceptRejectLines.toString());
-                out.println("<tt><b>" + line + "</b></tt><br>");
+                    + "</b>");
+                out.print(acceptRejectLines.toString());
+                out.println("<b>" + line + "</b>");
                 foundMatch = true;
               }
               if (ruleAccept) {
@@ -739,6 +872,9 @@ public class ExoneraTorServlet extends HttpServlet {
             }
           }
           br.close();
+          if (foundMatch) {
+            out.println("</code></pre>");
+          }
         } catch (IOException e) {
           /* Could not read descriptor string. */
           continue;
@@ -747,15 +883,43 @@ public class ExoneraTorServlet extends HttpServlet {
     }
 
     /* Print out result. */
-    matches = positiveConsensuses;
-    if (matches.contains(relevantConsensuses.lastKey())) {
-      out.println("        <p>Result is POSITIVE with high certainty!</p>"
-            + "\n"
+    inMostRelevantConsensuses = false;
+    inOtherRelevantConsensus = false;
+    inTooOldConsensuses = false;
+    inTooNewConsensuses = false;
+    for (long match : positiveConsensuses) {
+      if (timestampIsDate &&
+          dateFormat.format(match).equals(timestampStr)) {
+        inMostRelevantConsensuses = true;
+      } else if (!timestampIsDate && match == relevantConsensuses.last()) {
+        inMostRelevantConsensuses = true;
+      } else if (relevantConsensuses.contains(match)) {
+        inOtherRelevantConsensus = true;
+      } else if (tooOldConsensuses.contains(match)) {
+        inTooOldConsensuses = true;
+      } else if (tooNewConsensuses.contains(match)) {
+        inTooNewConsensuses = true;
+      }
+    }
+    if (inMostRelevantConsensuses) {
+      out.print("        <p>Result is POSITIVE with high certainty!"
+            + "</p>\n"
           + "        <p>We found one or more relays on IP address "
-          + relayIP + " permitting exit to " + target
-          + " in the most recent relay list preceding " + timestampStr
-          + " that clients were likely to know.</p>\n");
+          + relayIP + " permitting exit to " + target + " in ");
+      if (timestampIsDate) {
+        out.print("relay list published on " + timestampStr);
+      } else {
+        out.print("the most recent relay list preceding " + timestampStr);
+      }
+      out.print(" that clients were likely to know.</p>\n");
       writeFooter(out);
+      try {
+        conn.close();
+        this.logger.info("Returned a database connection to the pool "
+            + "after " + (System.currentTimeMillis()
+            - requestedConnection) + " millis.");
+      } catch (SQLException e) {
+      }
       return;
     }
     boolean resultIndecisive = target.length() > 0
@@ -770,30 +934,22 @@ public class ExoneraTorServlet extends HttpServlet {
       for (String desc : missingDescriptors)
         out.println("        <p>" + desc + "</p>\n");
     }
-    boolean inOtherRelevantConsensus = false, inTooOldConsensuses = false,
-        inTooNewConsensuses = false;
-    for (long match : matches) {
-      if (relevantConsensuses.containsKey(match)) {
-        inOtherRelevantConsensus = true;
-      } else if (tooOldConsensuses.containsKey(match)) {
-        inTooOldConsensuses = true;
-      } else if (tooNewConsensuses.containsKey(match)) {
-        inTooNewConsensuses = true;
-      }
-    }
     if (inOtherRelevantConsensus) {
       if (!resultIndecisive) {
         out.println("        <p>Result is POSITIVE "
             + "with moderate certainty!</p>\n");
       }
       out.println("<p>We found one or more relays on IP address "
-          + relayIP + " permitting exit to " + target + ", but not in "
-          + "the relay list immediately preceding " + timestampStr
-          + ". A possible reason for the relay being missing in the last "
-          + "relay list preceding the given time might be that some of "
-          + "the directory authorities had difficulties connecting to "
-          + "the relay. However, clients might still have used the "
-          + "relay.</p>\n");
+          + relayIP + " permitting exit to " + target + ", but not in ");
+      if (timestampIsDate) {
+        out.print("a relay list published on " + timestampStr);
+      } else {
+        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 authorities "
+          + "had difficulties connecting to the relay. However, clients "
+          + "might still have used the relay.</p>\n");
     } else {
       if (!resultIndecisive) {
         out.println("        <p>Result is NEGATIVE "
@@ -806,20 +962,25 @@ public class ExoneraTorServlet extends HttpServlet {
       if (inTooOldConsensuses || inTooNewConsensuses) {
         if (inTooOldConsensuses && !inTooNewConsensuses) {
           out.println("        <p>Note that we found a matching relay in "
-              + "relay lists that were published between 5 and 3 "
+              + "relay lists that were published between 15 and 3 "
               + "hours before " + timestampStr + ".</p>\n");
         } else if (!inTooOldConsensuses && inTooNewConsensuses) {
           out.println("        <p>Note that we found a matching relay in "
-              + "relay lists that were published up to 2 hours after "
+              + "relay lists that were published up to 12 hours after "
               + timestampStr + ".</p>\n");
         } else {
           out.println("        <p>Note that we found a matching relay in "
-              + "relay lists that were published between 5 and 3 "
+              + "relay lists that were published between 15 and 3 "
               + "hours before and in relay lists that were published up "
-              + "to 2 hours after " + timestampStr + ".</p>\n");
+              + "to 12 hours after " + timestampStr + ".</p>\n");
+        }
+        if (timestampIsDate) {
+          out.println("<p>Be sure to try out the previous/next day or "
+              + "provide an exact timestamp in UTC.</p>");
+        } else {
+          out.println("<p>Make sure that the timestamp you provided is "
+              + "correctly converted to the UTC timezone.</p>");
         }
-        out.println("<p>Make sure that the timestamp you provided is "
-            + "in the correct timezone: UTC (or GMT).</p>");
       }
     }
     if (target != null) {
@@ -830,8 +991,13 @@ public class ExoneraTorServlet extends HttpServlet {
             + "or more relays running at the given time.</p>");
       }
     }
-
-    /* Finish writing response. */
+    try {
+      conn.close();
+      this.logger.info("Returned a database connection to the pool "
+          + "after " + (System.currentTimeMillis()
+          - requestedConnection) + " millis.");
+    } catch (SQLException e) {
+    }
     writeFooter(out);
   }
 }



More information about the tor-commits mailing list