[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[tor-commits] [exonerator/master] Move Java sources to src/main/java/.
commit 172e1498d328a62d0915ab1796ec63202bcdd12e
Author: Karsten Loesing <karsten.loesing@xxxxxxx>
Date: Wed Jul 6 10:59:08 2016 +0200
Move Java sources to src/main/java/.
---
build.xml | 2 +-
.../exonerator/ExoneraTorDatabaseImporter.java | 564 +++++++++++++
.../torproject/exonerator/ExoneraTorServlet.java | 911 +++++++++++++++++++++
.../exonerator/ExoneraTorDatabaseImporter.java | 564 -------------
.../torproject/exonerator/ExoneraTorServlet.java | 911 ---------------------
5 files changed, 1476 insertions(+), 1476 deletions(-)
diff --git a/build.xml b/build.xml
index 0326520..2e4f74d 100644
--- a/build.xml
+++ b/build.xml
@@ -1,7 +1,7 @@
<project default="run" name="exonerator" basedir=".">
<!-- Define build paths. -->
- <property name="sources" value="src"/>
+ <property name="sources" value="src/main/java"/>
<property name="resources" value="res"/>
<property name="classes" value="classes"/>
<property name="libs" value="lib"/>
diff --git a/src/main/java/org/torproject/exonerator/ExoneraTorDatabaseImporter.java b/src/main/java/org/torproject/exonerator/ExoneraTorDatabaseImporter.java
new file mode 100644
index 0000000..41751ca
--- /dev/null
+++ b/src/main/java/org/torproject/exonerator/ExoneraTorDatabaseImporter.java
@@ -0,0 +1,564 @@
+/* Copyright 2011, 2012 The Tor Project
+ * See LICENSE for licensing information */
+package org.torproject.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 org.torproject.descriptor.DescriptorCollector;
+import org.torproject.descriptor.DescriptorSourceFactory;
+
+/* 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();
+ fetchDescriptors();
+ 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 insertStatusentryStatement;
+ private static CallableStatement insertExitlistentryStatement;
+
+ /* Prepare statements for importing data into the database. */
+ private static void prepareDatabaseStatements() {
+ try {
+ insertStatusentryStatement = connection.prepareCall(
+ "{call insert_statusentry(?, ?, ?, ?, ?, ?, ?)}");
+ 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);
+ }
+ }
+
+ /* Fetch recent descriptors from CollecTor. */
+ private static void fetchDescriptors() {
+ DescriptorCollector collector =
+ DescriptorSourceFactory.createDescriptorCollector();
+ collector.collectDescriptors("https://collector.torproject.org",
+ new String[] { "/recent/relay-descriptors/consensuses/",
+ "/recent/exit-lists/" }, 0L, new File(importDirString), true);
+ }
+
+ /* 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.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("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 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;
+ }
+ } 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);
+ }
+ }
+
+ /* 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/main/java/org/torproject/exonerator/ExoneraTorServlet.java b/src/main/java/org/torproject/exonerator/ExoneraTorServlet.java
new file mode 100644
index 0000000..68d79a3
--- /dev/null
+++ b/src/main/java/org/torproject/exonerator/ExoneraTorServlet.java
@@ -0,0 +1,911 @@
+/* Copyright 2011--2015 The Tor Project
+ * See LICENSE for licensing information */
+package org.torproject.exonerator;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.sql.CallableStatement;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.List;
+import java.util.Locale;
+import java.util.ResourceBundle;
+import java.util.SortedSet;
+import java.util.TimeZone;
+import java.util.TreeSet;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+
+import javax.naming.Context;
+import javax.naming.InitialContext;
+import javax.naming.NamingException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.sql.DataSource;
+
+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);
+ }
+ }
+
+ public void doGet(HttpServletRequest request,
+ HttpServletResponse response) throws IOException,
+ ServletException {
+
+ /* Set content type, or the page doesn't render in Chrome. */
+ response.setContentType("text/html");
+
+ /* Start writing response. */
+ PrintWriter out = response.getWriter();
+ this.writeHeader(out);
+
+ /* Find the right resource bundle for the user's locale. */
+ Locale locale = request.getLocale();
+ ResourceBundle rb = ResourceBundle.getBundle("ExoneraTor", locale);
+
+ /* Open a database connection that we'll use to handle the whole
+ * request. */
+ long requestedConnection = System.currentTimeMillis();
+ Connection conn = this.connectToDatabase();
+ if (conn == null) {
+ this.writeSummaryUnableToConnectToDatabase(out, rb);
+ this.writeFooter(out, rb);
+ return;
+ }
+
+ /* Look up first and last date in the database. */
+ long[] firstAndLastDates = this.queryFirstAndLastDatesFromDatabase(
+ conn);
+ if (firstAndLastDates == null) {
+ this.writeSummaryNoData(out, rb);
+ this.writeFooter(out, rb);
+ this.closeDatabaseConnection(conn, requestedConnection);
+ }
+ SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
+ dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ String firstDate = dateFormat.format(firstAndLastDates[0]);
+ String lastDate = dateFormat.format(firstAndLastDates[1]);
+
+ /* Parse parameters. */
+ String ipParameter = request.getParameter("ip");
+ String relayIP = this.parseIpParameter(ipParameter);
+ boolean relayIPHasError = relayIP == null;
+
+ /* Parse timestamp parameter. */
+ String timestampParameter = request.getParameter("timestamp");
+ String timestampStr = this.parseTimestampParameter(
+ timestampParameter);
+ boolean timestampHasError = timestampStr == null;
+
+ /* Check that timestamp is within range. */
+ long timestamp = 0L;
+ boolean timestampOutOfRange = false;
+ if (timestampStr != null && timestampStr.length() > 0) {
+ try {
+ timestamp = dateFormat.parse(timestampParameter).getTime();
+ if (timestamp < firstAndLastDates[0] ||
+ timestamp > firstAndLastDates[1]) {
+ timestampOutOfRange = true;
+ }
+ } catch (ParseException e) {
+ /* Already checked in parseTimestamp(). */
+ }
+ }
+
+ /* Write form. */
+ this.writeForm(out, rb, relayIP, relayIPHasError ||
+ ("".equals(relayIP) && !"".equals(timestampStr)), timestampStr,
+ !relayIPHasError &&
+ !("".equals(relayIP) && !"".equals(timestampStr)) &&
+ (timestampHasError || timestampOutOfRange ||
+ (!"".equals(relayIP) && "".equals(timestampStr))));
+
+ /* If both parameters are empty, don't print any summary and exit.
+ * This is the start page. */
+ if ("".equals(relayIP) && "".equals(timestampStr)) {
+ this.writeFooter(out, rb);
+ this.closeDatabaseConnection(conn, requestedConnection);
+ return;
+ }
+
+ /* If either parameter is empty, print summary with warning message
+ * and exit. */
+ if ("".equals(relayIP) || "".equals(timestampStr)) {
+ if ("".equals(relayIP)) {
+ writeSummaryNoIp(out, rb);
+ } else {
+ writeSummaryNoTimestamp(out, rb);
+ }
+ this.writeFooter(out, rb);
+ this.closeDatabaseConnection(conn, requestedConnection);
+ return;
+ }
+
+ /* If there's a user error, print summary with exit message and
+ * exit. */
+ if (relayIPHasError || timestampHasError || timestampOutOfRange) {
+ if (relayIPHasError) {
+ this.writeSummaryInvalidIp(out, rb, ipParameter);
+ } else if (timestampHasError) {
+ this.writeSummaryInvalidTimestamp(out, rb, timestampParameter);
+ } else if (timestampOutOfRange) {
+ this.writeSummaryTimestampOutsideRange(out, rb, timestampStr,
+ firstDate, lastDate);
+ }
+ this.writeFooter(out, rb);
+ this.closeDatabaseConnection(conn, requestedConnection);
+ return;
+ }
+
+ /* Consider all consensuses published on or within a day of the given
+ * date. */
+ long timestampFrom = timestamp - 24L * 60L * 60L * 1000L;
+ long timestampTo = timestamp + 2 * 24L * 60L * 60L * 1000L - 1L;
+ SimpleDateFormat validAfterTimeFormat = new SimpleDateFormat(
+ "yyyy-MM-dd HH:mm:ss");
+ validAfterTimeFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ String fromValidAfter = validAfterTimeFormat.format(timestampFrom);
+ String toValidAfter = validAfterTimeFormat.format(timestampTo);
+ SortedSet<Long> relevantConsensuses =
+ this.queryKnownConsensusValidAfterTimes(conn, fromValidAfter,
+ toValidAfter);
+ if (relevantConsensuses == null || relevantConsensuses.isEmpty()) {
+ this.writeSummaryNoDataForThisInterval(out, rb);
+ this.writeFooter(out, rb);
+ this.closeDatabaseConnection(conn, requestedConnection);
+ 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. */
+ List<String[]> statusEntries = this.queryStatusEntries(conn, relayIP,
+ timestamp, validAfterTimeFormat);
+
+ /* If we didn't find anything, run another query to find out if there
+ * are relays running on other IP addresses in the same /24 or /48
+ * network and tell the user about it. */
+ List<String> addressesInSameNetwork = null;
+ if (statusEntries.isEmpty()) {
+ addressesInSameNetwork = new ArrayList<String>();
+ if (!relayIP.contains(":")) {
+ String address24 = this.convertIPv4ToHex(relayIP).substring(0, 6);
+ if (address24 != null) {
+ addressesInSameNetwork = this.queryAddressesInSame24(conn,
+ address24, timestamp);
+ }
+ } else {
+ String address48 = this.convertIPv6ToHex(relayIP).substring(
+ 0, 12);
+ if (address48 != null) {
+ addressesInSameNetwork = this.queryAddressesInSame48(conn,
+ address48, timestamp);
+ }
+ }
+ }
+
+ /* Print out result. */
+ if (!statusEntries.isEmpty()) {
+ this.writeSummaryPositive(out, rb, relayIP, timestampStr);
+ this.writeTechnicalDetails(out, rb, relayIP, timestampStr,
+ statusEntries);
+ } else if (addressesInSameNetwork != null &&
+ !addressesInSameNetwork.isEmpty()) {
+ this.writeSummaryAddressesInSameNetwork(out, rb, relayIP,
+ timestampStr, addressesInSameNetwork);
+ } else {
+ this.writeSummaryNegative(out, rb, relayIP, timestampStr);
+ }
+
+ this.writePermanentLink(out, rb, relayIP, timestampStr);
+
+ this.closeDatabaseConnection(conn, requestedConnection);
+ this.writeFooter(out, rb);
+ }
+
+ /* Helper methods for handling the request. */
+
+ private String parseIpParameter(String passedIpParameter) {
+ String relayIP = null;
+ if (passedIpParameter != null && passedIpParameter.length() > 0) {
+ String ipParameter = passedIpParameter.trim();
+ Pattern ipv4AddressPattern = Pattern.compile(
+ "^([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." +
+ "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." +
+ "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." +
+ "([01]?\\d\\d?|2[0-4]\\d|25[0-5])$");
+ Pattern ipv6AddressPattern = Pattern.compile(
+ "^\\[?[0-9a-fA-F:]{3,39}\\]?$");
+ if (ipv4AddressPattern.matcher(ipParameter).matches()) {
+ String[] ipParts = ipParameter.split("\\.");
+ relayIP = Integer.parseInt(ipParts[0]) + "."
+ + Integer.parseInt(ipParts[1]) + "."
+ + Integer.parseInt(ipParts[2]) + "."
+ + Integer.parseInt(ipParts[3]);
+ } else if (ipv6AddressPattern.matcher(ipParameter).matches()) {
+ if (ipParameter.startsWith("[") && ipParameter.endsWith("]")) {
+ ipParameter = ipParameter.substring(1,
+ ipParameter.length() - 1);
+ }
+ StringBuilder addressHex = new StringBuilder();
+ int start = ipParameter.startsWith("::") ? 1 : 0;
+ int end = ipParameter.length()
+ - (ipParameter.endsWith("::") ? 1 : 0);
+ String[] parts = ipParameter.substring(start, end).split(":", -1);
+ for (int i = 0; i < parts.length; i++) {
+ String part = parts[i];
+ if (part.length() == 0) {
+ addressHex.append("x");
+ } else if (part.length() <= 4) {
+ addressHex.append(String.format("%4s", part));
+ } else {
+ addressHex = null;
+ break;
+ }
+ }
+ if (addressHex != null) {
+ String addressHexString = addressHex.toString();
+ addressHexString = addressHexString.replaceFirst("x",
+ String.format("%" + (33 - addressHexString.length()) + "s",
+ "0"));
+ if (!addressHexString.contains("x") &&
+ addressHexString.length() == 32) {
+ relayIP = ipParameter.toLowerCase();
+ }
+ }
+ }
+ } else {
+ relayIP = "";
+ }
+ return relayIP;
+ }
+
+ private String convertIPv4ToHex(String relayIP) {
+ String[] relayIPParts = relayIP.split("\\.");
+ byte[] address24Bytes = new byte[4];
+ for (int i = 0; i < address24Bytes.length; i++) {
+ address24Bytes[i] = (byte) Integer.parseInt(relayIPParts[i]);
+ }
+ String address24 = Hex.encodeHexString(address24Bytes);
+ return address24;
+ }
+
+ private String convertIPv6ToHex(String relayIP) {
+ if (relayIP.startsWith("[") && relayIP.endsWith("]")) {
+ relayIP = relayIP.substring(1, relayIP.length() - 1);
+ }
+ StringBuilder addressHex = new StringBuilder();
+ int start = relayIP.startsWith("::") ? 1 : 0;
+ int end = relayIP.length() - (relayIP.endsWith("::") ? 1 : 0);
+ String[] parts = relayIP.substring(start, end).split(":", -1);
+ for (int i = 0; i < parts.length; i++) {
+ String part = parts[i];
+ if (part.length() == 0) {
+ addressHex.append("x");
+ } else if (part.length() <= 4) {
+ addressHex.append(String.format("%4s", part));
+ } else {
+ addressHex = null;
+ break;
+ }
+ }
+ String address48 = null;
+ if (addressHex != null) {
+ String addressHexString = addressHex.toString();
+ addressHexString = addressHexString.replaceFirst("x",
+ String.format("%" + (33 - addressHexString.length())
+ + "s", "0"));
+ if (!addressHexString.contains("x") &&
+ addressHexString.length() == 32) {
+ address48 = addressHexString.replaceAll(" ", "0").
+ toLowerCase();
+ }
+ }
+ return address48;
+ }
+
+ private String parseTimestampParameter(
+ String passedTimestampParameter) {
+ String timestampStr = "";
+ SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
+ dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ dateFormat.setLenient(false);
+ if (passedTimestampParameter != null &&
+ passedTimestampParameter.length() > 0) {
+ String timestampParameter = passedTimestampParameter.trim();
+ try {
+ long timestamp = dateFormat.parse(timestampParameter).getTime();
+ timestampStr = dateFormat.format(timestamp);
+ } catch (ParseException e) {
+ timestampStr = null;
+ }
+ }
+ return timestampStr;
+ }
+
+ /* Helper methods for querying the database. */
+
+ private Connection connectToDatabase() {
+ Connection conn = null;
+ try {
+ conn = this.ds.getConnection();
+ } catch (SQLException e) {
+ }
+ return conn;
+ }
+
+ private long[] queryFirstAndLastDatesFromDatabase(Connection conn) {
+ long[] firstAndLastDates = null;
+ try {
+ Statement statement = conn.createStatement();
+ String query = "SELECT DATE(MIN(validafter)) AS first, "
+ + "DATE(MAX(validafter)) AS last FROM statusentry";
+ ResultSet rs = statement.executeQuery(query);
+ if (rs.next()) {
+ Calendar utcCalendar = Calendar.getInstance(
+ TimeZone.getTimeZone("UTC"));
+ firstAndLastDates = new long[] {
+ rs.getTimestamp(1, utcCalendar).getTime(),
+ rs.getTimestamp(2, utcCalendar).getTime()
+ };
+ }
+ rs.close();
+ statement.close();
+ } catch (SQLException e) {
+ /* Looks like we don't have any consensuses. */
+ firstAndLastDates = null;
+ }
+ return firstAndLastDates;
+ }
+
+ private SortedSet<Long> queryKnownConsensusValidAfterTimes(
+ Connection conn, String fromValidAfter, String toValidAfter) {
+ SortedSet<Long> relevantConsensuses = new TreeSet<Long>();
+ try {
+ Statement statement = conn.createStatement();
+ String query = "SELECT DISTINCT validafter FROM statusentry "
+ + "WHERE validafter >= '" + fromValidAfter
+ + "' AND validafter <= '" + toValidAfter + "'";
+ ResultSet rs = statement.executeQuery(query);
+ while (rs.next()) {
+ long consensusTime = rs.getTimestamp(1).getTime();
+ relevantConsensuses.add(consensusTime);
+ }
+ rs.close();
+ statement.close();
+ } catch (SQLException e) {
+ /* Looks like we don't have any consensuses in the requested
+ * interval. */
+ relevantConsensuses = null;
+ }
+ return relevantConsensuses;
+ }
+
+ private List<String[]> queryStatusEntries(Connection conn,
+ String relayIP, long timestamp,
+ SimpleDateFormat validAfterTimeFormat) {
+ List<String[]> statusEntries = new ArrayList<String[]>();
+ String addressHex = !relayIP.contains(":")
+ ? this.convertIPv4ToHex(relayIP) : this.convertIPv6ToHex(relayIP);
+ if (addressHex == null) {
+ return null;
+ }
+ String address24Or48Hex = !relayIP.contains(":")
+ ? addressHex.substring(0, 6) : addressHex.substring(0, 12);
+ try {
+ CallableStatement cs;
+ if (!relayIP.contains(":")) {
+ cs = conn.prepareCall("{call search_by_address24_date(?, ?)}");
+ } else {
+ cs = conn.prepareCall("{call search_by_address48_date(?, ?)}");
+ }
+ cs.setString(1, address24Or48Hex);
+ Calendar utcCalendar = Calendar.getInstance(
+ TimeZone.getTimeZone("UTC"));
+ cs.setDate(2, new java.sql.Date(timestamp), utcCalendar);
+ ResultSet rs = cs.executeQuery();
+ while (rs.next()) {
+ byte[] rawstatusentry = rs.getBytes(1);
+ SortedSet<String> addresses = new TreeSet<String>(),
+ addressesHex = new TreeSet<String>();
+ long validafter = rs.getTimestamp(2, utcCalendar).getTime();
+ String validAfterString = validAfterTimeFormat.format(validafter);
+ String fingerprint = rs.getString(3).toUpperCase();
+ String nickname = null;
+ String exit = "U";
+ for (String line : new String(rawstatusentry).split("\n")) {
+ if (line.startsWith("r ")) {
+ String[] parts = line.split(" ");
+ nickname = parts[1];
+ addresses.add(parts[6]);
+ addressesHex.add(this.convertIPv4ToHex(parts[6]));
+ } else if (line.startsWith("a ")) {
+ String address = line.substring("a ".length(),
+ line.lastIndexOf(":"));
+ addresses.add(address);
+ String orAddressHex = !address.contains(":")
+ ? this.convertIPv4ToHex(address)
+ : this.convertIPv6ToHex(address);
+ addressesHex.add(orAddressHex);
+ } else if (line.startsWith("p ")) {
+ exit = line.equals("p reject 1-65535") ? "N" : "Y";
+ }
+ }
+ String exitaddress = rs.getString(4);
+ if (exitaddress != null && exitaddress.length() > 0) {
+ addresses.add(exitaddress);
+ addressesHex.add(this.convertIPv4ToHex(exitaddress));
+ }
+ if (!addressesHex.contains(addressHex)) {
+ continue;
+ }
+ StringBuilder sb = new StringBuilder();
+ int writtenAddresses = 0;
+ for (String address : addresses) {
+ sb.append((writtenAddresses++ > 0 ? ", " : "") + address);
+ }
+ String[] statusEntry = new String[] { validAfterString,
+ sb.toString(), fingerprint, nickname, exit };
+ statusEntries.add(statusEntry);
+ }
+ rs.close();
+ cs.close();
+ } catch (SQLException e) {
+ /* Nothing found. */
+ statusEntries = null;
+ }
+ return statusEntries;
+ }
+
+ private List<String> queryAddressesInSame24(Connection conn,
+ String address24, long timestamp) {
+ List<String> addressesInSameNetwork = new ArrayList<String>();
+ try {
+ CallableStatement cs = conn.prepareCall(
+ "{call search_addresses_in_same_24 (?, ?)}");
+ cs.setString(1, address24);
+ cs.setDate(2, new java.sql.Date(timestamp));
+ ResultSet rs = cs.executeQuery();
+ while (rs.next()) {
+ String address = rs.getString(1);
+ if (!addressesInSameNetwork.contains(address)) {
+ addressesInSameNetwork.add(address);
+ }
+ }
+ rs.close();
+ cs.close();
+ } catch (SQLException e) {
+ /* No other addresses in the same /24 found. */
+ addressesInSameNetwork = null;
+ }
+ return addressesInSameNetwork;
+ }
+
+ private List<String> queryAddressesInSame48(Connection conn,
+ String address48, long timestamp) {
+ List<String> addressesInSameNetwork = new ArrayList<String>();
+ try {
+ CallableStatement cs = conn.prepareCall(
+ "{call search_addresses_in_same_48 (?, ?)}");
+ cs.setString(1, address48);
+ cs.setDate(2, new java.sql.Date(timestamp));
+ ResultSet rs = cs.executeQuery();
+ while (rs.next()) {
+ String address = rs.getString(1);
+ if (!addressesInSameNetwork.contains(address)) {
+ addressesInSameNetwork.add(address);
+ }
+ }
+ rs.close();
+ cs.close();
+ } catch (SQLException e) {
+ /* No other addresses in the same /48 found. */
+ addressesInSameNetwork = null;
+ }
+ return addressesInSameNetwork;
+ }
+
+ private void closeDatabaseConnection(Connection conn,
+ long requestedConnection) {
+ try {
+ conn.close();
+ this.logger.info("Returned a database connection to the pool "
+ + "after " + (System.currentTimeMillis()
+ - requestedConnection) + " millis.");
+ } catch (SQLException e) {
+ }
+ return;
+ }
+
+ /* Helper methods for writing the response. */
+
+ private void writeHeader(PrintWriter out) throws IOException {
+ out.println("<!DOCTYPE html>\n"
+ + "<html lang=\"en\">\n"
+ + " <head>\n"
+ + " <meta charset=\"utf-8\">\n"
+ + " <meta http-equiv=\"X-UA-Compatible\" "
+ + "content=\"IE=edge\">\n"
+ + " <meta name=\"viewport\" content=\"width=device-width, "
+ + "initial-scale=1\">\n"
+ + " <title>ExoneraTor</title>\n"
+ + " <link rel=\"stylesheet\" href=\"css/bootstrap.min.css\">\n"
+ + " <link rel=\"stylesheet\" href=\"css/exonerator.css\">\n"
+ + " <link href=\"images/favicon.ico\" type=\"image/x-icon\" "
+ + "rel=\"icon\">\n"
+ + " </head>\n"
+ + " <body>\n"
+ + " <div class=\"container\">\n"
+ + " <div class=\"row\">\n"
+ + " <div class=\"col-xs-12\">\n"
+ + " <div class=\"page-header\">\n"
+ + " <h1>\n"
+ + " <div class=\"text-center\">\n"
+ + " <a href=\"/\">"
+ + "<img src=\"images/exonerator-logo.png\" "
+ + "width=\"334\" height=\"252\" alt=\"ExoneraTor logo\">"
+ + "<img src=\"images/exonerator-wordmark.png\" width=\"428\" "
+ + "height=\"63\" alt=\"ExoneraTor wordmark\"></a>\n"
+ + " </div><!-- text-center -->\n"
+ + " </h1>\n"
+ + " </div><!-- page-header -->\n"
+ + " </div><!-- col -->\n"
+ + " </div><!-- row -->\n");
+ }
+
+ private void writeForm(PrintWriter out, ResourceBundle rb,
+ String relayIP, boolean relayIPHasError, String timestampStr,
+ boolean timestampHasError) throws IOException {
+ String ipValue = "";
+ if (relayIP != null && relayIP.length() > 0) {
+ if (relayIP.contains(":")) {
+ ipValue = String.format(" value=\"[%s]\"", relayIP);
+ } else {
+ ipValue = String.format(" value=\"%s\"", relayIP);
+ }
+ }
+ out.printf(" <div class=\"row\">\n"
+ + " <div class=\"col-xs-12\">\n"
+ + " <div class=\"text-center\">\n"
+ + " <div class=\"row vbottom15\">\n"
+ + " <h4>%s</h4>\n"
+ + " </div> <!-- row -->\n"
+ + " <form class=\"form-inline\">\n"
+ + " <div class=\"form-group%s\">\n"
+ + " <label for=\"inputIp\" "
+ + "class=\"control-label\">%s</label>\n"
+ + " <input type=\"text\" class=\"form-control\" "
+ + "name=\"ip\" id=\"inputIp\" placeholder=\"86.59.21.38\"%s "
+ + "required>\n"
+ + " </div><!-- form-group -->\n"
+ + " <div class=\"form-group%s\">\n"
+ + " <label for=\"inputTimestamp\" "
+ + "class=\"control-label\">%s</label>\n"
+ + " <input type=\"date\" class=\"form-control\" "
+ + "name=\"timestamp\" id=\"inputTimestamp\" "
+ + "placeholder=\"2010-01-01\"%s required>\n"
+ + " </div><!-- form-group -->\n"
+ + " <button type=\"submit\" "
+ + "class=\"btn btn-primary\">%s</button>\n"
+ + " </form>\n"
+ + " </div><!-- text-center -->\n"
+ + " </div><!-- col -->\n"
+ + " </div><!-- row -->\n",
+ rb.getString("form.explanation"),
+ relayIPHasError ? " has-error" : "",
+ rb.getString("form.ip.label"),
+ ipValue,
+ timestampHasError ? " has-error" : "",
+ rb.getString("form.timestamp.label"),
+ timestampStr != null && timestampStr.length() > 0 ?
+ " value=\"" + timestampStr + "\"" : "",
+ rb.getString("form.search.label"));
+ }
+
+ private void writeSummaryUnableToConnectToDatabase(PrintWriter out,
+ ResourceBundle rb) throws IOException {
+ String contactLink =
+ "<a href=\"https://www.torproject.org/about/contact\">"
+ + rb.getString("summary.serverproblem.dbempty.body.link")
+ + "</a>";
+ this.writeSummary(out, rb.getString("summary.heading"),
+ "panel-danger",
+ rb.getString("summary.serverproblem.dbnoconnect.title"), null,
+ rb.getString("summary.serverproblem.dbnoconnect.body.text"),
+ contactLink);
+ }
+
+ private void writeSummaryNoData(PrintWriter out, ResourceBundle rb)
+ throws IOException {
+ String contactLink =
+ "<a href=\"https://www.torproject.org/about/contact\">"
+ + rb.getString("summary.serverproblem.dbempty.body.link")
+ + "</a>";
+ this.writeSummary(out, rb.getString("summary.heading"),
+ "panel-danger",
+ rb.getString("summary.serverproblem.dbempty.title"), null,
+ rb.getString("summary.serverproblem.dbempty.body.text"),
+ contactLink);
+ }
+
+ private void writeSummaryNoTimestamp(PrintWriter out, ResourceBundle rb)
+ throws IOException {
+ this.writeSummary(out, rb.getString("summary.heading"),
+ "panel-danger",
+ rb.getString("summary.invalidparams.notimestamp.title"), null,
+ rb.getString("summary.invalidparams.notimestamp.body"));
+ }
+
+ private void writeSummaryNoIp(PrintWriter out, ResourceBundle rb)
+ throws IOException {
+ this.writeSummary(out, rb.getString("summary.heading"),
+ "panel-danger", rb.getString("summary.invalidparams.noip.title"),
+ null, rb.getString("summary.invalidparams.noip.body"));
+ }
+
+ private void writeSummaryTimestampOutsideRange(PrintWriter out,
+ ResourceBundle rb, String timestampStr, String firstDate,
+ String lastDate) throws IOException {
+ this.writeSummary(out, rb.getString("summary.heading"),
+ "panel-danger",
+ rb.getString("summary.invalidparams.timestamprange.title"), null,
+ rb.getString("summary.invalidparams.timestamprange.body"),
+ timestampStr, firstDate, lastDate);
+ }
+
+ private void writeSummaryInvalidIp(PrintWriter out, ResourceBundle rb,
+ String ipParameter) throws IOException {
+ String escapedIpParameter = ipParameter.length() > 40 ?
+ StringEscapeUtils.escapeHtml(ipParameter.substring(0, 40))
+ + "[...]" : StringEscapeUtils.escapeHtml(ipParameter);
+ this.writeSummary(out, rb.getString("summary.heading"),
+ "panel-danger",
+ rb.getString("summary.invalidparams.invalidip.title"), null,
+ rb.getString("summary.invalidparams.invalidip.body"),
+ escapedIpParameter, "\"a.b.c.d\"", "\"[a:b:c:d:e:f:g:h]\"");
+ }
+
+ private void writeSummaryInvalidTimestamp(PrintWriter out,
+ ResourceBundle rb, String timestampParameter) throws IOException {
+ String escapedTimestampParameter = timestampParameter.length() > 20 ?
+ StringEscapeUtils.escapeHtml(timestampParameter.
+ substring(0, 20)) + "[...]" :
+ StringEscapeUtils.escapeHtml(timestampParameter);
+ this.writeSummary(out, rb.getString("summary.heading"),
+ "panel-danger",
+ rb.getString("summary.invalidparams.invalidtimestamp.title"),
+ null, rb.getString("summary.invalidparams.invalidtimestamp.body"),
+ escapedTimestampParameter, "\"YYYY-MM-DD\"");
+ }
+
+ private void writeSummaryNoDataForThisInterval(PrintWriter out,
+ ResourceBundle rb) throws IOException {
+ String contactLink =
+ "<a href=\"https://www.torproject.org/about/contact\">"
+ + rb.getString("summary.serverproblem.dbempty.body.link")
+ + "</a>";
+ this.writeSummary(out, rb.getString("summary.heading"),
+ "panel-danger",
+ rb.getString("summary.serverproblem.nodata.title"), null,
+ rb.getString("summary.serverproblem.nodata.body.text"),
+ contactLink);
+ }
+
+ private void writeSummaryAddressesInSameNetwork(PrintWriter out,
+ ResourceBundle rb, String relayIP, String timestampStr,
+ List<String> addressesInSameNetwork) throws IOException {
+ Object[][] panelItems = new Object[addressesInSameNetwork.size()][];
+ for (int i = 0; i < addressesInSameNetwork.size(); i++) {
+ String addressInSameNetwork = addressesInSameNetwork.get(i);
+ String link, address;
+ if (addressInSameNetwork.contains(":")) {
+ link = String.format("/?ip=[%s]×tamp=%s",
+ addressInSameNetwork.replaceAll(":", "%3A"), timestampStr);
+ address = "[" + addressInSameNetwork + "]";
+ } else {
+ link = String.format("/?ip=%s×tamp=%s",
+ addressInSameNetwork, timestampStr);
+ address = addressInSameNetwork;
+ }
+ panelItems[i] = new Object[] { link, address };
+ }
+ this.writeSummary(out, rb.getString("summary.heading"),
+ "panel-warning",
+ rb.getString("summary.negativesamenetwork.title"), panelItems,
+ rb.getString("summary.negativesamenetwork.body"),
+ relayIP, timestampStr, relayIP.contains(":") ? 48 : 24);
+ }
+
+ private void writeSummaryPositive(PrintWriter out, ResourceBundle rb,
+ String relayIP, String timestampStr) throws IOException {
+ String formattedRelayIP = relayIP.contains(":") ?
+ "[" + relayIP + "]" : relayIP;
+ this.writeSummary(out, rb.getString("summary.heading"),
+ "panel-success", rb.getString("summary.positive.title"), null,
+ rb.getString("summary.positive.body"), formattedRelayIP,
+ timestampStr);
+ }
+
+ private void writeSummaryNegative(PrintWriter out, ResourceBundle rb,
+ String relayIP, String timestampStr) throws IOException {
+ String formattedRelayIP = relayIP.contains(":") ?
+ "[" + relayIP + "]" : relayIP;
+ this.writeSummary(out, rb.getString("summary.heading"),
+ "panel-warning", rb.getString("summary.negative.title"), null,
+ rb.getString("summary.negative.body"), formattedRelayIP,
+ timestampStr);
+ }
+
+ private void writeSummary(PrintWriter out, String heading,
+ String panelContext, String panelTitle, Object[][] panelItems,
+ String panelBodyTemplate, Object... panelBodyArgs)
+ throws IOException {
+ out.printf(" <div class=\"row\">\n"
+ + " <div class=\"col-xs-12\">\n"
+ + " <h2>%s</h2>\n"
+ + " <div class=\"panel %s\">\n"
+ + " <div class=\"panel-heading\">\n"
+ + " <h3 class=\"panel-title\">%s</h3>\n"
+ + " </div><!-- panel-heading -->\n"
+ + " <div class=\"panel-body\">\n"
+ + " <p>%s</p>\n", heading, panelContext, panelTitle,
+ String.format(panelBodyTemplate, panelBodyArgs));
+ if (panelItems != null) {
+ out.print(" <ul>\n");
+ for (Object[] panelItem : panelItems) {
+ out.printf(" <li><a href=\"%s\">%s</a></li>\n",
+ panelItem);
+ }
+ out.print(" </ul>\n");
+ }
+ out.print(" </div><!-- panel-body -->\n"
+ + " </div><!-- panel -->\n"
+ + " </div><!-- col -->\n"
+ + " </div><!-- row -->\n");
+ }
+
+ private void writeTechnicalDetails(PrintWriter out, ResourceBundle rb,
+ String relayIP, String timestampStr, List<String[]> tableRows)
+ throws IOException {
+ String formattedRelayIP = relayIP.contains(":") ?
+ "[" + relayIP + "]" : relayIP;
+ out.printf(" <div class=\"row\">\n"
+ + " <div class=\"col-xs-12\">\n"
+ + " <h2>%s</h2>\n"
+ + " <p>%s</p>\n"
+ + " <table class=\"table\">\n"
+ + " <thead>\n"
+ + " <tr>\n"
+ + " <th>%s</th>\n"
+ + " <th>%s</th>\n"
+ + " <th>%s</th>\n"
+ + " <th>%s</th>\n"
+ + " <th>%s</th>\n"
+ + " </tr>\n"
+ + " </thead>\n"
+ + " <tbody>\n",
+ rb.getString("technicaldetails.heading"),
+ String.format(rb.getString("technicaldetails.pre"),
+ formattedRelayIP, timestampStr),
+ rb.getString("technicaldetails.colheader.timestamp"),
+ rb.getString("technicaldetails.colheader.ip"),
+ rb.getString("technicaldetails.colheader.fingerprint"),
+ rb.getString("technicaldetails.colheader.nickname"),
+ rb.getString("technicaldetails.colheader.exit"));
+ for (String[] tableRow : tableRows) {
+ out.print(" <tr>");
+ for (int i = 0; i < tableRow.length; i++) {
+ String content = tableRow[i];
+ if (i == 2) {
+ content = content.substring(0, 20) + "​"
+ + content.substring(20, 40);
+ } else if (i == 3 && content == null) {
+ content = "("
+ + rb.getString("technicaldetails.nickname.unknown") + ")";
+ } else if (i == 4) {
+ if (content.equals("U")) {
+ content = rb.getString("technicaldetails.exit.unknown");
+ } else if (content.equals("Y")) {
+ content = rb.getString("technicaldetails.exit.yes");
+ } else {
+ content = rb.getString("technicaldetails.exit.no");
+ }
+ }
+ out.print(" <td>" + content + "</td>");
+ }
+ out.print(" </tr>\n");
+ }
+ out.print(" </tbody>\n"
+ + " </table>\n"
+ + " </div><!-- col -->\n"
+ + " </div><!-- row -->\n");
+ }
+
+ private void writePermanentLink(PrintWriter out, ResourceBundle rb,
+ String relayIP, String timestampStr) throws IOException {
+ String encodedAddress = relayIP.contains(":") ?
+ "[" + relayIP.replaceAll(":", "%3A") + "]" : relayIP;
+ out.printf(" <div class=\"row\">\n"
+ + " <div class=\"col-xs-12\">\n"
+ + " <h2>%s</h2>\n"
+ + " <pre>https://exonerator.torproject.org/?ip=%s&"
+ + "timestamp=%s</pre>\n"
+ + " </div><!-- col -->\n"
+ + " </div><!-- row -->\n",
+ rb.getString("permanentlink.heading"),
+ encodedAddress, timestampStr);
+ }
+
+ private void writeFooter(PrintWriter out, ResourceBundle rb)
+ throws IOException {
+ out.printf(" </div><!-- container -->\n"
+ + " <div class=\"footer\">\n"
+ + " <div class=\"container\">\n"
+ + " <div class=\"row\">\n"
+ + " <div class=\"col-xs-6\">\n"
+ + " <h3>%s</h3>\n"
+ + " <p class=\"small\">%s</p>\n"
+ + " </div><!-- col -->\n",
+ rb.getString("footer.abouttor.heading"),
+ String.format(rb.getString("footer.abouttor.body.text"),
+ "<a href=\"https://www.torproject.org/about/"
+ + "overview#thesolution\">"
+ + rb.getString("footer.abouttor.body.link1") + "</a>",
+ "<a href=\"https://www.torproject.org/about/overview\">"
+ + rb.getString("footer.abouttor.body.link2") + "</a>",
+ "<a href=\"https://www.torproject.org/about/contact\">"
+ + rb.getString("footer.abouttor.body.link3") + "</a>"));
+ out.printf(" <div class=\"col-xs-6\">\n"
+ + " <h3>%s</h3>\n"
+ + " <p class=\"small\">%s</p>\n"
+ + " </div><!-- col -->\n"
+ + " </div><!-- row -->\n"
+ + " <div class=\"row\">\n",
+ rb.getString("footer.aboutexonerator.heading"),
+ rb.getString("footer.aboutexonerator.body"));
+ out.printf(" <div class=\"col-xs-12\">\n"
+ + " <p class=\"text-center small\">%s</p>\n"
+ + " </div><!-- col -->\n"
+ + " </div><!-- row -->\n"
+ + " </div><!-- container -->\n"
+ + " </div><!-- footer -->\n"
+ + " </body>\n"
+ + "</html>\n",
+ String.format(rb.getString("footer.trademark.text"),
+ "<a href=\"https://www.torproject.org/docs/"
+ + "trademark-faq.html.en\">"
+ + rb.getString("footer.trademark.link") + "</a>"));
+ out.close();
+ }
+}
+
diff --git a/src/org/torproject/exonerator/ExoneraTorDatabaseImporter.java b/src/org/torproject/exonerator/ExoneraTorDatabaseImporter.java
deleted file mode 100644
index 41751ca..0000000
--- a/src/org/torproject/exonerator/ExoneraTorDatabaseImporter.java
+++ /dev/null
@@ -1,564 +0,0 @@
-/* Copyright 2011, 2012 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.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 org.torproject.descriptor.DescriptorCollector;
-import org.torproject.descriptor.DescriptorSourceFactory;
-
-/* 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();
- fetchDescriptors();
- 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 insertStatusentryStatement;
- private static CallableStatement insertExitlistentryStatement;
-
- /* Prepare statements for importing data into the database. */
- private static void prepareDatabaseStatements() {
- try {
- insertStatusentryStatement = connection.prepareCall(
- "{call insert_statusentry(?, ?, ?, ?, ?, ?, ?)}");
- 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);
- }
- }
-
- /* Fetch recent descriptors from CollecTor. */
- private static void fetchDescriptors() {
- DescriptorCollector collector =
- DescriptorSourceFactory.createDescriptorCollector();
- collector.collectDescriptors("https://collector.torproject.org",
- new String[] { "/recent/relay-descriptors/consensuses/",
- "/recent/exit-lists/" }, 0L, new File(importDirString), true);
- }
-
- /* 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.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("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 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;
- }
- } 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);
- }
- }
-
- /* 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/exonerator/ExoneraTorServlet.java b/src/org/torproject/exonerator/ExoneraTorServlet.java
deleted file mode 100644
index 68d79a3..0000000
--- a/src/org/torproject/exonerator/ExoneraTorServlet.java
+++ /dev/null
@@ -1,911 +0,0 @@
-/* Copyright 2011--2015 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.exonerator;
-
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.sql.CallableStatement;
-import java.sql.Connection;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.List;
-import java.util.Locale;
-import java.util.ResourceBundle;
-import java.util.SortedSet;
-import java.util.TimeZone;
-import java.util.TreeSet;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import java.util.regex.Pattern;
-
-import javax.naming.Context;
-import javax.naming.InitialContext;
-import javax.naming.NamingException;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.sql.DataSource;
-
-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);
- }
- }
-
- public void doGet(HttpServletRequest request,
- HttpServletResponse response) throws IOException,
- ServletException {
-
- /* Set content type, or the page doesn't render in Chrome. */
- response.setContentType("text/html");
-
- /* Start writing response. */
- PrintWriter out = response.getWriter();
- this.writeHeader(out);
-
- /* Find the right resource bundle for the user's locale. */
- Locale locale = request.getLocale();
- ResourceBundle rb = ResourceBundle.getBundle("ExoneraTor", locale);
-
- /* Open a database connection that we'll use to handle the whole
- * request. */
- long requestedConnection = System.currentTimeMillis();
- Connection conn = this.connectToDatabase();
- if (conn == null) {
- this.writeSummaryUnableToConnectToDatabase(out, rb);
- this.writeFooter(out, rb);
- return;
- }
-
- /* Look up first and last date in the database. */
- long[] firstAndLastDates = this.queryFirstAndLastDatesFromDatabase(
- conn);
- if (firstAndLastDates == null) {
- this.writeSummaryNoData(out, rb);
- this.writeFooter(out, rb);
- this.closeDatabaseConnection(conn, requestedConnection);
- }
- SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
- dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
- String firstDate = dateFormat.format(firstAndLastDates[0]);
- String lastDate = dateFormat.format(firstAndLastDates[1]);
-
- /* Parse parameters. */
- String ipParameter = request.getParameter("ip");
- String relayIP = this.parseIpParameter(ipParameter);
- boolean relayIPHasError = relayIP == null;
-
- /* Parse timestamp parameter. */
- String timestampParameter = request.getParameter("timestamp");
- String timestampStr = this.parseTimestampParameter(
- timestampParameter);
- boolean timestampHasError = timestampStr == null;
-
- /* Check that timestamp is within range. */
- long timestamp = 0L;
- boolean timestampOutOfRange = false;
- if (timestampStr != null && timestampStr.length() > 0) {
- try {
- timestamp = dateFormat.parse(timestampParameter).getTime();
- if (timestamp < firstAndLastDates[0] ||
- timestamp > firstAndLastDates[1]) {
- timestampOutOfRange = true;
- }
- } catch (ParseException e) {
- /* Already checked in parseTimestamp(). */
- }
- }
-
- /* Write form. */
- this.writeForm(out, rb, relayIP, relayIPHasError ||
- ("".equals(relayIP) && !"".equals(timestampStr)), timestampStr,
- !relayIPHasError &&
- !("".equals(relayIP) && !"".equals(timestampStr)) &&
- (timestampHasError || timestampOutOfRange ||
- (!"".equals(relayIP) && "".equals(timestampStr))));
-
- /* If both parameters are empty, don't print any summary and exit.
- * This is the start page. */
- if ("".equals(relayIP) && "".equals(timestampStr)) {
- this.writeFooter(out, rb);
- this.closeDatabaseConnection(conn, requestedConnection);
- return;
- }
-
- /* If either parameter is empty, print summary with warning message
- * and exit. */
- if ("".equals(relayIP) || "".equals(timestampStr)) {
- if ("".equals(relayIP)) {
- writeSummaryNoIp(out, rb);
- } else {
- writeSummaryNoTimestamp(out, rb);
- }
- this.writeFooter(out, rb);
- this.closeDatabaseConnection(conn, requestedConnection);
- return;
- }
-
- /* If there's a user error, print summary with exit message and
- * exit. */
- if (relayIPHasError || timestampHasError || timestampOutOfRange) {
- if (relayIPHasError) {
- this.writeSummaryInvalidIp(out, rb, ipParameter);
- } else if (timestampHasError) {
- this.writeSummaryInvalidTimestamp(out, rb, timestampParameter);
- } else if (timestampOutOfRange) {
- this.writeSummaryTimestampOutsideRange(out, rb, timestampStr,
- firstDate, lastDate);
- }
- this.writeFooter(out, rb);
- this.closeDatabaseConnection(conn, requestedConnection);
- return;
- }
-
- /* Consider all consensuses published on or within a day of the given
- * date. */
- long timestampFrom = timestamp - 24L * 60L * 60L * 1000L;
- long timestampTo = timestamp + 2 * 24L * 60L * 60L * 1000L - 1L;
- SimpleDateFormat validAfterTimeFormat = new SimpleDateFormat(
- "yyyy-MM-dd HH:mm:ss");
- validAfterTimeFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
- String fromValidAfter = validAfterTimeFormat.format(timestampFrom);
- String toValidAfter = validAfterTimeFormat.format(timestampTo);
- SortedSet<Long> relevantConsensuses =
- this.queryKnownConsensusValidAfterTimes(conn, fromValidAfter,
- toValidAfter);
- if (relevantConsensuses == null || relevantConsensuses.isEmpty()) {
- this.writeSummaryNoDataForThisInterval(out, rb);
- this.writeFooter(out, rb);
- this.closeDatabaseConnection(conn, requestedConnection);
- 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. */
- List<String[]> statusEntries = this.queryStatusEntries(conn, relayIP,
- timestamp, validAfterTimeFormat);
-
- /* If we didn't find anything, run another query to find out if there
- * are relays running on other IP addresses in the same /24 or /48
- * network and tell the user about it. */
- List<String> addressesInSameNetwork = null;
- if (statusEntries.isEmpty()) {
- addressesInSameNetwork = new ArrayList<String>();
- if (!relayIP.contains(":")) {
- String address24 = this.convertIPv4ToHex(relayIP).substring(0, 6);
- if (address24 != null) {
- addressesInSameNetwork = this.queryAddressesInSame24(conn,
- address24, timestamp);
- }
- } else {
- String address48 = this.convertIPv6ToHex(relayIP).substring(
- 0, 12);
- if (address48 != null) {
- addressesInSameNetwork = this.queryAddressesInSame48(conn,
- address48, timestamp);
- }
- }
- }
-
- /* Print out result. */
- if (!statusEntries.isEmpty()) {
- this.writeSummaryPositive(out, rb, relayIP, timestampStr);
- this.writeTechnicalDetails(out, rb, relayIP, timestampStr,
- statusEntries);
- } else if (addressesInSameNetwork != null &&
- !addressesInSameNetwork.isEmpty()) {
- this.writeSummaryAddressesInSameNetwork(out, rb, relayIP,
- timestampStr, addressesInSameNetwork);
- } else {
- this.writeSummaryNegative(out, rb, relayIP, timestampStr);
- }
-
- this.writePermanentLink(out, rb, relayIP, timestampStr);
-
- this.closeDatabaseConnection(conn, requestedConnection);
- this.writeFooter(out, rb);
- }
-
- /* Helper methods for handling the request. */
-
- private String parseIpParameter(String passedIpParameter) {
- String relayIP = null;
- if (passedIpParameter != null && passedIpParameter.length() > 0) {
- String ipParameter = passedIpParameter.trim();
- Pattern ipv4AddressPattern = Pattern.compile(
- "^([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." +
- "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." +
- "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." +
- "([01]?\\d\\d?|2[0-4]\\d|25[0-5])$");
- Pattern ipv6AddressPattern = Pattern.compile(
- "^\\[?[0-9a-fA-F:]{3,39}\\]?$");
- if (ipv4AddressPattern.matcher(ipParameter).matches()) {
- String[] ipParts = ipParameter.split("\\.");
- relayIP = Integer.parseInt(ipParts[0]) + "."
- + Integer.parseInt(ipParts[1]) + "."
- + Integer.parseInt(ipParts[2]) + "."
- + Integer.parseInt(ipParts[3]);
- } else if (ipv6AddressPattern.matcher(ipParameter).matches()) {
- if (ipParameter.startsWith("[") && ipParameter.endsWith("]")) {
- ipParameter = ipParameter.substring(1,
- ipParameter.length() - 1);
- }
- StringBuilder addressHex = new StringBuilder();
- int start = ipParameter.startsWith("::") ? 1 : 0;
- int end = ipParameter.length()
- - (ipParameter.endsWith("::") ? 1 : 0);
- String[] parts = ipParameter.substring(start, end).split(":", -1);
- for (int i = 0; i < parts.length; i++) {
- String part = parts[i];
- if (part.length() == 0) {
- addressHex.append("x");
- } else if (part.length() <= 4) {
- addressHex.append(String.format("%4s", part));
- } else {
- addressHex = null;
- break;
- }
- }
- if (addressHex != null) {
- String addressHexString = addressHex.toString();
- addressHexString = addressHexString.replaceFirst("x",
- String.format("%" + (33 - addressHexString.length()) + "s",
- "0"));
- if (!addressHexString.contains("x") &&
- addressHexString.length() == 32) {
- relayIP = ipParameter.toLowerCase();
- }
- }
- }
- } else {
- relayIP = "";
- }
- return relayIP;
- }
-
- private String convertIPv4ToHex(String relayIP) {
- String[] relayIPParts = relayIP.split("\\.");
- byte[] address24Bytes = new byte[4];
- for (int i = 0; i < address24Bytes.length; i++) {
- address24Bytes[i] = (byte) Integer.parseInt(relayIPParts[i]);
- }
- String address24 = Hex.encodeHexString(address24Bytes);
- return address24;
- }
-
- private String convertIPv6ToHex(String relayIP) {
- if (relayIP.startsWith("[") && relayIP.endsWith("]")) {
- relayIP = relayIP.substring(1, relayIP.length() - 1);
- }
- StringBuilder addressHex = new StringBuilder();
- int start = relayIP.startsWith("::") ? 1 : 0;
- int end = relayIP.length() - (relayIP.endsWith("::") ? 1 : 0);
- String[] parts = relayIP.substring(start, end).split(":", -1);
- for (int i = 0; i < parts.length; i++) {
- String part = parts[i];
- if (part.length() == 0) {
- addressHex.append("x");
- } else if (part.length() <= 4) {
- addressHex.append(String.format("%4s", part));
- } else {
- addressHex = null;
- break;
- }
- }
- String address48 = null;
- if (addressHex != null) {
- String addressHexString = addressHex.toString();
- addressHexString = addressHexString.replaceFirst("x",
- String.format("%" + (33 - addressHexString.length())
- + "s", "0"));
- if (!addressHexString.contains("x") &&
- addressHexString.length() == 32) {
- address48 = addressHexString.replaceAll(" ", "0").
- toLowerCase();
- }
- }
- return address48;
- }
-
- private String parseTimestampParameter(
- String passedTimestampParameter) {
- String timestampStr = "";
- SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
- dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
- dateFormat.setLenient(false);
- if (passedTimestampParameter != null &&
- passedTimestampParameter.length() > 0) {
- String timestampParameter = passedTimestampParameter.trim();
- try {
- long timestamp = dateFormat.parse(timestampParameter).getTime();
- timestampStr = dateFormat.format(timestamp);
- } catch (ParseException e) {
- timestampStr = null;
- }
- }
- return timestampStr;
- }
-
- /* Helper methods for querying the database. */
-
- private Connection connectToDatabase() {
- Connection conn = null;
- try {
- conn = this.ds.getConnection();
- } catch (SQLException e) {
- }
- return conn;
- }
-
- private long[] queryFirstAndLastDatesFromDatabase(Connection conn) {
- long[] firstAndLastDates = null;
- try {
- Statement statement = conn.createStatement();
- String query = "SELECT DATE(MIN(validafter)) AS first, "
- + "DATE(MAX(validafter)) AS last FROM statusentry";
- ResultSet rs = statement.executeQuery(query);
- if (rs.next()) {
- Calendar utcCalendar = Calendar.getInstance(
- TimeZone.getTimeZone("UTC"));
- firstAndLastDates = new long[] {
- rs.getTimestamp(1, utcCalendar).getTime(),
- rs.getTimestamp(2, utcCalendar).getTime()
- };
- }
- rs.close();
- statement.close();
- } catch (SQLException e) {
- /* Looks like we don't have any consensuses. */
- firstAndLastDates = null;
- }
- return firstAndLastDates;
- }
-
- private SortedSet<Long> queryKnownConsensusValidAfterTimes(
- Connection conn, String fromValidAfter, String toValidAfter) {
- SortedSet<Long> relevantConsensuses = new TreeSet<Long>();
- try {
- Statement statement = conn.createStatement();
- String query = "SELECT DISTINCT validafter FROM statusentry "
- + "WHERE validafter >= '" + fromValidAfter
- + "' AND validafter <= '" + toValidAfter + "'";
- ResultSet rs = statement.executeQuery(query);
- while (rs.next()) {
- long consensusTime = rs.getTimestamp(1).getTime();
- relevantConsensuses.add(consensusTime);
- }
- rs.close();
- statement.close();
- } catch (SQLException e) {
- /* Looks like we don't have any consensuses in the requested
- * interval. */
- relevantConsensuses = null;
- }
- return relevantConsensuses;
- }
-
- private List<String[]> queryStatusEntries(Connection conn,
- String relayIP, long timestamp,
- SimpleDateFormat validAfterTimeFormat) {
- List<String[]> statusEntries = new ArrayList<String[]>();
- String addressHex = !relayIP.contains(":")
- ? this.convertIPv4ToHex(relayIP) : this.convertIPv6ToHex(relayIP);
- if (addressHex == null) {
- return null;
- }
- String address24Or48Hex = !relayIP.contains(":")
- ? addressHex.substring(0, 6) : addressHex.substring(0, 12);
- try {
- CallableStatement cs;
- if (!relayIP.contains(":")) {
- cs = conn.prepareCall("{call search_by_address24_date(?, ?)}");
- } else {
- cs = conn.prepareCall("{call search_by_address48_date(?, ?)}");
- }
- cs.setString(1, address24Or48Hex);
- Calendar utcCalendar = Calendar.getInstance(
- TimeZone.getTimeZone("UTC"));
- cs.setDate(2, new java.sql.Date(timestamp), utcCalendar);
- ResultSet rs = cs.executeQuery();
- while (rs.next()) {
- byte[] rawstatusentry = rs.getBytes(1);
- SortedSet<String> addresses = new TreeSet<String>(),
- addressesHex = new TreeSet<String>();
- long validafter = rs.getTimestamp(2, utcCalendar).getTime();
- String validAfterString = validAfterTimeFormat.format(validafter);
- String fingerprint = rs.getString(3).toUpperCase();
- String nickname = null;
- String exit = "U";
- for (String line : new String(rawstatusentry).split("\n")) {
- if (line.startsWith("r ")) {
- String[] parts = line.split(" ");
- nickname = parts[1];
- addresses.add(parts[6]);
- addressesHex.add(this.convertIPv4ToHex(parts[6]));
- } else if (line.startsWith("a ")) {
- String address = line.substring("a ".length(),
- line.lastIndexOf(":"));
- addresses.add(address);
- String orAddressHex = !address.contains(":")
- ? this.convertIPv4ToHex(address)
- : this.convertIPv6ToHex(address);
- addressesHex.add(orAddressHex);
- } else if (line.startsWith("p ")) {
- exit = line.equals("p reject 1-65535") ? "N" : "Y";
- }
- }
- String exitaddress = rs.getString(4);
- if (exitaddress != null && exitaddress.length() > 0) {
- addresses.add(exitaddress);
- addressesHex.add(this.convertIPv4ToHex(exitaddress));
- }
- if (!addressesHex.contains(addressHex)) {
- continue;
- }
- StringBuilder sb = new StringBuilder();
- int writtenAddresses = 0;
- for (String address : addresses) {
- sb.append((writtenAddresses++ > 0 ? ", " : "") + address);
- }
- String[] statusEntry = new String[] { validAfterString,
- sb.toString(), fingerprint, nickname, exit };
- statusEntries.add(statusEntry);
- }
- rs.close();
- cs.close();
- } catch (SQLException e) {
- /* Nothing found. */
- statusEntries = null;
- }
- return statusEntries;
- }
-
- private List<String> queryAddressesInSame24(Connection conn,
- String address24, long timestamp) {
- List<String> addressesInSameNetwork = new ArrayList<String>();
- try {
- CallableStatement cs = conn.prepareCall(
- "{call search_addresses_in_same_24 (?, ?)}");
- cs.setString(1, address24);
- cs.setDate(2, new java.sql.Date(timestamp));
- ResultSet rs = cs.executeQuery();
- while (rs.next()) {
- String address = rs.getString(1);
- if (!addressesInSameNetwork.contains(address)) {
- addressesInSameNetwork.add(address);
- }
- }
- rs.close();
- cs.close();
- } catch (SQLException e) {
- /* No other addresses in the same /24 found. */
- addressesInSameNetwork = null;
- }
- return addressesInSameNetwork;
- }
-
- private List<String> queryAddressesInSame48(Connection conn,
- String address48, long timestamp) {
- List<String> addressesInSameNetwork = new ArrayList<String>();
- try {
- CallableStatement cs = conn.prepareCall(
- "{call search_addresses_in_same_48 (?, ?)}");
- cs.setString(1, address48);
- cs.setDate(2, new java.sql.Date(timestamp));
- ResultSet rs = cs.executeQuery();
- while (rs.next()) {
- String address = rs.getString(1);
- if (!addressesInSameNetwork.contains(address)) {
- addressesInSameNetwork.add(address);
- }
- }
- rs.close();
- cs.close();
- } catch (SQLException e) {
- /* No other addresses in the same /48 found. */
- addressesInSameNetwork = null;
- }
- return addressesInSameNetwork;
- }
-
- private void closeDatabaseConnection(Connection conn,
- long requestedConnection) {
- try {
- conn.close();
- this.logger.info("Returned a database connection to the pool "
- + "after " + (System.currentTimeMillis()
- - requestedConnection) + " millis.");
- } catch (SQLException e) {
- }
- return;
- }
-
- /* Helper methods for writing the response. */
-
- private void writeHeader(PrintWriter out) throws IOException {
- out.println("<!DOCTYPE html>\n"
- + "<html lang=\"en\">\n"
- + " <head>\n"
- + " <meta charset=\"utf-8\">\n"
- + " <meta http-equiv=\"X-UA-Compatible\" "
- + "content=\"IE=edge\">\n"
- + " <meta name=\"viewport\" content=\"width=device-width, "
- + "initial-scale=1\">\n"
- + " <title>ExoneraTor</title>\n"
- + " <link rel=\"stylesheet\" href=\"css/bootstrap.min.css\">\n"
- + " <link rel=\"stylesheet\" href=\"css/exonerator.css\">\n"
- + " <link href=\"images/favicon.ico\" type=\"image/x-icon\" "
- + "rel=\"icon\">\n"
- + " </head>\n"
- + " <body>\n"
- + " <div class=\"container\">\n"
- + " <div class=\"row\">\n"
- + " <div class=\"col-xs-12\">\n"
- + " <div class=\"page-header\">\n"
- + " <h1>\n"
- + " <div class=\"text-center\">\n"
- + " <a href=\"/\">"
- + "<img src=\"images/exonerator-logo.png\" "
- + "width=\"334\" height=\"252\" alt=\"ExoneraTor logo\">"
- + "<img src=\"images/exonerator-wordmark.png\" width=\"428\" "
- + "height=\"63\" alt=\"ExoneraTor wordmark\"></a>\n"
- + " </div><!-- text-center -->\n"
- + " </h1>\n"
- + " </div><!-- page-header -->\n"
- + " </div><!-- col -->\n"
- + " </div><!-- row -->\n");
- }
-
- private void writeForm(PrintWriter out, ResourceBundle rb,
- String relayIP, boolean relayIPHasError, String timestampStr,
- boolean timestampHasError) throws IOException {
- String ipValue = "";
- if (relayIP != null && relayIP.length() > 0) {
- if (relayIP.contains(":")) {
- ipValue = String.format(" value=\"[%s]\"", relayIP);
- } else {
- ipValue = String.format(" value=\"%s\"", relayIP);
- }
- }
- out.printf(" <div class=\"row\">\n"
- + " <div class=\"col-xs-12\">\n"
- + " <div class=\"text-center\">\n"
- + " <div class=\"row vbottom15\">\n"
- + " <h4>%s</h4>\n"
- + " </div> <!-- row -->\n"
- + " <form class=\"form-inline\">\n"
- + " <div class=\"form-group%s\">\n"
- + " <label for=\"inputIp\" "
- + "class=\"control-label\">%s</label>\n"
- + " <input type=\"text\" class=\"form-control\" "
- + "name=\"ip\" id=\"inputIp\" placeholder=\"86.59.21.38\"%s "
- + "required>\n"
- + " </div><!-- form-group -->\n"
- + " <div class=\"form-group%s\">\n"
- + " <label for=\"inputTimestamp\" "
- + "class=\"control-label\">%s</label>\n"
- + " <input type=\"date\" class=\"form-control\" "
- + "name=\"timestamp\" id=\"inputTimestamp\" "
- + "placeholder=\"2010-01-01\"%s required>\n"
- + " </div><!-- form-group -->\n"
- + " <button type=\"submit\" "
- + "class=\"btn btn-primary\">%s</button>\n"
- + " </form>\n"
- + " </div><!-- text-center -->\n"
- + " </div><!-- col -->\n"
- + " </div><!-- row -->\n",
- rb.getString("form.explanation"),
- relayIPHasError ? " has-error" : "",
- rb.getString("form.ip.label"),
- ipValue,
- timestampHasError ? " has-error" : "",
- rb.getString("form.timestamp.label"),
- timestampStr != null && timestampStr.length() > 0 ?
- " value=\"" + timestampStr + "\"" : "",
- rb.getString("form.search.label"));
- }
-
- private void writeSummaryUnableToConnectToDatabase(PrintWriter out,
- ResourceBundle rb) throws IOException {
- String contactLink =
- "<a href=\"https://www.torproject.org/about/contact\">"
- + rb.getString("summary.serverproblem.dbempty.body.link")
- + "</a>";
- this.writeSummary(out, rb.getString("summary.heading"),
- "panel-danger",
- rb.getString("summary.serverproblem.dbnoconnect.title"), null,
- rb.getString("summary.serverproblem.dbnoconnect.body.text"),
- contactLink);
- }
-
- private void writeSummaryNoData(PrintWriter out, ResourceBundle rb)
- throws IOException {
- String contactLink =
- "<a href=\"https://www.torproject.org/about/contact\">"
- + rb.getString("summary.serverproblem.dbempty.body.link")
- + "</a>";
- this.writeSummary(out, rb.getString("summary.heading"),
- "panel-danger",
- rb.getString("summary.serverproblem.dbempty.title"), null,
- rb.getString("summary.serverproblem.dbempty.body.text"),
- contactLink);
- }
-
- private void writeSummaryNoTimestamp(PrintWriter out, ResourceBundle rb)
- throws IOException {
- this.writeSummary(out, rb.getString("summary.heading"),
- "panel-danger",
- rb.getString("summary.invalidparams.notimestamp.title"), null,
- rb.getString("summary.invalidparams.notimestamp.body"));
- }
-
- private void writeSummaryNoIp(PrintWriter out, ResourceBundle rb)
- throws IOException {
- this.writeSummary(out, rb.getString("summary.heading"),
- "panel-danger", rb.getString("summary.invalidparams.noip.title"),
- null, rb.getString("summary.invalidparams.noip.body"));
- }
-
- private void writeSummaryTimestampOutsideRange(PrintWriter out,
- ResourceBundle rb, String timestampStr, String firstDate,
- String lastDate) throws IOException {
- this.writeSummary(out, rb.getString("summary.heading"),
- "panel-danger",
- rb.getString("summary.invalidparams.timestamprange.title"), null,
- rb.getString("summary.invalidparams.timestamprange.body"),
- timestampStr, firstDate, lastDate);
- }
-
- private void writeSummaryInvalidIp(PrintWriter out, ResourceBundle rb,
- String ipParameter) throws IOException {
- String escapedIpParameter = ipParameter.length() > 40 ?
- StringEscapeUtils.escapeHtml(ipParameter.substring(0, 40))
- + "[...]" : StringEscapeUtils.escapeHtml(ipParameter);
- this.writeSummary(out, rb.getString("summary.heading"),
- "panel-danger",
- rb.getString("summary.invalidparams.invalidip.title"), null,
- rb.getString("summary.invalidparams.invalidip.body"),
- escapedIpParameter, "\"a.b.c.d\"", "\"[a:b:c:d:e:f:g:h]\"");
- }
-
- private void writeSummaryInvalidTimestamp(PrintWriter out,
- ResourceBundle rb, String timestampParameter) throws IOException {
- String escapedTimestampParameter = timestampParameter.length() > 20 ?
- StringEscapeUtils.escapeHtml(timestampParameter.
- substring(0, 20)) + "[...]" :
- StringEscapeUtils.escapeHtml(timestampParameter);
- this.writeSummary(out, rb.getString("summary.heading"),
- "panel-danger",
- rb.getString("summary.invalidparams.invalidtimestamp.title"),
- null, rb.getString("summary.invalidparams.invalidtimestamp.body"),
- escapedTimestampParameter, "\"YYYY-MM-DD\"");
- }
-
- private void writeSummaryNoDataForThisInterval(PrintWriter out,
- ResourceBundle rb) throws IOException {
- String contactLink =
- "<a href=\"https://www.torproject.org/about/contact\">"
- + rb.getString("summary.serverproblem.dbempty.body.link")
- + "</a>";
- this.writeSummary(out, rb.getString("summary.heading"),
- "panel-danger",
- rb.getString("summary.serverproblem.nodata.title"), null,
- rb.getString("summary.serverproblem.nodata.body.text"),
- contactLink);
- }
-
- private void writeSummaryAddressesInSameNetwork(PrintWriter out,
- ResourceBundle rb, String relayIP, String timestampStr,
- List<String> addressesInSameNetwork) throws IOException {
- Object[][] panelItems = new Object[addressesInSameNetwork.size()][];
- for (int i = 0; i < addressesInSameNetwork.size(); i++) {
- String addressInSameNetwork = addressesInSameNetwork.get(i);
- String link, address;
- if (addressInSameNetwork.contains(":")) {
- link = String.format("/?ip=[%s]×tamp=%s",
- addressInSameNetwork.replaceAll(":", "%3A"), timestampStr);
- address = "[" + addressInSameNetwork + "]";
- } else {
- link = String.format("/?ip=%s×tamp=%s",
- addressInSameNetwork, timestampStr);
- address = addressInSameNetwork;
- }
- panelItems[i] = new Object[] { link, address };
- }
- this.writeSummary(out, rb.getString("summary.heading"),
- "panel-warning",
- rb.getString("summary.negativesamenetwork.title"), panelItems,
- rb.getString("summary.negativesamenetwork.body"),
- relayIP, timestampStr, relayIP.contains(":") ? 48 : 24);
- }
-
- private void writeSummaryPositive(PrintWriter out, ResourceBundle rb,
- String relayIP, String timestampStr) throws IOException {
- String formattedRelayIP = relayIP.contains(":") ?
- "[" + relayIP + "]" : relayIP;
- this.writeSummary(out, rb.getString("summary.heading"),
- "panel-success", rb.getString("summary.positive.title"), null,
- rb.getString("summary.positive.body"), formattedRelayIP,
- timestampStr);
- }
-
- private void writeSummaryNegative(PrintWriter out, ResourceBundle rb,
- String relayIP, String timestampStr) throws IOException {
- String formattedRelayIP = relayIP.contains(":") ?
- "[" + relayIP + "]" : relayIP;
- this.writeSummary(out, rb.getString("summary.heading"),
- "panel-warning", rb.getString("summary.negative.title"), null,
- rb.getString("summary.negative.body"), formattedRelayIP,
- timestampStr);
- }
-
- private void writeSummary(PrintWriter out, String heading,
- String panelContext, String panelTitle, Object[][] panelItems,
- String panelBodyTemplate, Object... panelBodyArgs)
- throws IOException {
- out.printf(" <div class=\"row\">\n"
- + " <div class=\"col-xs-12\">\n"
- + " <h2>%s</h2>\n"
- + " <div class=\"panel %s\">\n"
- + " <div class=\"panel-heading\">\n"
- + " <h3 class=\"panel-title\">%s</h3>\n"
- + " </div><!-- panel-heading -->\n"
- + " <div class=\"panel-body\">\n"
- + " <p>%s</p>\n", heading, panelContext, panelTitle,
- String.format(panelBodyTemplate, panelBodyArgs));
- if (panelItems != null) {
- out.print(" <ul>\n");
- for (Object[] panelItem : panelItems) {
- out.printf(" <li><a href=\"%s\">%s</a></li>\n",
- panelItem);
- }
- out.print(" </ul>\n");
- }
- out.print(" </div><!-- panel-body -->\n"
- + " </div><!-- panel -->\n"
- + " </div><!-- col -->\n"
- + " </div><!-- row -->\n");
- }
-
- private void writeTechnicalDetails(PrintWriter out, ResourceBundle rb,
- String relayIP, String timestampStr, List<String[]> tableRows)
- throws IOException {
- String formattedRelayIP = relayIP.contains(":") ?
- "[" + relayIP + "]" : relayIP;
- out.printf(" <div class=\"row\">\n"
- + " <div class=\"col-xs-12\">\n"
- + " <h2>%s</h2>\n"
- + " <p>%s</p>\n"
- + " <table class=\"table\">\n"
- + " <thead>\n"
- + " <tr>\n"
- + " <th>%s</th>\n"
- + " <th>%s</th>\n"
- + " <th>%s</th>\n"
- + " <th>%s</th>\n"
- + " <th>%s</th>\n"
- + " </tr>\n"
- + " </thead>\n"
- + " <tbody>\n",
- rb.getString("technicaldetails.heading"),
- String.format(rb.getString("technicaldetails.pre"),
- formattedRelayIP, timestampStr),
- rb.getString("technicaldetails.colheader.timestamp"),
- rb.getString("technicaldetails.colheader.ip"),
- rb.getString("technicaldetails.colheader.fingerprint"),
- rb.getString("technicaldetails.colheader.nickname"),
- rb.getString("technicaldetails.colheader.exit"));
- for (String[] tableRow : tableRows) {
- out.print(" <tr>");
- for (int i = 0; i < tableRow.length; i++) {
- String content = tableRow[i];
- if (i == 2) {
- content = content.substring(0, 20) + "​"
- + content.substring(20, 40);
- } else if (i == 3 && content == null) {
- content = "("
- + rb.getString("technicaldetails.nickname.unknown") + ")";
- } else if (i == 4) {
- if (content.equals("U")) {
- content = rb.getString("technicaldetails.exit.unknown");
- } else if (content.equals("Y")) {
- content = rb.getString("technicaldetails.exit.yes");
- } else {
- content = rb.getString("technicaldetails.exit.no");
- }
- }
- out.print(" <td>" + content + "</td>");
- }
- out.print(" </tr>\n");
- }
- out.print(" </tbody>\n"
- + " </table>\n"
- + " </div><!-- col -->\n"
- + " </div><!-- row -->\n");
- }
-
- private void writePermanentLink(PrintWriter out, ResourceBundle rb,
- String relayIP, String timestampStr) throws IOException {
- String encodedAddress = relayIP.contains(":") ?
- "[" + relayIP.replaceAll(":", "%3A") + "]" : relayIP;
- out.printf(" <div class=\"row\">\n"
- + " <div class=\"col-xs-12\">\n"
- + " <h2>%s</h2>\n"
- + " <pre>https://exonerator.torproject.org/?ip=%s&"
- + "timestamp=%s</pre>\n"
- + " </div><!-- col -->\n"
- + " </div><!-- row -->\n",
- rb.getString("permanentlink.heading"),
- encodedAddress, timestampStr);
- }
-
- private void writeFooter(PrintWriter out, ResourceBundle rb)
- throws IOException {
- out.printf(" </div><!-- container -->\n"
- + " <div class=\"footer\">\n"
- + " <div class=\"container\">\n"
- + " <div class=\"row\">\n"
- + " <div class=\"col-xs-6\">\n"
- + " <h3>%s</h3>\n"
- + " <p class=\"small\">%s</p>\n"
- + " </div><!-- col -->\n",
- rb.getString("footer.abouttor.heading"),
- String.format(rb.getString("footer.abouttor.body.text"),
- "<a href=\"https://www.torproject.org/about/"
- + "overview#thesolution\">"
- + rb.getString("footer.abouttor.body.link1") + "</a>",
- "<a href=\"https://www.torproject.org/about/overview\">"
- + rb.getString("footer.abouttor.body.link2") + "</a>",
- "<a href=\"https://www.torproject.org/about/contact\">"
- + rb.getString("footer.abouttor.body.link3") + "</a>"));
- out.printf(" <div class=\"col-xs-6\">\n"
- + " <h3>%s</h3>\n"
- + " <p class=\"small\">%s</p>\n"
- + " </div><!-- col -->\n"
- + " </div><!-- row -->\n"
- + " <div class=\"row\">\n",
- rb.getString("footer.aboutexonerator.heading"),
- rb.getString("footer.aboutexonerator.body"));
- out.printf(" <div class=\"col-xs-12\">\n"
- + " <p class=\"text-center small\">%s</p>\n"
- + " </div><!-- col -->\n"
- + " </div><!-- row -->\n"
- + " </div><!-- container -->\n"
- + " </div><!-- footer -->\n"
- + " </body>\n"
- + "</html>\n",
- String.format(rb.getString("footer.trademark.text"),
- "<a href=\"https://www.torproject.org/docs/"
- + "trademark-faq.html.en\">"
- + rb.getString("footer.trademark.link") + "</a>"));
- out.close();
- }
-}
-
_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits