[tor-commits] [metrics-web/master] Excise ExoneraTor to make it a service of its own.

karsten at torproject.org karsten at torproject.org
Sun Dec 8 16:37:54 UTC 2013


commit 0a43ae9eba5aa70d4321cb3caf2b6dfeee6494f9
Author: Karsten Loesing <karsten.loesing at gmx.net>
Date:   Sun Dec 1 07:04:05 2013 +0100

    Excise ExoneraTor to make it a service of its own.
---
 config.template                                    |    7 -
 db/exonerator.sql                                  |  361 -------
 etc/context.xml.template                           |   10 -
 etc/web.xml                                        |   22 -
 .../ernie/status/exonerator/ConsensusServlet.java  |  124 ---
 .../exonerator/ExoneraTorDatabaseImporter.java     |  619 -----------
 .../ernie/status/exonerator/ExoneraTorServlet.java | 1140 +-------------------
 .../status/exonerator/ServerDescriptorServlet.java |  132 ---
 .../status/relaysearch/RelaySearchServlet.java     |    7 +-
 web/WEB-INF/exonerator.jsp                         |   45 +
 web/robots.txt                                     |    2 -
 11 files changed, 55 insertions(+), 2414 deletions(-)

diff --git a/config.template b/config.template
index 0491431..8f0789b 100644
--- a/config.template
+++ b/config.template
@@ -45,10 +45,3 @@
 ## Relative path to directory to import torperf results from
 #TorperfDirectory in/torperf/
 #
-## JDBC string for ExoneraTor database
-#ExoneraTorDatabaseJdbc jdbc:postgresql://localhost/exonerator?user=metrics&password=password
-#
-## Relative path to directory where to find descriptors to import into the
-## ExoneraTor database
-#ExoneraTorImportDirectory exonerator-import/
-
diff --git a/db/exonerator.sql b/db/exonerator.sql
deleted file mode 100755
index fd58531..0000000
--- a/db/exonerator.sql
+++ /dev/null
@@ -1,361 +0,0 @@
--- Copyright 2011 The Tor Project
--- See LICENSE for licensing information
-
--- The descriptor table holds server descriptors that we use for display
--- purposes and to parse exit policies.
-CREATE TABLE descriptor (
-
-  -- The 40-character lower-case hex string identifies a descriptor
-  -- uniquely and is used to join statusentry and this table.
-  descriptor CHARACTER(40) NOT NULL PRIMARY KEY,
-
-  -- The raw descriptor string is used for display purposes and to check
-  -- whether the relay allowed exiting to a given target or not.
-  rawdescriptor BYTEA NOT NULL
-);
-
--- The consensus table stores network status consensuses to be looked up
--- by valid-after time and displayed upon request.  A second purpose is
--- to learn quickly whether the database contains status entries for a
--- given day or not.
-CREATE TABLE consensus (
-
-  -- The unique valid-after time of the consensus.
-  validafter TIMESTAMP WITHOUT TIME ZONE NOT NULL PRIMARY KEY,
-
-  -- The raw consensus string for display purposes only.
-  rawconsensus BYTEA NOT NULL
-);
-
--- The statusentry table stores network status consensus entries listing
--- a relay as running at a certain point in time.  Only relays with the
--- Running flag shall be inserted into this table.  If a relay advertises
--- more than one IP address, there is a distinct entry for each address in
--- this table.  If a relay advertises more than one TCP port on the same
--- IP address, there is only a single entry in this table.
-CREATE TABLE statusentry (
-
-  -- The valid-after time of the consensus that contains this entry.
-  validafter TIMESTAMP WITHOUT TIME ZONE NOT NULL,
-
-  -- The 40-character lower-case hex string uniquely identifying the
-  -- relay.
-  fingerprint CHARACTER(40) NOT NULL,
-
-  -- The 40-character lower-case hex string that identifies the server
-  -- descriptor published by the relay.
-  descriptor CHARACTER(40) NOT NULL,
-
-  -- The most significant 3 bytes of the relay's onion routing IPv4
-  -- address in lower-case hex notation, or null if the relay's onion
-  -- routing address in this status entry is IPv6.  The purpose is to
-  -- quickly reduce query results for relays in the same /24 network.
-  oraddress24 CHARACTER(6),
-
-  -- The most significant 6 bytes of the relay's onion routing IPv6
-  -- address in lower-case hex notation, or null if the relay's onion
-  -- routing address in this status entry is IPv4.  The purpose is to
-  -- quickly reduce query results for relays in the same /48 network.
-  oraddress48 CHARACTER(12),
-
-  -- The relay's onion routing address.  Can be an IPv4 or an IPv6
-  -- address.  If a relay advertises more than one address, there are
-  -- multiple entries in this table for the same status entry.
-  oraddress INET NOT NULL,
-
-  -- The raw status entry string as contained in the network status
-  -- consensus for display purposes only.
-  rawstatusentry BYTEA NOT NULL,
-
-  -- A status entry is uniquely identified by its valid-after time, relay
-  -- fingerprint, and onion routing address.
-  CONSTRAINT statusentry_pkey
-      PRIMARY KEY (validafter, fingerprint, oraddress)
-);
-
--- The index on the exact onion routing address and on the valid-after
--- date is used to speed up ExoneraTor's query for status entries.
-CREATE INDEX statusentry_oraddress_validafterdate
-    ON statusentry (oraddress, DATE(validafter));
-
--- The index on the most significant 3 bytes of the relay's onion routing
--- address and on the valid-after date is used to speed up queries for
--- other relays in the same /24 network.
-CREATE INDEX statusentry_oraddress24_validafterdate
-    ON statusentry (oraddress24, DATE(validafter));
-
--- The index on the most significant 6 bytes of the relay's onion routing
--- address and on the valid-after date is used to speed up queries for
--- other relays in the same /48 network.
-CREATE INDEX statusentry_oraddress48_validafterdate
-    ON statusentry (oraddress48, DATE(validafter));
-
--- The exitlistentry table stores the results of the active testing,
--- DNS-based exit list for exit nodes.  An entry in this table means that
--- a relay was scanned at a given time and found to be exiting to the
--- Internet from a given IP address.  This IP address can be different
--- from the relay's onion routing address if the relay uses more than one
--- IP addresses.
-CREATE TABLE exitlistentry (
-
-  -- The 40-character lower-case hex string identifying the relay.
-  fingerprint CHARACTER(40) NOT NULL,
-
-  -- The most significant 3 bytes of the relay's exit IPv4 address in
-  -- lower-case hex notation, or null if the relay's exit address in this
-  -- entry is IPv6.  The purpose is to quickly reduce query results for
-  -- relays exiting from the same /24 network.
-  exitaddress24 CHARACTER(6),
-
-  -- The IP address that the relay uses for exiting to the Internet.  If
-  -- the relay uses more than one IP address, there are multiple entries
-  -- in this table.
-  exitaddress INET NOT NULL,
-
-  -- The time when the relay was scanned to find out its exit IP
-  -- address(es).
-  scanned TIMESTAMP WITHOUT TIME ZONE NOT NULL,
-
-  -- The raw exit list entry containing all scan results for a given relay
-  -- for display purposes.
-  rawexitlistentry BYTEA NOT NULL,
-
-  -- An exit list entry is uniquely identified by its scan time, relay
-  -- fingerprint, and exit address.
-  CONSTRAINT exitlistentry_pkey
-      PRIMARY KEY (scanned, fingerprint, exitaddress)
-);
-
--- The index on the exact exit address and on the valid-after date is used
--- to speed up ExoneraTor's query for status entries referencing exit list
--- entries.
-CREATE INDEX exitlistentry_exitaddress_scanneddate
-    ON exitlistentry (exitaddress, DATE(scanned));
-
--- The index on the most significant 3 bytes of the relay's exit address
--- and on the valid-after date is used to speed up queries for other
--- relays in the same /24 network.
-CREATE INDEX exitlistentry_exitaddress24_scanneddate
-    ON exitlistentry (exitaddress24, DATE(scanned));
-
--- Create the plpgsql language, so that we can use it below.
-CREATE LANGUAGE plpgsql;
-
--- Insert a server descriptor into the descriptor table.  Before doing so,
--- check that there is no descriptor with the same descriptor identifier
--- in the table yet.  Return 1 if the descriptor was inserted, 0
--- otherwise.
-CREATE OR REPLACE FUNCTION insert_descriptor (
-    insert_descriptor CHARACTER(40),
-    insert_rawdescriptor BYTEA)
-    RETURNS INTEGER AS $$
-  BEGIN
-    -- Look up if the descriptor is already contained in the descriptor
-    -- table.
-    IF (SELECT COUNT(*)
-        FROM descriptor
-        WHERE descriptor = insert_descriptor) = 0 THEN
-      -- Insert the descriptor and remember the new descriptorid to update
-      -- the foreign key in statusentry.
-      INSERT INTO descriptor (descriptor, rawdescriptor)
-          VALUES (insert_descriptor, insert_rawdescriptor);
-      -- Return 1 for a successfully inserted descriptor.
-      RETURN 1;
-    ELSE
-      -- Return 0 because we didn't change anything.
-      RETURN 0;
-    END IF;
-  END;
-$$ LANGUAGE 'plpgsql';
-
--- Insert a status entry into the statusentry table.  First check that
--- this status entry isn't contained in the table yet.  It's okay to
--- insert the same status entry multiple times for different IP addresses
--- though.  Return 1 if it was inserted, 0 otherwise.
-CREATE OR REPLACE FUNCTION insert_statusentry (
-    insert_validafter TIMESTAMP WITHOUT TIME ZONE,
-    insert_fingerprint CHARACTER(40),
-    insert_descriptor CHARACTER(40),
-    insert_oraddress24 CHARACTER(6),
-    insert_oraddress48 CHARACTER(12),
-    insert_oraddress TEXT,
-    insert_rawstatusentry BYTEA)
-    RETURNS INTEGER AS $$
-  BEGIN
-    -- Look up if the status entry is already contained in the statusentry
-    -- table.
-    IF (SELECT COUNT(*)
-        FROM statusentry
-        WHERE validafter = insert_validafter
-        AND fingerprint = insert_fingerprint
-        AND oraddress = insert_oraddress::INET) = 0 THEN
-      -- Insert the status entry.
-      INSERT INTO statusentry (validafter, fingerprint, descriptor,
-            oraddress24, oraddress48, oraddress, rawstatusentry)
-          VALUES (insert_validafter, insert_fingerprint,
-            insert_descriptor, insert_oraddress24, insert_oraddress48,
-            insert_oraddress::INET, insert_rawstatusentry);
-      -- Return 1 for a successfully inserted status entry.
-      RETURN 1;
-    ELSE
-      -- Return 0 because we already had this status entry.
-      RETURN 0;
-    END IF;
-  END;
-$$ LANGUAGE 'plpgsql';
-
--- Insert a consensus into the consensus table.  Check that the same
--- consensus has not been imported before.  Return 1 if it was inserted, 0
--- otherwise.
-CREATE OR REPLACE FUNCTION insert_consensus (
-    insert_validafter TIMESTAMP WITHOUT TIME ZONE,
-    insert_rawconsensus BYTEA)
-    RETURNS INTEGER AS $$
-  BEGIN
-    -- Look up if the consensus is already contained in the consensus
-    -- table.
-    IF (SELECT COUNT(*)
-        FROM consensus
-        WHERE validafter = insert_validafter) = 0 THEN
-      -- Insert the consensus.
-      INSERT INTO consensus (validafter, rawconsensus)
-          VALUES (insert_validafter, insert_rawconsensus);
-      -- Return 1 for a successful insert operation.
-      RETURN 1;
-    ELSE
-      -- Return 0 for not inserting the consensus.
-      RETURN 0;
-    END IF;
-  END;
-$$ LANGUAGE 'plpgsql';
-
--- Insert an exit list entry into the exitlistentry table.  Check that
--- this entry hasn't been inserted before.  It's okay to insert the same
--- exit list entry multiple times for different exit addresses.  Return 1
--- if the entry was inserted, 0 otherwise.
-CREATE OR REPLACE FUNCTION insert_exitlistentry (
-    insert_fingerprint CHARACTER(40),
-    insert_exitaddress24 CHARACTER(6),
-    insert_exitaddress TEXT,
-    insert_scanned TIMESTAMP WITHOUT TIME ZONE,
-    insert_rawexitlistentry BYTEA)
-    RETURNS INTEGER AS $$
-  BEGIN
-    IF (SELECT COUNT(*)
-        FROM exitlistentry
-        WHERE fingerprint = insert_fingerprint
-        AND exitaddress = insert_exitaddress::INET
-        AND scanned = insert_scanned) = 0 THEN
-      -- This exit list entry is not in the database yet.  Add it.
-      INSERT INTO exitlistentry (fingerprint, exitaddress24, exitaddress,
-            scanned, rawexitlistentry)
-          VALUES (insert_fingerprint, insert_exitaddress24,
-            insert_exitaddress::INET, insert_scanned,
-            insert_rawexitlistentry);
-      -- Return 1 for a successfully inserted exit list entry.
-      RETURN 1;
-    ELSE
-      -- Return 0 to show that we didn't add anything.
-      RETURN 0;
-    END IF;
-  END;
-$$ LANGUAGE 'plpgsql';
-
--- 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.
-CREATE OR REPLACE FUNCTION search_statusentries_by_address_date (
-    select_address TEXT,
-    select_date DATE)
-    RETURNS TABLE(rawstatusentry BYTEA,
-          descriptor CHARACTER(40),
-          validafter TIMESTAMP WITHOUT TIME ZONE,
-          fingerprint CHARACTER(40),
-          oraddress TEXT,
-          exitaddress TEXT,
-          scanned TIMESTAMP WITHOUT TIME ZONE) AS $$
-  -- The first select finds all status entries of relays with the given
-  -- IP address as onion routing address.
-  SELECT rawstatusentry,
-        descriptor,
-        validafter,
-        fingerprint,
-        HOST(oraddress),
-        NULL,
-        NULL
-      FROM statusentry
-      WHERE oraddress = $1::INET
-      AND DATE(validafter) >= $2 - 1
-      AND DATE(validafter) <= $2 + 1
-  UNION
-  -- The second select finds status entries of relays having an exit list
-  -- entry with the provided IP address as the exit address.
-  SELECT statusentry.rawstatusentry,
-        statusentry.descriptor,
-        statusentry.validafter,
-        statusentry.fingerprint,
-        HOST(statusentry.oraddress),
-        HOST(exitlistentry.exitaddress),
-        -- Pick only the last scan result that took place in the 24 hours
-        -- before the valid-after time.
-        MAX(exitlistentry.scanned)
-      FROM statusentry
-      JOIN exitlistentry
-      ON statusentry.fingerprint = exitlistentry.fingerprint
-      WHERE exitlistentry.exitaddress = $1::INET
-      -- Focus on a time period from 1 day before and 1 day after the
-      -- given date.  Also include a second day before the given date
-      -- for exit lists, because it can take up to 24 hours to scan a
-      -- relay again.  We shouldn't miss exit list entries here.
-      AND DATE(exitlistentry.scanned) >= $2 - 2
-      AND DATE(exitlistentry.scanned) <= $2 + 1
-      AND DATE(statusentry.validafter) >= $2 - 1
-      AND DATE(statusentry.validafter) <= $2 + 1
-      -- Consider only exit list scans that took place in the 24 hours
-      -- before the relay was listed in a consensus.
-      AND statusentry.validafter >= exitlistentry.scanned
-      AND statusentry.validafter - exitlistentry.scanned <=
-          '1 day'::INTERVAL
-      GROUP BY 1, 2, 3, 4, 5, 6
-  ORDER BY 3, 4, 6;
-$$ LANGUAGE SQL;
-
--- Look up all IPv4 OR and exit addresses in the /24 network of a given
--- address to suggest other addresses the user may be looking for.
-CREATE OR REPLACE FUNCTION search_addresses_in_same_24 (
-    select_address24 CHARACTER(6),
-    select_date DATE)
-    RETURNS TABLE(addresstext TEXT,
-          addressinet INET) AS $$
-  SELECT HOST(oraddress),
-        oraddress
-      FROM statusentry
-      WHERE oraddress24 = $1
-      AND DATE(validafter) >= $2 - 1
-      AND DATE(validafter) <= $2 + 1
-  UNION
-  SELECT HOST(exitaddress),
-        exitaddress
-      FROM exitlistentry
-      WHERE exitaddress24 = $1
-      AND DATE(scanned) >= $2 - 2
-      AND DATE(scanned) <= $2 + 1
-  ORDER BY 2;
-$$ LANGUAGE SQL;
-
--- Look up all IPv6 OR addresses in the /48 network of a given address to
--- suggest other addresses the user may be looking for.
-CREATE OR REPLACE FUNCTION search_addresses_in_same_48 (
-    select_address48 CHARACTER(12),
-    select_date DATE)
-    RETURNS TABLE(addresstext TEXT,
-          addressinet INET) AS $$
-  SELECT HOST(oraddress),
-        oraddress
-      FROM statusentry
-      WHERE oraddress48 = $1
-      AND DATE(validafter) >= $2 - 1
-      AND DATE(validafter) <= $2 + 1
-  ORDER BY 2;
-$$ LANGUAGE SQL;
-
diff --git a/etc/context.xml.template b/etc/context.xml.template
index 152f1de..00f14fe 100644
--- a/etc/context.xml.template
+++ b/etc/context.xml.template
@@ -1,14 +1,4 @@
 <Context cookies="false">
-  <Resource name="jdbc/exonerator"
-            type="javax.sql.DataSource"
-            auth="Container"
-            username="metrics"
-            password="password"
-            driverClassName="org.postgresql.Driver"
-            url="jdbc:postgresql://localhost/exonerator"
-            maxActive="8"
-            maxIdle="4"
-            maxWait="15000"/>
   <Resource name="jdbc/tordir"
             type="javax.sql.DataSource"
             auth="Container"
diff --git a/etc/web.xml b/etc/web.xml
index e499eca..9b4f23a 100644
--- a/etc/web.xml
+++ b/etc/web.xml
@@ -245,28 +245,6 @@
   </servlet-mapping>
 
   <servlet>
-    <servlet-name>ServerDescriptor</servlet-name>
-    <servlet-class>
-      org.torproject.ernie.status.exonerator.ServerDescriptorServlet
-    </servlet-class>
-  </servlet>
-  <servlet-mapping>
-    <servlet-name>ServerDescriptor</servlet-name>
-    <url-pattern>/serverdesc</url-pattern>
-  </servlet-mapping>
-
-  <servlet>
-    <servlet-name>Consensus</servlet-name>
-    <servlet-class>
-      org.torproject.ernie.status.exonerator.ConsensusServlet
-    </servlet-class>
-  </servlet>
-  <servlet-mapping>
-    <servlet-name>Consensus</servlet-name>
-    <url-pattern>/consensus</url-pattern>
-  </servlet-mapping>
-
-  <servlet>
     <servlet-name>ConsensusHealthServlet</servlet-name>
     <servlet-class>
       org.torproject.ernie.status.doctor.ConsensusHealthServlet
diff --git a/src/org/torproject/ernie/status/exonerator/ConsensusServlet.java b/src/org/torproject/ernie/status/exonerator/ConsensusServlet.java
deleted file mode 100644
index f7ed381..0000000
--- a/src/org/torproject/ernie/status/exonerator/ConsensusServlet.java
+++ /dev/null
@@ -1,124 +0,0 @@
-/* Copyright 2011, 2012 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.ernie.status.exonerator;
-
-import java.io.BufferedOutputStream;
-import java.io.IOException;
-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.TimeZone;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-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 ConsensusServlet extends HttpServlet {
-
-  private static final long serialVersionUID = 3147332016303032164L;
-
-  private DataSource ds;
-
-  private Logger logger;
-
-  public void init() {
-
-    /* Initialize logger. */
-    this.logger = Logger.getLogger(ConsensusServlet.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);
-    }
-  }
-
-  public void doGet(HttpServletRequest request,
-      HttpServletResponse response) throws IOException,
-      ServletException {
-
-    /* Check valid-after parameter. */
-    String validAfterParameter = request.getParameter("valid-after");
-    if (validAfterParameter == null ||
-        validAfterParameter.length() != "yyyy-MM-dd-HH-mm-ss".length()) {
-      response.sendError(HttpServletResponse.SC_BAD_REQUEST);
-      return;
-    }
-    SimpleDateFormat parameterFormat = new SimpleDateFormat(
-        "yyyy-MM-dd-HH-mm-ss");
-    parameterFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
-    long parsedTimestamp = -1L;
-    try {
-      parsedTimestamp = parameterFormat.parse(validAfterParameter).
-          getTime();
-    } catch (ParseException e) {
-      response.sendError(HttpServletResponse.SC_BAD_REQUEST);
-      return;
-    }
-    if (parsedTimestamp < 0L) {
-      response.sendError(HttpServletResponse.SC_BAD_REQUEST);
-      return;
-    }
-
-    /* Look up consensus in the database. */
-    SimpleDateFormat databaseFormat = new SimpleDateFormat(
-        "yyyy-MM-dd HH:mm:ss");
-    databaseFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
-    String databaseParameter = databaseFormat.format(parsedTimestamp);
-    byte[] rawDescriptor = null;
-    try {
-      long requestedConnection = System.currentTimeMillis();
-      Connection conn = this.ds.getConnection();
-      Statement statement = conn.createStatement();
-      String query = "SELECT rawconsensus FROM consensus "
-          + "WHERE validafter = '" + databaseParameter + "'";
-      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) {
-      response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
-      return;
-    }
-
-    /* Write response. */
-    if (rawDescriptor == null) {
-      response.sendError(HttpServletResponse.SC_NOT_FOUND);
-      return;
-    }
-    try {
-      response.setContentType("text/plain");
-      response.setHeader("Content-Length", String.valueOf(
-          rawDescriptor.length));
-      response.setHeader("Content-Disposition", "inline; filename=\""
-          + validAfterParameter + "-consensus\"");
-      BufferedOutputStream output = new BufferedOutputStream(
-          response.getOutputStream());
-      output.write(rawDescriptor);
-      output.flush();
-      output.close();
-    } finally {
-      /* Nothing to do here. */
-    }
-  }
-}
-
diff --git a/src/org/torproject/ernie/status/exonerator/ExoneraTorDatabaseImporter.java b/src/org/torproject/ernie/status/exonerator/ExoneraTorDatabaseImporter.java
deleted file mode 100644
index d89288f..0000000
--- a/src/org/torproject/ernie/status/exonerator/ExoneraTorDatabaseImporter.java
+++ /dev/null
@@ -1,619 +0,0 @@
-/* Copyright 2011, 2012 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.ernie.status.exonerator;
-
-import java.io.BufferedInputStream;
-import java.io.BufferedReader;
-import java.io.BufferedWriter;
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileReader;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.io.StringReader;
-import java.io.UnsupportedEncodingException;
-import java.sql.CallableStatement;
-import java.sql.Connection;
-import java.sql.DriverManager;
-import java.sql.SQLException;
-import java.sql.Timestamp;
-import java.sql.Types;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.Calendar;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.Stack;
-import java.util.TimeZone;
-
-import org.apache.commons.codec.binary.Base64;
-import org.apache.commons.codec.binary.Hex;
-import org.apache.commons.codec.digest.DigestUtils;
-
-/* Import Tor descriptors into the ExoneraTor database. */
-public class ExoneraTorDatabaseImporter {
-
-  /* Main function controlling the parsing process. */
-  public static void main(String[] args) {
-    readConfiguration();
-    openDatabaseConnection();
-    prepareDatabaseStatements();
-    createLockFile();
-    readImportHistoryToMemory();
-    parseDescriptors();
-    writeImportHistoryToDisk();
-    closeDatabaseConnection();
-    deleteLockFile();
-  }
-
-  /* JDBC string of the ExoneraTor database. */
-  private static String jdbcString;
-
-  /* Directory from which to import descriptors. */
-  private static String importDirString;
-
-  /* Learn JDBC string and directory to parse descriptors from. */
-  private static void readConfiguration() {
-    File configFile = new File("config");
-    if (!configFile.exists()) {
-      System.err.println("Could not find config file.  Exiting.");
-      System.exit(1);
-    }
-    String line = null;
-    try {
-      BufferedReader br = new BufferedReader(new FileReader(configFile));
-      while ((line = br.readLine()) != null) {
-        if (line.startsWith("#") || line.length() < 1) {
-          continue;
-        } else if (line.startsWith("ExoneraTorDatabaseJdbc")) {
-          jdbcString = line.split(" ")[1];
-        } else if (line.startsWith("ExoneraTorImportDirectory")) {
-          importDirString = line.split(" ")[1];
-        } else {
-          /* Ignore unrecognized configuration keys. */
-        }
-      }
-      br.close();
-    } catch (IOException e) {
-      System.err.println("Could not parse config file.  Exiting.");
-      System.exit(1);
-    }
-  }
-
-  /* Database connection. */
-  private static Connection connection;
-
-  /* Open a database connection using the JDBC string in the config. */
-  private static void openDatabaseConnection() {
-    try {
-      connection = DriverManager.getConnection(jdbcString);
-    } catch (SQLException e) {
-      System.out.println("Could not connect to database.  Exiting.");
-      System.exit(1);
-    }
-  }
-
-  /* Callable statements to import data into the database. */
-  private static CallableStatement insertDescriptorStatement;
-  private static CallableStatement insertStatusentryStatement;
-  private static CallableStatement insertConsensusStatement;
-  private static CallableStatement insertExitlistentryStatement;
-
-  /* Prepare statements for importing data into the database. */
-  private static void prepareDatabaseStatements() {
-    try {
-      insertDescriptorStatement = connection.prepareCall(
-          "{call insert_descriptor(?, ?)}");
-      insertStatusentryStatement = connection.prepareCall(
-          "{call insert_statusentry(?, ?, ?, ?, ?, ?, ?)}");
-      insertConsensusStatement = connection.prepareCall(
-          "{call insert_consensus(?, ?)}");
-      insertExitlistentryStatement = connection.prepareCall(
-          "{call insert_exitlistentry(?, ?, ?, ?, ?)}");
-    } catch (SQLException e) {
-      System.out.println("Could not prepare callable statements to "
-          + "import data into the database.  Exiting.");
-      System.exit(1);
-    }
-  }
-
-  /* Create a local lock file to prevent other instances of this import
-   * tool to run concurrently. */
-  private static void createLockFile() {
-    File lockFile = new File("exonerator-lock");
-    try {
-      if (lockFile.exists()) {
-        BufferedReader br = new BufferedReader(new FileReader(lockFile));
-        long runStarted = Long.parseLong(br.readLine());
-        br.close();
-        if (System.currentTimeMillis() - runStarted
-            < 6L * 60L * 60L * 1000L) {
-          System.out.println("File 'exonerator-lock' is less than 6 "
-              + "hours old.  Exiting.");
-          System.exit(1);
-        } else {
-          System.out.println("File 'exonerator-lock' is at least 6 hours "
-              + "old.  Overwriting and executing anyway.");
-        }
-      }
-      BufferedWriter bw = new BufferedWriter(new FileWriter(
-          "exonerator-lock"));
-      bw.append(String.valueOf(System.currentTimeMillis()) + "\n");
-      bw.close();
-    } catch (IOException e) {
-      System.out.println("Could not create 'exonerator-lock' file.  "
-          + "Exiting.");
-      System.exit(1);
-    }
-  }
-
-  /* Last and next parse histories containing paths of parsed files and
-   * last modified times. */
-  private static Map<String, Long>
-      lastImportHistory = new HashMap<String, Long>(),
-      nextImportHistory = new HashMap<String, Long>();
-
-  /* Read stats/exonerator-import-history file from disk and remember
-   * locally when files were last parsed. */
-  private static void readImportHistoryToMemory() {
-    File parseHistoryFile = new File("stats",
-        "exonerator-import-history");
-    if (parseHistoryFile.exists()) {
-      try {
-        BufferedReader br = new BufferedReader(new FileReader(
-            parseHistoryFile));
-        String line = null;
-        int lineNumber = 0;
-        while ((line = br.readLine()) != null) {
-          lineNumber++;
-          String[] parts = line.split(",");
-          if (parts.length != 2) {
-            System.out.println("File 'stats/exonerator-import-history' "
-                + "contains a corrupt entry in line " + lineNumber
-                + ".  Ignoring parse history file entirely.");
-            lastImportHistory.clear();
-            br.close();
-            return;
-          }
-          long lastModified = Long.parseLong(parts[0]);
-          String filename = parts[1];
-          lastImportHistory.put(filename, lastModified);
-        }
-        br.close();
-      } catch (IOException e) {
-        System.out.println("Could not read import history.  Ignoring.");
-        lastImportHistory.clear();
-      }
-    }
-  }
-
-  /* Parse descriptors in the import directory and its subdirectories. */
-  private static void parseDescriptors() {
-    File file = new File(importDirString);
-    if (!file.exists()) {
-      System.out.println("File or directory " + importDirString + " does "
-          + "not exist.  Exiting.");
-      return;
-    }
-    Stack<File> files = new Stack<File>();
-    files.add(file);
-    while (!files.isEmpty()) {
-      file = files.pop();
-      if (file.isDirectory()) {
-        for (File f : file.listFiles()) {
-          files.add(f);
-        }
-      } else {
-        parseFile(file);
-      }
-    }
-  }
-
-  /* Import a file if it wasn't imported before, and add it to the import
-   * history for the next execution. */
-  private static void parseFile(File file) {
-    long lastModified = file.lastModified();
-    String filename = file.getName();
-    nextImportHistory.put(filename, lastModified);
-    if (!lastImportHistory.containsKey(filename) ||
-        lastImportHistory.get(filename) < lastModified) {
-      try {
-        FileInputStream fis = new FileInputStream(file);
-        BufferedInputStream bis = new BufferedInputStream(fis);
-        ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        int len;
-        byte[] bytes = new byte[1024];
-        while ((len = bis.read(bytes, 0, 1024)) >= 0) {
-          baos.write(bytes, 0, len);
-        }
-        bis.close();
-        byte[] allBytes = baos.toByteArray();
-        splitFile(file, allBytes);
-      } catch (IOException e) {
-        System.out.println("Could not read '" + file + "' to memory.  "
-            + "Skipping.");
-        nextImportHistory.remove(filename);
-      }
-    }
-  }
-
-  /* Detect what descriptor type is contained in a file and split it to
-   * parse the single descriptors. */
-  private static void splitFile(File file, byte[] bytes) {
-    try {
-      String asciiString = new String(bytes, "US-ASCII");
-      BufferedReader br = new BufferedReader(new StringReader(
-          asciiString));
-      String line = br.readLine();
-      while (line != null && line.startsWith("@")) {
-        line = br.readLine();
-      }
-      if (line == null) {
-        return;
-      }
-      br.close();
-      String startToken = null;
-      if (line.startsWith("router ")) {
-        startToken = "router ";
-      } else if (line.equals("network-status-version 3")) {
-        startToken = "network-status-version 3";
-      } else if (line.startsWith("Downloaded ") ||
-          line.startsWith("ExitNode ")) {
-        startToken = "ExitNode ";
-      } else {
-        System.out.println("Unknown descriptor type in file '" + file
-            + "'.  Ignoring.");
-        return;
-      }
-      String splitToken = "\n" + startToken;
-      int length = bytes.length, start = asciiString.indexOf(startToken);
-      while (start < length) {
-        int end = asciiString.indexOf(splitToken, start);
-        if (end < 0) {
-          end = length;
-        } else {
-          end += 1;
-        }
-        byte[] descBytes = new byte[end - start];
-        System.arraycopy(bytes, start, descBytes, 0, end - start);
-        if (startToken.equals("router ")) {
-          parseServerDescriptor(file, descBytes);
-        } else if (startToken.equals("network-status-version 3")) {
-          parseConsensus(file, descBytes);
-        } else if (startToken.equals("ExitNode ")) {
-          parseExitList(file, descBytes);
-        }
-        start = end;
-      }
-    } catch (IOException e) {
-      System.out.println("Could not parse descriptor '" + file + "'.  "
-          + "Skipping.");
-    }
-  }
-
-  /* Date format to parse UTC timestamps. */
-  private static SimpleDateFormat parseFormat;
-  static {
-    parseFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
-    parseFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
-  }
-
-  /* Parse a single server descriptor. */
-  private static void parseServerDescriptor(File file, byte[] bytes) {
-    String ascii = "";
-    try {
-      ascii = new String(bytes, "US-ASCII");
-    } catch (UnsupportedEncodingException e) {
-      /* We know that US-ASCII is a supported encoding. */
-    }
-    String startToken = "router ";
-    String sigToken = "\nrouter-signature\n";
-    int start = ascii.indexOf(startToken);
-    int sig = ascii.indexOf(sigToken) + sigToken.length();
-    String descriptor = null;
-    if (start >= 0 || sig >= 0 || sig > start) {
-      byte[] forDigest = new byte[sig - start];
-      System.arraycopy(bytes, start, forDigest, 0, sig - start);
-      descriptor = DigestUtils.shaHex(forDigest);
-    }
-    if (descriptor == null) {
-      System.out.println("Could not calculate descriptor digest.  "
-          + "Skipping.");
-      return;
-    }
-    importDescriptor(descriptor, bytes);
-  }
-
-  /* Import a single server descriptor into the database. */
-  private static void importDescriptor(String descriptor,
-      byte[] rawDescriptor) {
-    try {
-      insertDescriptorStatement.clearParameters();
-      insertDescriptorStatement.setString(1, descriptor);
-      insertDescriptorStatement.setBytes(2, rawDescriptor);
-      insertDescriptorStatement.execute();
-    } catch (SQLException e) {
-      System.out.println("Could not import descriptor into the "
-          + "database.  Exiting.");
-      System.exit(1);
-    }
-  }
-
-  /* Parse a consensus. */
-  private static void parseConsensus(File file, byte[] bytes) {
-    try {
-      BufferedReader br = new BufferedReader(new StringReader(new String(
-          bytes, "US-ASCII")));
-      String line, fingerprint = null, descriptor = null;
-      Set<String> orAddresses = new HashSet<String>();
-      long validAfterMillis = -1L;
-      StringBuilder rawStatusentryBuilder = null;
-      boolean isRunning = false;
-      while ((line = br.readLine()) != null) {
-        if (line.startsWith("vote-status ") &&
-            !line.equals("vote-status consensus")) {
-          System.out.println("File '" + file + "' contains network status "
-              + "*votes*, not network status *consensuses*.  Skipping.");
-          return;
-        } else if (line.startsWith("valid-after ")) {
-          String validAfterTime = line.substring("valid-after ".length());
-          try {
-            validAfterMillis = parseFormat.parse(validAfterTime).
-                getTime();
-          } catch (ParseException e) {
-            System.out.println("Could not parse valid-after timestamp in "
-                + "'" + file + "'.  Skipping.");
-            return;
-          }
-          importConsensus(validAfterMillis, bytes);
-        } else if (line.startsWith("r ") ||
-            line.equals("directory-footer")) {
-          if (isRunning) {
-            byte[] rawStatusentry = rawStatusentryBuilder.toString().
-                getBytes();
-            importStatusentry(validAfterMillis, fingerprint, descriptor,
-                orAddresses, rawStatusentry);
-            orAddresses = new HashSet<String>();
-          }
-          if (line.equals("directory-footer")) {
-            return;
-          }
-          rawStatusentryBuilder = new StringBuilder(line + "\n");
-          String[] parts = line.split(" ");
-          if (parts.length < 9) {
-            System.out.println("Could not parse r line '" + line
-                + "'.  Skipping.");
-            return;
-          }
-          fingerprint = Hex.encodeHexString(Base64.decodeBase64(parts[2]
-              + "=")).toLowerCase();
-          descriptor = Hex.encodeHexString(Base64.decodeBase64(parts[3]
-              + "=")).toLowerCase();
-          orAddresses.add(parts[6]);
-        } else if (line.startsWith("a ")) {
-          rawStatusentryBuilder.append(line + "\n");
-          orAddresses.add(line.substring("a ".length(),
-              line.lastIndexOf(":")));
-        } else if (line.startsWith("s ") || line.equals("s")) {
-          rawStatusentryBuilder.append(line + "\n");
-          isRunning = line.contains(" Running");
-        } else if (rawStatusentryBuilder != null) {
-          rawStatusentryBuilder.append(line + "\n");
-        }
-      }
-    } catch (IOException e) {
-      System.out.println("Could not parse consensus.  Skipping.");
-      return;
-    }
-  }
-
-  /* UTC calendar for importing timestamps into the database. */
-  private static Calendar calendarUTC = Calendar.getInstance(
-      TimeZone.getTimeZone("UTC"));
-
-  /* Import a status entry with one or more OR addresses into the
-   * database. */
-  private static void importStatusentry(long validAfterMillis,
-      String fingerprint, String descriptor, Set<String> orAddresses,
-      byte[] rawStatusentry) {
-    try {
-      for (String orAddress : orAddresses) {
-        insertStatusentryStatement.clearParameters();
-        insertStatusentryStatement.setTimestamp(1,
-            new Timestamp(validAfterMillis), calendarUTC);
-        insertStatusentryStatement.setString(2, fingerprint);
-        insertStatusentryStatement.setString(3, descriptor);
-        if (!orAddress.contains(":")) {
-          String[] addressParts = orAddress.split("\\.");
-          byte[] address24Bytes = new byte[3];
-          address24Bytes[0] = (byte) Integer.parseInt(addressParts[0]);
-          address24Bytes[1] = (byte) Integer.parseInt(addressParts[1]);
-          address24Bytes[2] = (byte) Integer.parseInt(addressParts[2]);
-          String orAddress24 = Hex.encodeHexString(address24Bytes);
-          insertStatusentryStatement.setString(4, orAddress24);
-          insertStatusentryStatement.setNull(5, Types.VARCHAR);
-          insertStatusentryStatement.setString(6, orAddress);
-        } else {
-          StringBuilder addressHex = new StringBuilder();
-          int start = orAddress.startsWith("[::") ? 2 : 1;
-          int end = orAddress.length()
-              - (orAddress.endsWith("::]") ? 2 : 1);
-          String[] parts = orAddress.substring(start, end).split(":", -1);
-          for (int i = 0; i < parts.length; i++) {
-            String part = parts[i];
-            if (part.length() == 0) {
-              addressHex.append("x");
-            } else if (part.length() <= 4) {
-              addressHex.append(String.format("%4s", part));
-            } else {
-              addressHex = null;
-              break;
-            }
-          }
-          String orAddress48 = null;
-          if (addressHex != null) {
-            String addressHexString = addressHex.toString();
-            addressHexString = addressHexString.replaceFirst("x",
-                String.format("%" + (33 - addressHexString.length())
-                + "s", "0"));
-            if (!addressHexString.contains("x") &&
-                addressHexString.length() == 32) {
-              orAddress48 = addressHexString.replaceAll(" ", "0").
-                  toLowerCase().substring(0, 12);
-            }
-          }
-          if (orAddress48 != null) {
-            insertStatusentryStatement.setNull(4, Types.VARCHAR);
-            insertStatusentryStatement.setString(5, orAddress48);
-            insertStatusentryStatement.setString(6,
-                orAddress.replaceAll("[\\[\\]]", ""));
-          } else {
-            System.err.println("Could not import status entry with IPv6 "
-                + "address '" + orAddress + "'.  Exiting.");
-            System.exit(1);
-          }
-        }
-        insertStatusentryStatement.setBytes(7, rawStatusentry);
-        insertStatusentryStatement.execute();
-      }
-    } catch (SQLException e) {
-      System.out.println("Could not import status entry.  Exiting.");
-      System.exit(1);
-    }
-  }
-
-  /* Import a consensus into the database. */
-  private static void importConsensus(long validAfterMillis,
-      byte[] rawConsensus) {
-    try {
-      insertConsensusStatement.clearParameters();
-      insertConsensusStatement.setTimestamp(1,
-          new Timestamp(validAfterMillis), calendarUTC);
-      insertConsensusStatement.setBytes(2, rawConsensus);
-      insertConsensusStatement.execute();
-    } catch (SQLException e) {
-      System.out.println("Could not import consensus.  Exiting.");
-      System.exit(1);
-    }
-  }
-
-  /* Parse an exit list. */
-  private static void parseExitList(File file, byte[] bytes) {
-    try {
-      BufferedReader br = new BufferedReader(new StringReader(new String(
-          bytes, "US-ASCII")));
-      String fingerprint = null;
-      Set<String> exitAddressLines = new HashSet<String>();
-      StringBuilder rawExitlistentryBuilder = new StringBuilder();
-      while (true) {
-        String line = br.readLine();
-        if ((line == null || line.startsWith("ExitNode ")) &&
-            fingerprint != null) {
-          for (String exitAddressLine : exitAddressLines) {
-            String[] parts = exitAddressLine.split(" ");
-            String exitAddress = parts[1];
-            /* TODO Extend the following code for IPv6 once the exit list
-             * format supports it. */
-            String[] exitAddressParts = exitAddress.split("\\.");
-            byte[] exitAddress24Bytes = new byte[3];
-            exitAddress24Bytes[0] = (byte) Integer.parseInt(
-                exitAddressParts[0]);
-            exitAddress24Bytes[1] = (byte) Integer.parseInt(
-                exitAddressParts[1]);
-            exitAddress24Bytes[2] = (byte) Integer.parseInt(
-                exitAddressParts[2]);
-            String exitAddress24 = Hex.encodeHexString(
-                exitAddress24Bytes);
-            String scannedTime = parts[2] + " " + parts[3];
-            long scannedMillis = -1L;
-            try {
-              scannedMillis = parseFormat.parse(scannedTime).getTime();
-            } catch (ParseException e) {
-              System.out.println("Could not parse timestamp in "
-                  + "'" + file + "'.  Skipping.");
-              return;
-            }
-            byte[] rawExitlistentry = rawExitlistentryBuilder.toString().
-                getBytes();
-            importExitlistentry(fingerprint, exitAddress24, exitAddress,
-                scannedMillis, rawExitlistentry);
-          }
-          exitAddressLines.clear();
-          rawExitlistentryBuilder = new StringBuilder();
-        }
-        if (line == null) {
-          break;
-        }
-        rawExitlistentryBuilder.append(line + "\n");
-        if (line.startsWith("ExitNode ")) {
-          fingerprint = line.substring("ExitNode ".length()).
-              toLowerCase();
-        } else if (line.startsWith("ExitAddress ")) {
-          exitAddressLines.add(line);
-        }
-      }
-      br.close();
-    } catch (IOException e) {
-      System.out.println("Could not parse exit list.  Skipping.");
-      return;
-    }
-  }
-
-  /* Import an exit list entry into the database. */
-  private static void importExitlistentry(String fingerprint,
-      String exitAddress24, String exitAddress, long scannedMillis,
-      byte[] rawExitlistentry) {
-    try {
-      insertExitlistentryStatement.clearParameters();
-      insertExitlistentryStatement.setString(1, fingerprint);
-      insertExitlistentryStatement.setString(2, exitAddress24);
-      insertExitlistentryStatement.setString(3, exitAddress);
-      insertExitlistentryStatement.setTimestamp(4,
-          new Timestamp(scannedMillis), calendarUTC);
-      insertExitlistentryStatement.setBytes(5, rawExitlistentry);
-      insertExitlistentryStatement.execute();
-    } catch (SQLException e) {
-      System.out.println("Could not import exit list entry.  Exiting.");
-      System.exit(1);
-    }
-  }
-
-  /* Write parse history from memory to disk for the next execution. */
-  private static void writeImportHistoryToDisk() {
-    File parseHistoryFile = new File("stats/exonerator-import-history");
-    parseHistoryFile.getParentFile().mkdirs();
-    try {
-      BufferedWriter bw = new BufferedWriter(new FileWriter(
-          parseHistoryFile));
-      for (Map.Entry<String, Long> historyEntry :
-          nextImportHistory.entrySet()) {
-        bw.write(String.valueOf(historyEntry.getValue()) + ","
-            + historyEntry.getKey() + "\n");
-      }
-      bw.close();
-    } catch (IOException e) {
-      System.out.println("File 'stats/exonerator-import-history' could "
-          + "not be written.  Ignoring.");
-    }
-  }
-
-  /* Close the database connection. */
-  private static void closeDatabaseConnection() {
-    try {
-      connection.close();
-    } catch (SQLException e) {
-      System.out.println("Could not close database connection.  "
-          + "Ignoring.");
-    }
-  }
-
-  /* Delete the exonerator-lock file to allow the next executing of this
-   * tool. */
-  private static void deleteLockFile() {
-    new File("exonerator-lock").delete();
-  }
-}
-
diff --git a/src/org/torproject/ernie/status/exonerator/ExoneraTorServlet.java b/src/org/torproject/ernie/status/exonerator/ExoneraTorServlet.java
index 9d296fc..d37b9a8 100644
--- a/src/org/torproject/ernie/status/exonerator/ExoneraTorServlet.java
+++ b/src/org/torproject/ernie/status/exonerator/ExoneraTorServlet.java
@@ -2,1153 +2,23 @@
  * See LICENSE for licensing information */
 package org.torproject.ernie.status.exonerator;
 
-import java.io.BufferedReader;
 import java.io.IOException;
-import java.io.PrintWriter;
-import java.io.StringReader;
-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.HashSet;
-import java.util.List;
-import java.util.Set;
-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.Matcher;
-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;
-
-import org.apache.commons.codec.binary.Hex;
-import org.apache.commons.lang.StringEscapeUtils;
 
 public class ExoneraTorServlet extends HttpServlet {
 
-  private static final long serialVersionUID = 1370088989739567509L;
-
-  private DataSource ds;
-
-  private Logger logger;
-
-  public void init() {
-
-    /* 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);
-    }
-  }
-
-  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 a given IP "
-          + "address was a Tor relay</h3>\n"
-        + "        <br>\n"
-        + "        <p>Just because you see an Internet connection from a "
-          + "particular IP address does not mean you know <i>who</i> "
-          + "originated the traffic. Tor anonymizes Internet traffic by "
-          + "\"<a href=\"https://www.torproject.org/about/overview"
-          + "#thesolution\">onion routing</a>,\" sending packets "
-          + "through a series of encrypted hops before they reach their "
-          + "destination. Therefore, if you see traffic from a Tor node, "
-          + "you may be seeing traffic that originated from someone "
-          + "using Tor, rather than from the node operator itself. The "
-          + "Tor Project and Tor node operators have no records of the "
-          + "traffic that passes over the network, but we do maintain "
-          + "current and historical records of which IP addresses are "
-          + "part of the Tor network.</p>\n"
-        + "        <br>\n"
-        + "        <p>ExoneraTor tells you whether there was a Tor relay "
-          + "running on a given IP address at a given time. ExoneraTor "
-          + "can further indicate whether this relay permitted exiting "
-          + "to a given server and/or TCP port. ExoneraTor learns these "
-          + "facts by parsing the public relay lists and relay "
-          + "descriptors that are collected from the Tor directory "
-          + "authorities and the exit lists collected by TorDNSEL. By "
-          + "inputting an IP address and time, you can determine whether "
-          + "that IP was then a part of the Tor network.</p>\n"
-        + "        <br>\n"
-        + "        <p><font color=\"red\"><b>Notice:</b> Note that the "
-          + "information you are providing below may be visible to "
-          + "anyone who can read the network traffic between you and "
-          + "this web server or who has access to this web "
-          + "server.</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();
-  }
+  private static final long serialVersionUID = -6227541092325776626L;
 
   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. */
-    Pattern ipv4AddressPattern = Pattern.compile(
-        "^([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." +
-        "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." +
-        "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." +
-        "([01]?\\d\\d?|2[0-4]\\d|25[0-5])$");
-    Pattern ipv6AddressPattern = Pattern.compile(
-        "^\\[?[0-9a-fA-F:]{3,39}\\]?$");
-    String ipParameter = request.getParameter("ip");
-    String relayIP = "", ipWarning = "";
-    if (ipParameter != null && ipParameter.length() > 0) {
-      if (ipv4AddressPattern.matcher(ipParameter).matches()) {
-        String[] ipParts = ipParameter.split("\\.");
-        relayIP = Integer.parseInt(ipParts[0]) + "."
-            + Integer.parseInt(ipParts[1]) + "."
-            + Integer.parseInt(ipParts[2]) + "."
-            + Integer.parseInt(ipParts[3]);
-      } else if (ipv6AddressPattern.matcher(ipParameter).matches()) {
-        if (ipParameter.startsWith("[") && ipParameter.endsWith("]")) {
-          ipParameter = ipParameter.substring(1,
-              ipParameter.length() - 1);
-        }
-        StringBuilder addressHex = new StringBuilder();
-        int start = ipParameter.startsWith("::") ? 1 : 0;
-        int end = ipParameter.length()
-            - (ipParameter.endsWith("::") ? 1 : 0);
-        String[] parts = ipParameter.substring(start, end).split(":", -1);
-        for (int i = 0; i < parts.length; i++) {
-          String part = parts[i];
-          if (part.length() == 0) {
-            addressHex.append("x");
-          } else if (part.length() <= 4) {
-            addressHex.append(String.format("%4s", part));
-          } else {
-            addressHex = null;
-            break;
-          }
-        }
-        if (addressHex != null) {
-          String addressHexString = addressHex.toString();
-          addressHexString = addressHexString.replaceFirst("x",
-              String.format("%" + (33 - addressHexString.length()) + "s",
-              "0"));
-          if (!addressHexString.contains("x") &&
-              addressHexString.length() == 32) {
-            relayIP = ipParameter.toLowerCase();
-          }
-        }
-        if (relayIP.length() < 1) {
-          ipWarning = "\"" + (ipParameter.length() > 40 ?
-              StringEscapeUtils.escapeHtml(ipParameter.substring(0, 40))
-              + "[...]" : StringEscapeUtils.escapeHtml(ipParameter))
-              + "\" is not a valid IP address.";
-        }
-      } else {
-        ipWarning = "\"" + (ipParameter.length() > 20 ?
-            StringEscapeUtils.escapeHtml(ipParameter.substring(0, 20))
-            + "[...]" : 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 =
-          ipv4AddressPattern.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\" size=\"30\""
-          + (relayIP.length() > 0 ? " value=\"" + relayIP + "\""
-            : "")
-          + ">"
-          + (ipWarning.length() > 0 ? "<br><font color=\"red\">"
-          + ipWarning + "</font>" : "")
-        + "</td>\n"
-        + "              <td><i>(Ex.: 86.59.21.38 or "
-          + "2001:858:2:2:aabb:0:563b:1526)</i></td>\n"
-        + "            </tr>\n"
-        + "            <tr>\n"
-        + "              <td align=\"right\">Date or timestamp, in "
-          + "UTC:</td>\n"
-        + "              <td><input type=\"text\" name=\"timestamp\""
-          + " size=\"30\""
-          + (timestampStr.length() > 0 ? " value=\"" + timestampStr + "\""
-            : "")
-          + ">"
-          + (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);
-        String exitaddress = rs.getString(6);
-        StringBuilder html = new StringBuilder();
-        for (String line : new String(rawstatusentry).split("\n")) {
-          if (line.startsWith("r ")) {
-            String[] parts = line.split(" ");
-            boolean orAddressMatches = parts[6].equals(relayIP);
-            html.append("r " + parts[1] + " " + parts[2] + " "
-                + "<a href=\"serverdesc?desc-id=" + descriptor + "\" "
-                + "target=\"_blank\">" + parts[3] + "</a> " + parts[4]
-                + " " + parts[5] + " " + (orAddressMatches ? "<b>" : "")
-                + parts[6] + (orAddressMatches ? "</b>" : "") + " "
-                + parts[7] + " " + parts[8] + "\n");
-          } else if (line.startsWith("a ") &&
-              line.toLowerCase().contains(relayIP)) {
-            String address = line.substring("a ".length(),
-                line.lastIndexOf(":"));
-            String port = line.substring(line.lastIndexOf(":"));
-            html.append("a <b>" + address + "</b>" + port + "\n");
-          }
-        }
-        if (exitaddress != null && exitaddress.length() > 0) {
-          long scanned = rs.getTimestamp(7).getTime();
-          html.append("  [ExitAddress <b>" + exitaddress
-              + "</b> " + validAfterTimeFormat.format(scanned) + "]\n");
-        }
-        if (!statusEntries.containsKey(validafter)) {
-          statusEntries.put(validafter, new TreeMap<String, String>());
-        }
-        statusEntries.get(validafter).put(fingerprint, html.toString());
-      }
-      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 or /48 network and tell the
-       * user about it. */
-      List<String> addressesInSameNetwork = new ArrayList<String>();
-      if (!relayIP.contains(":")) {
-        String[] relayIPParts = relayIP.split("\\.");
-        byte[] address24Bytes = new byte[3];
-        address24Bytes[0] = (byte) Integer.parseInt(relayIPParts[0]);
-        address24Bytes[1] = (byte) Integer.parseInt(relayIPParts[1]);
-        address24Bytes[2] = (byte) Integer.parseInt(relayIPParts[2]);
-        String address24 = Hex.encodeHexString(address24Bytes);
-        try {
-          CallableStatement cs = conn.prepareCall(
-              "{call search_addresses_in_same_24 (?, ?)}");
-          cs.setString(1, address24);
-          cs.setDate(2, new java.sql.Date(timestamp));
-          ResultSet rs = cs.executeQuery();
-          while (rs.next()) {
-            String address = rs.getString(1);
-            if (!addressesInSameNetwork.contains(address)) {
-              addressesInSameNetwork.add(address);
-            }
-          }
-          rs.close();
-          cs.close();
-        } catch (SQLException e) {
-          /* No other addresses in the same /24 found. */
-        }
-      } else {
-        StringBuilder addressHex = new StringBuilder();
-        int start = relayIP.startsWith("::") ? 1 : 0;
-        int end = relayIP.length() - (relayIP.endsWith("::") ? 1 : 0);
-        String[] parts = relayIP.substring(start, end).split(":", -1);
-        for (int i = 0; i < parts.length; i++) {
-          String part = parts[i];
-          if (part.length() == 0) {
-            addressHex.append("x");
-          } else if (part.length() <= 4) {
-            addressHex.append(String.format("%4s", part));
-          } else {
-            addressHex = null;
-            break;
-          }
-        }
-        String address48 = null;
-        if (addressHex != null) {
-          String addressHexString = addressHex.toString();
-          addressHexString = addressHexString.replaceFirst("x",
-              String.format("%" + (33 - addressHexString.length())
-              + "s", "0"));
-          if (!addressHexString.contains("x") &&
-              addressHexString.length() == 32) {
-            address48 = addressHexString.replaceAll(" ", "0").
-                toLowerCase().substring(0, 12);
-          }
-        }
-        if (address48 != null) {
-          try {
-            CallableStatement cs = conn.prepareCall(
-                "{call search_addresses_in_same_48 (?, ?)}");
-            cs.setString(1, address48);
-            cs.setDate(2, new java.sql.Date(timestamp));
-            ResultSet rs = cs.executeQuery();
-            while (rs.next()) {
-              String address = rs.getString(1);
-              if (!addressesInSameNetwork.contains(address)) {
-                addressesInSameNetwork.add(address);
-              }
-            }
-            rs.close();
-            cs.close();
-          } catch (SQLException e) {
-            /* No other addresses in the same /48 found. */
-          }
-        }
-      }
-      if (!addressesInSameNetwork.isEmpty()) {
-        if (!relayIP.contains(":")) {
-          out.print("        <p>The following other IP addresses of Tor "
-              + "relays in the same /24 network were found in relay "
-              + "and/or exit lists around the time that could be related "
-              + "to IP address " + relayIP + ":</p>\n");
-        } else {
-          out.print("        <p>The following other IP addresses of Tor "
-              + "relays in the same /48 network were found in relay "
-              + "lists around the time that could be related to IP "
-              + "address " + relayIP + ":</p>\n");
-        }
-        out.print("        <ul>\n");
-        for (String s : addressesInSameNetwork) {
-          out.print("        <li>" + s + "</li>\n");
-        }
-        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;
-      }
-    }
-
-    /* Looking up targets for IPv6 is not supported yet. */
-    if (relayIP.contains(":")) {
-      writeFooter(out);
-      return;
-    }
-
-    /* Second part: target */
-    out.println("<br><a name=\"exit\"></a><h3>Was this relay configured "
-        + "to permit exiting to a given target?</h3>");
-
-    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;
-        }
-      }
-    }
+      HttpServletResponse response) throws IOException, ServletException {
 
-    /* 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);
+    /* Forward the request to the JSP that does all the hard work. */
+    request.getRequestDispatcher("WEB-INF/exonerator.jsp").forward(
+        request, response);
   }
 }
 
diff --git a/src/org/torproject/ernie/status/exonerator/ServerDescriptorServlet.java b/src/org/torproject/ernie/status/exonerator/ServerDescriptorServlet.java
deleted file mode 100644
index f94611e..0000000
--- a/src/org/torproject/ernie/status/exonerator/ServerDescriptorServlet.java
+++ /dev/null
@@ -1,132 +0,0 @@
-/* Copyright 2011, 2012 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.ernie.status.exonerator;
-
-import java.io.BufferedOutputStream;
-import java.io.IOException;
-import java.sql.Connection;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import java.util.regex.Matcher;
-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 ServerDescriptorServlet extends HttpServlet {
-
-  private static final long serialVersionUID = -7935883442750583462L;
-
-  private DataSource ds;
-
-  private Logger logger;
-
-  public void init() {
-
-    /* Initialize logger. */
-    this.logger = Logger.getLogger(
-        ServerDescriptorServlet.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);
-    }
-  }
-
-  public void doGet(HttpServletRequest request,
-      HttpServletResponse response) throws IOException,
-      ServletException {
-
-    /* Read desc-id parameter. */
-    String descIdParameter = request.getParameter("desc-id");
-
-    /* See if we were given a desc-id parameter.  If so, look up this
-     * descriptor and return it. */
-    List<byte[]> rawDescriptors = new ArrayList<byte[]>();
-    String filename = null;
-    if (descIdParameter != null) {
-      if (descIdParameter.length() < 8 ||
-          descIdParameter.length() > 40) {
-        response.sendError(HttpServletResponse.SC_BAD_REQUEST);
-        return;
-      }
-      String descId = descIdParameter.toLowerCase();
-      Pattern descIdPattern = Pattern.compile("^[0-9a-f]+$");
-      Matcher descIdMatcher = descIdPattern.matcher(descId);
-      if (!descIdMatcher.matches()) {
-        response.sendError(HttpServletResponse.SC_BAD_REQUEST);
-        return;
-      }
-
-      /* Look up descriptor in the database. */
-      try {
-        long requestedConnection = System.currentTimeMillis();
-        Connection conn = ds.getConnection();
-        Statement statement = conn.createStatement();
-        String query = "SELECT descriptor, rawdescriptor FROM descriptor "
-            + "WHERE descriptor LIKE '" + descId + "%'";
-        ResultSet rs = statement.executeQuery(query);
-        if (rs.next()) {
-          filename = rs.getString(1);
-          rawDescriptors.add(rs.getBytes(2));
-        }
-        rs.close();
-        statement.close();
-        conn.close();
-        this.logger.info("Returned a database connection to the pool "
-            + "after " + (System.currentTimeMillis()
-            - requestedConnection) + " millis.");
-      } catch (SQLException e) {
-        response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
-        return;
-      }
-
-    /* Return an error if no desc-id parameter was given. */
-    } else {
-      response.sendError(HttpServletResponse.SC_BAD_REQUEST);
-      return;
-    }
-
-    /* Write response. */
-    if (rawDescriptors.size() == 0) {
-      response.sendError(HttpServletResponse.SC_NOT_FOUND);
-      return;
-    }
-    try {
-      response.setContentType("text/plain");
-      int responseLength = 0;
-      for (byte[] rawDescriptor : rawDescriptors) {
-        responseLength += rawDescriptor.length;
-      }
-      response.setHeader("Content-Length", String.valueOf(
-          responseLength));
-      response.setHeader("Content-Disposition", "inline; filename=\""
-          + filename + "\"");
-      BufferedOutputStream output = new BufferedOutputStream(
-          response.getOutputStream());
-      for (byte[] rawDescriptor : rawDescriptors) {
-        output.write(rawDescriptor);
-      }
-      output.flush();
-      output.close();
-    } finally {
-      /* Nothing to do here. */
-    }
-  }
-}
-
diff --git a/src/org/torproject/ernie/status/relaysearch/RelaySearchServlet.java b/src/org/torproject/ernie/status/relaysearch/RelaySearchServlet.java
index cd5c4c1..b7c8291 100644
--- a/src/org/torproject/ernie/status/relaysearch/RelaySearchServlet.java
+++ b/src/org/torproject/ernie/status/relaysearch/RelaySearchServlet.java
@@ -467,7 +467,8 @@ public class RelaySearchServlet extends HttpServlet {
               + fingerprint);
           if (!rawValidAfterLines.containsKey(validAfter)) {
             rawValidAfterLines.put(validAfter, "<tt>valid-after "
-                + "<a href=\"consensus?valid-after="
+                + "<a href=\"https://exonerator.torproject.org/"
+                + "consensus?valid-after="
                 + validAfter.replaceAll(":", "-").replaceAll(" ", "-")
                 + "\" target=\"_blank\">" + validAfter + "</a></tt><br>");
           }
@@ -488,7 +489,9 @@ public class RelaySearchServlet extends HttpServlet {
                   new BigInteger(1, Base64.decodeBase64(parts[3]
                   + "==")));
               rawStatusEntryBuilder.append("<tt>r " + parts[1] + " "
-                  + parts[2] + " <a href=\"serverdesc?desc-id="
+                  + parts[2] + " <a href=\""
+                  + "https://exonerator.torproject.org/"
+                  + "serverdesc?desc-id="
                   + descriptorBase64 + "\" target=\"_blank\">" + parts[3]
                   + "</a> " + parts[4] + " " + parts[5] + " " + parts[6]
                   + " " + parts[7] + " " + parts[8] + "</tt><br>");
diff --git a/web/WEB-INF/exonerator.jsp b/web/WEB-INF/exonerator.jsp
new file mode 100644
index 0000000..0eefe99
--- /dev/null
+++ b/web/WEB-INF/exonerator.jsp
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<html>
+<head>
+  <title>Tor Metrics Portal: ExoneraTor</title>
+  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
+  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
+  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
+</head>
+<body>
+  <div class="center">
+    <%@ include file="banner.jsp"%>
+  <div class="main-column" style="margin:5; Padding:0;">
+    <h2>ExoneraTor</h2>
+    <h3>or: a website that tells you whether a given IP address was a Tor
+    relay</h3>
+    <br>
+    <p>Just because you see an Internet connection from a particular IP
+    address does not mean you know <i>who</i> originated the traffic. Tor
+    anonymizes Internet traffic by "<a href="https://www.torproject.org/about/overview#thesolution">onion
+    routing</a>," sending packets through a series of encrypted hops
+    before they reach their destination. Therefore, if you see traffic
+    from a Tor node, you may be seeing traffic that originated from
+    someone using Tor, rather than from the node operator itself. The Tor
+    Project and Tor node operators have no records of the traffic that
+    passes over the network, but we do maintain current and historical
+    records of which IP addresses are part of the Tor network.</p>
+    <br>
+    <p>ExoneraTor tells you whether there was a Tor relay running on a
+    given IP address at a given time. ExoneraTor can further indicate
+    whether this relay permitted exiting to a given server and/or TCP
+    port. ExoneraTor learns these facts by parsing the public relay lists
+    and relay descriptors that are collected from the Tor directory
+    authorities and the exit lists collected by TorDNSEL. By inputting an
+    IP address and time, you can determine whether that IP was then a part
+    of the Tor network.</p>
+    <br>
+    <p><font color="red"><b>Notice:</b> This service has moved to:
+    <a href="https://exonerator.torproject.org/">https://exonerator.torproject.org/</a></font></p>
+  </div>
+  </div>
+  <div class="bottom" id="bottom">
+    <%@ include file="footer.jsp"%>
+  </div>
+</body>
+</html>
diff --git a/web/robots.txt b/web/robots.txt
index f3ffac3..c59aca1 100644
--- a/web/robots.txt
+++ b/web/robots.txt
@@ -1,7 +1,5 @@
 User-agent: *
 Disallow: /relay.html
 Disallow: /csv/
-Disallow: /serverdesc
-Disallow: /consensus
 Disallow: /consensus-health.html
 





More information about the tor-commits mailing list