commit 172e1498d328a62d0915ab1796ec63202bcdd12e Author: Karsten Loesing karsten.loesing@gmx.net 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%5C%22%3E" + + 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%5C%22%3E" + + 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%5C%22%3E" + + 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%5C%22%3E" + + rb.getString("footer.abouttor.body.link2") + "</a>", + "<a href="https://www.torproject.org/about/contact%5C%22%3E" + + 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%5C%22%3E" - + 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%5C%22%3E" - + 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%5C%22%3E" - + 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%5C%22%3E" - + rb.getString("footer.abouttor.body.link2") + "</a>", - "<a href="https://www.torproject.org/about/contact%5C%22%3E" - + 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@lists.torproject.org