[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[or-cvs] [ernie/master 3/5] Add ExoneraTor servlet.
Author: Karsten Loesing <karsten.loesing@xxxxxxx>
Date: Wed, 19 May 2010 14:57:27 +0200
Subject: Add ExoneraTor servlet.
Commit: 580aa2f5e26054017ae97eb7df46ae84bb11792d
---
etc/web.xml | 24 +
src/org/torproject/ernie/web/ConsensusServlet.java | 86 +++
.../torproject/ernie/web/ExoneraTorServlet.java | 789 ++++++++++++++++++++
.../ernie/web/ServerDescriptorServlet.java | 86 +++
4 files changed, 985 insertions(+), 0 deletions(-)
create mode 100644 src/org/torproject/ernie/web/ConsensusServlet.java
create mode 100644 src/org/torproject/ernie/web/ExoneraTorServlet.java
create mode 100644 src/org/torproject/ernie/web/ServerDescriptorServlet.java
diff --git a/etc/web.xml b/etc/web.xml
index 67e0822..885a632 100644
--- a/etc/web.xml
+++ b/etc/web.xml
@@ -18,5 +18,29 @@
<servlet-name>Image</servlet-name>
<url-pattern>/graphs/*</url-pattern>
</servlet-mapping>-->
+ <servlet>
+ <servlet-name>ExoneraTor</servlet-name>
+ <servlet-class>org.torproject.ernie.web.ExoneraTorServlet</servlet-class>
+ </servlet>
+ <servlet-mapping>
+ <servlet-name>ExoneraTor</servlet-name>
+ <url-pattern>/exonerator.html</url-pattern>
+ </servlet-mapping>
+ <servlet>
+ <servlet-name>ServerDescriptor</servlet-name>
+ <servlet-class>org.torproject.ernie.web.ServerDescriptorServlet</servlet-class>
+ </servlet>
+ <servlet-mapping>
+ <servlet-name>ServerDescriptor</servlet-name>
+ <url-pattern>/serverdesc</url-pattern>
+ </servlet-mapping>
+ <servlet>
+ <servlet-name>Consensus</servlet-name>
+ <servlet-class>org.torproject.ernie.web.ConsensusServlet</servlet-class>
+ </servlet>
+ <servlet-mapping>
+ <servlet-name>Consensus</servlet-name>
+ <url-pattern>/consensus</url-pattern>
+ </servlet-mapping>
</web-app>
diff --git a/src/org/torproject/ernie/web/ConsensusServlet.java b/src/org/torproject/ernie/web/ConsensusServlet.java
new file mode 100644
index 0000000..a95df12
--- /dev/null
+++ b/src/org/torproject/ernie/web/ConsensusServlet.java
@@ -0,0 +1,86 @@
+package org.torproject.ernie.web;
+
+import javax.servlet.*;
+import javax.servlet.http.*;
+import java.io.*;
+import java.math.*;
+import java.text.*;
+import java.util.*;
+
+public class ConsensusServlet extends HttpServlet {
+
+ public void doGet(HttpServletRequest request,
+ HttpServletResponse response) throws IOException,
+ ServletException {
+
+ String validAfterParameter = request.getParameter("valid-after");
+
+ /* Check if we have a descriptors directory. */
+ // TODO make this configurable!
+ File archiveDirectory = new File("/srv/metrics.torproject.org/archives");
+ if (!archiveDirectory.exists() || !archiveDirectory.isDirectory()) {
+ /* Oops, we don't have any descriptors to serve. */
+// TODO change to internal server error
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+
+ /* Check valid-after parameter. */
+ if (validAfterParameter == null ||
+ validAfterParameter.length() < "yyyy-MM-dd-HH-mm-ss".length()) {
+// TODO is there something like "wrong parameter"?
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+ SimpleDateFormat timeFormat = new SimpleDateFormat(
+ "yyyy-MM-dd-HH-mm-ss");
+ timeFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ Date parsedTimestamp = null;
+ try {
+ parsedTimestamp = timeFormat.parse(validAfterParameter);
+ } catch (ParseException e) {
+// TODO is there something like "wrong parameter"?
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+ if (parsedTimestamp == null) {
+// TODO is there something like "wrong parameter"?
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+ String consensusFilename = archiveDirectory.getAbsolutePath()
+ + "/consensuses-" + validAfterParameter.substring(0, 4) + "-"
+ + validAfterParameter.substring(5, 7) + "/"
+ + validAfterParameter.substring(8, 10) + "/"
+ + validAfterParameter + "-consensus";
+ File consensusFile = new File(consensusFilename);
+
+ if (!consensusFile.exists()) {
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+
+ /* Read file from disk and write it to response. */
+ BufferedInputStream input = null;
+ BufferedOutputStream output = null;
+ try {
+ response.setContentType("text/plain");
+ response.setHeader("Content-Length", String.valueOf(
+ consensusFile.length()));
+ response.setHeader("Content-Disposition",
+ "inline; filename=\"" + consensusFile.getName() + "\"");
+ input = new BufferedInputStream(new FileInputStream(consensusFile),
+ 1024);
+ output = new BufferedOutputStream(response.getOutputStream(), 1024);
+ byte[] buffer = new byte[1024];
+ int length;
+ while ((length = input.read(buffer)) > 0) {
+ output.write(buffer, 0, length);
+ }
+ } finally {
+ output.close();
+ input.close();
+ }
+ }
+}
+
diff --git a/src/org/torproject/ernie/web/ExoneraTorServlet.java b/src/org/torproject/ernie/web/ExoneraTorServlet.java
new file mode 100644
index 0000000..c1bfea1
--- /dev/null
+++ b/src/org/torproject/ernie/web/ExoneraTorServlet.java
@@ -0,0 +1,789 @@
+package org.torproject.ernie.web;
+
+import javax.servlet.*;
+import javax.servlet.http.*;
+import java.io.*;
+import java.math.*;
+import java.text.*;
+import java.util.*;
+import java.util.regex.*;
+
+import org.apache.commons.codec.binary.*;
+
+public class ExoneraTorServlet extends HttpServlet {
+
+ private void writeHeader(PrintWriter out) throws IOException {
+ out.println("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 "
+ + "Transitional//EN\"\n"
+ + "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n"
+ + "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"
+ + " <head>\n"
+ + " <meta content=\"text/html; charset=ISO-8859-1\"\n"
+ + " http-equiv=\"content-type\" />\n"
+ + " <title>ExoneraTor</title>\n"
+ + " <meta http-equiv=Content-Type content=\"text/html; "
+ + "charset=iso-8859-1\">\n"
+ + " <link href=\"http://www.torproject.org/"
+ + "stylesheet-ltr.css\" type=text/css rel=stylesheet>\n"
+ + " <link href=\"http://www.torproject.org/favicon.ico\" "
+ + "type=image/x-icon rel=\"shortcut icon\">\n"
+ + " </head>\n"
+ + " <body>\n"
+ + " <div class=\"center\">\n"
+ + " <table class=\"banner\" border=\"0\" cellpadding=\"0\" "
+ + "cellspacing=\"0\" summary=\"\">\n"
+ + " <tr>\n"
+ + " <td class=\"banner-left\"><a "
+ + "href=\"https://www.torproject.org/\"><img "
+ + "src=\"http://www.torproject.org/images/top-left.png\" "
+ + "alt=\"Click to go to home page\" width=\"193\" "
+ + "height=\"79\"></a></td>\n"
+ + " <td class=\"banner-middle\"></td>\n"
+ + " <td class=\"banner-right\"></td>\n"
+ + " </tr>\n"
+ + " </table>\n"
+ + " <div class=\"main-column\" style=\"margin:5; "
+ + "Padding:0;\">\n"
+ + " <h2>ExoneraTor</h2>\n"
+ + " <h3>or: a website that tells you whether some IP "
+ + "address was a Tor relay</h3>\n"
+ + " <p>ExoneraTor tells you whether there was a Tor relay "
+ + "running on a given IP address at a given time. ExoneraTor "
+ + "can further find out whether this relay permitted exiting "
+ + "to a given server and/or TCP port. ExoneraTor learns about "
+ + "these facts from parsing the public relay lists and relay "
+ + "descriptors that are collected from the Tor directory "
+ + "authorities.\n"
+ + " <br/>\n"
+ + " <p><font color=\"red\"><b>Notice:</b> Note that the "
+ + "information you are providing below may be leaked to anyone "
+ + "who can read the network traffic between you and this web "
+ + "server or who has access to this web server. If you need to "
+ + "keep the IP addresses and incident times confidential, you "
+ + "should download the <a href=\"https://svn.torproject.org/"
+ + "svn/projects/archives/trunk/exonerator/\">Java or Python "
+ + "version of ExoneraTor</a> and run it on your local "
+ + "machine.</font></p>\n"
+ + " <br/>\n");
+ }
+
+ private void writeFooter(PrintWriter out) throws IOException {
+ out.println(" <br/>\n"
+ + " </div>\n"
+ + " </div>\n"
+ + " <div class=\"bottom\" id=\"bottom\">\n"
+ + " <p>\"Tor\" and the \"Onion Logo\" are <a "
+ + "href=\"https://www.torproject.org/trademark-faq.html.en\">"
+ + "registered trademarks</a> of The Tor Project, Inc.</p>\n"
+ + " </div>\n"
+ + " </body>\n"
+ + "</html>");
+ out.close();
+ }
+
+ // TODO make this configurable!
+ public final String ARCHIVES_DIRECTORY = "/home/karsten/archives";
+
+ private static final boolean TEST_MODE = false;
+
+ public void doGet(HttpServletRequest request,
+ HttpServletResponse response) throws IOException,
+ ServletException {
+
+ /* Get print writer and start writing response. We're wrapping the
+ * PrintWriter, because we want it to auto-flush as soon as we have
+ * written a line. */
+ //PrintWriter out = new PrintWriter(response.getWriter(), true);
+ PrintWriter out = response.getWriter();
+ writeHeader(out);
+
+ SortedSet<File> consensusDirectories = new TreeSet<File>();
+ SortedSet<File> serverDescriptorDirectories = new TreeSet<File>();
+
+ /* Check if we have a descriptors directory. */
+ File archiveDirectory = new File("/srv/metrics.torproject.org/archives");
+ if (!archiveDirectory.exists() || !archiveDirectory.isDirectory()) {
+ /* Leave sets with consensus and server descriptor directories
+ empty. */
+ return;
+ }
+ for (File dir : archiveDirectory.listFiles()) {
+ if (dir.getName().startsWith("consensuses-")) {
+ consensusDirectories.add(dir);
+ } else if (dir.getName().startsWith("server-descriptors-")) {
+ serverDescriptorDirectories.add(dir);
+ }
+ }
+
+ if (consensusDirectories.isEmpty()) {
+ out.println("<p><font color=\"red\"><b>Warning: </b></font>This "
+ + "server doesn't have any relay lists available. If this "
+ + "problem persists, please "
+ + "<a href=\"mailto:tor-assistants@xxxxxxxxxxxxx\">let us "
+ + "know</a>!</p>\n");
+ writeFooter(out);
+ return;
+ }
+ String firstDay = consensusDirectories.first().getName().
+ substring("consensuses-".length()) + "-" + new TreeSet<File>(
+ Arrays.asList(consensusDirectories.first().listFiles())).
+ first().getName();
+ String lastDay = consensusDirectories.last().getName().
+ substring("consensuses-".length()) + "-" + new TreeSet<File>(
+ Arrays.asList(consensusDirectories.last().listFiles())).
+ last().getName();
+
+ out.println("<a id=\"relay\"/><h3>Was there a Tor relay running on "
+ + "this IP address?</h3>");
+
+ /* Parse IP parameter. */
+ Pattern ipAddressPattern = Pattern.compile(
+ "^([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." +
+ "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." +
+ "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." +
+ "([01]?\\d\\d?|2[0-4]\\d|25[0-5])$");
+ String ipParameter = request.getParameter("ip");
+ String relayIP = "", ipWarning = "";
+ if (ipParameter != null && ipParameter.length() > 0) {
+ Matcher ipParameterMatcher = ipAddressPattern.matcher(ipParameter);
+ if (ipParameterMatcher.matches()) {
+ String[] ipParts = ipParameter.split("\\.");
+ relayIP = Integer.parseInt(ipParts[0]) + "."
+ + Integer.parseInt(ipParts[1]) + "."
+ + Integer.parseInt(ipParts[2]) + "."
+ + Integer.parseInt(ipParts[3]);
+ } else {
+ ipWarning = "\"" + (ipParameter.length() > 20 ?
+ ipParameter.substring(0, 20) + "[...]" :
+ ipParameter) + "\" is not a valid IP address.";
+ }
+ }
+
+ /* Parse timestamp parameter. */
+ String timestampParameter = request.getParameter("timestamp");
+ long timestamp = 0L;
+ String timestampStr = "", timestampWarning = "";
+ SimpleDateFormat parseTimeFormat = new SimpleDateFormat(
+ "yyyy-MM-dd HH:mm");
+ parseTimeFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ if (timestampParameter != null && timestampParameter.length() > 0) {
+ try {
+ Date parsedTimestamp = parseTimeFormat.parse(timestampParameter);
+ if (timestampParameter.compareTo(firstDay) >= 0 &&
+ timestampParameter.compareTo(lastDay) <= 0) {
+ timestamp = parsedTimestamp.getTime();
+ timestampStr = parseTimeFormat.format(timestamp);
+ } else {
+ timestampWarning = "Please pick a value between \"" + firstDay
+ + " 03:00\" and \"" + lastDay + " 21:00\".";
+ }
+ } catch (ParseException e) {
+ /* We have no way to handle this exception, other than leaving
+ timestampStr at "". */
+ timestampWarning = "\"" + (timestampParameter.length() > 20 ?
+ timestampParameter.substring(0, 20) + "[...]" :
+ timestampParameter) + "\" is not a valid timestamp.";
+ }
+ }
+
+ /* If either IP address or timestamp is provided, the other one must
+ * be provided, too. */
+ if (relayIP.length() < 1 && timestampStr.length() > 0 &&
+ ipWarning.length() < 1) {
+ ipWarning = "Please provide an IP address.";
+ }
+ if (relayIP.length() > 0 && timestampStr.length() < 1 &&
+ timestampWarning.length() < 1) {
+ timestampWarning = "Please provide a timestamp.";
+ }
+
+ /* Parse target IP parameter. */
+ String targetIP = "", targetPort = "", target = "";
+ String[] targetIPParts = null;
+ String targetAddrParameter = request.getParameter("targetaddr");
+ String targetAddrWarning = "";
+ if (targetAddrParameter != null && targetAddrParameter.length() > 0) {
+ Matcher targetAddrParameterMatcher =
+ ipAddressPattern.matcher(targetAddrParameter);
+ if (targetAddrParameterMatcher.matches()) {
+ String[] targetAddrParts = targetAddrParameter.split("\\.");
+ targetIP = Integer.parseInt(targetAddrParts[0]) + "."
+ + Integer.parseInt(targetAddrParts[1]) + "."
+ + Integer.parseInt(targetAddrParts[2]) + "."
+ + Integer.parseInt(targetAddrParts[3]);
+ target = targetIP;
+ targetIPParts = targetIP.split("\\.");
+ } else {
+ targetAddrWarning = "\"" + (targetAddrParameter.length() > 20 ?
+ timestampParameter.substring(0, 20) + "[...]" :
+ timestampParameter) + "\" is not a valid IP address.";
+ }
+ }
+
+ /* Parse target port parameter. */
+ String targetPortParameter = request.getParameter("targetport");
+ String targetPortWarning = "";
+ if (targetPortParameter != null && targetPortParameter.length() > 0) {
+ Pattern targetPortPattern = Pattern.compile("\\d+");
+ if (targetPortParameter.length() < 5 &&
+ targetPortPattern.matcher(targetPortParameter).matches() &&
+ !targetPortParameter.equals("0") &&
+ Integer.parseInt(targetPortParameter) < 65536) {
+ targetPort = targetPortParameter;
+ if (target != null) {
+ target += ":" + targetPort;
+ } else {
+ target = targetPort;
+ }
+ } else {
+ targetPortWarning = "\"" + (targetPortParameter.length() > 8 ?
+ targetPortParameter.substring(0, 8) + "[...]" :
+ targetPortParameter) + "\" is not a valid TCP port.";
+ }
+ }
+
+ /* If target port is provided, a target address must be provided,
+ * too. */
+ if (targetPort.length() > 0 && targetIP.length() < 1 &&
+ targetAddrWarning.length() < 1) {
+ targetAddrWarning = "Please provide an IP address.";
+ }
+
+ /* Write form with IP address and timestamp. */
+ out.println(" <form action=\"exonerator.html#relay\">\n"
+ + " <input type=\"hidden\" name=\"targetaddr\" "
+ + (targetIP.length() > 0 ? " value=\"" + targetIP + "\"" : "")
+ + "/>\n"
+ + " <input type=\"hidden\" name=\"targetPort\""
+ + (targetPort.length() > 0 ? " value=\"" + targetPort + "\"" : "")
+ + "/>\n"
+ + " <table>\n"
+ + " <tr>\n"
+ + " <td align=\"right\">IP address in question:</td>\n"
+ + " <td><input type=\"text\" name=\"ip\""
+ + (relayIP.length() > 0 ? " value=\"" + relayIP + "\"" :
+ (TEST_MODE ? " value=\"209.17.171.104\"" : ""))
+ + "\"/>"
+ + (ipWarning.length() > 0 ? "<br/><font color=\"red\">"
+ + ipWarning + "</font>" : "")
+ + "</td>\n"
+ + " <td><i>(Ex.: 1.2.3.4)</i></td>\n"
+ + " </tr>\n"
+ + " <tr>\n"
+ + " <td align=\"right\">Timestamp, in UTC:</td>\n"
+ + " <td><input type=\"text\" name=\"timestamp\""
+ + (timestampStr.length() > 0 ? " value=\"" + timestampStr + "\"" :
+ (TEST_MODE ? " value=\"2009-08-15 16:05\"" : ""))
+ + "\"/>"
+ + (timestampWarning.length() > 0 ? "<br/><font color=\"red\">"
+ + timestampWarning + "</font>" : "")
+ + "</td>\n"
+ + " <td><i>(Ex.: 2010-01-01 12:00)</i></td>\n"
+ + " </tr>\n"
+ + " <tr>\n"
+ + " <td/>\n"
+ + " <td>\n"
+ + " <input type=\"submit\">\n"
+ + " <input type=\"reset\">\n"
+ + " </td>\n"
+ + " <td/>\n"
+ + " </tr>\n"
+ + " </table>\n"
+ + " </form>\n");
+
+ if (relayIP.length() < 1 || timestampStr.length() < 1) {
+ writeFooter(out);
+ return;
+ }
+
+ /* Look up relevant consensuses. */
+ long timestampTooOld = timestamp - 15L * 60L * 60L * 1000L;
+ long timestampFrom = timestamp - 3L * 60L * 60L * 1000L;
+ long timestampTooNew = timestamp + 12L * 60L * 60L * 1000L;
+ Calendar calTooOld = Calendar.getInstance(
+ TimeZone.getTimeZone("UTC"));
+ Calendar calFrom = Calendar.getInstance(
+ TimeZone.getTimeZone("UTC"));
+ Calendar calTooNew = Calendar.getInstance(
+ TimeZone.getTimeZone("UTC"));
+ calTooOld.setTimeInMillis(timestampTooOld);
+ calFrom.setTimeInMillis(timestampFrom);
+ calTooNew.setTimeInMillis(timestampTooNew);
+ out.printf("<p>Looking up IP address %s in the relay lists published "
+ + "between %tF %tR and %s. "
+ + "Clients could have used any of these relay lists to "
+ + "select relays for their paths and build circuits using them. "
+ + "You may follow the links to relay lists and relay descriptors "
+ + "to grep for the lines printed below and confirm that results "
+ + "are correct.<br/>", relayIP, calFrom, calFrom, timestampStr);
+ SimpleDateFormat consensusTimeFormat = new SimpleDateFormat(
+ "yyyy-MM-dd-HH-mm-ss");
+ consensusTimeFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ String fromTime = consensusTimeFormat.format(
+ new Date(timestampTooOld));
+ String fromDay = fromTime.substring(0, "yyyy-MM-dd".length());
+ String fromMonth = fromDay.substring(0, "yyyy-MM".length());
+ String toTime = consensusTimeFormat.format(new Date(timestampTooNew));
+ String toDay = toTime.substring(0, "yyyy-MM-dd".length());
+ String toMonth = toDay.substring(0, "yyyy-MM".length());
+ SortedSet<File> tooOldConsensuses = new TreeSet<File>();
+ SortedSet<File> relevantConsensuses = new TreeSet<File>();
+ SortedSet<File> tooNewConsensuses = new TreeSet<File>();
+ for (File consensusMonth : consensusDirectories) {
+ String month = consensusMonth.getName().substring(
+ "consensuses-".length());
+ if (month.compareTo(fromMonth) < 0 ||
+ month.compareTo(toMonth) > 0) {
+ continue;
+ }
+ for (File consensusDay : consensusMonth.listFiles()) {
+ String day = month + "-" + consensusDay.getName();
+ if (day.compareTo(fromDay) < 0 ||
+ day.compareTo(toDay) > 0) {
+ continue;
+ }
+ for (File consensusFile : consensusDay.listFiles()) {
+ String time = consensusFile.getName().substring(0,
+ "yyyy-MM-dd-HH-mm-ss".length());
+ if (time.compareTo(fromTime) < 0 ||
+ time.compareTo(toTime) > 0) {
+ continue;
+ }
+ Date consensusDate = null;
+ try {
+ consensusDate = consensusTimeFormat.parse(time);
+ } catch (ParseException e) {
+ /* This should never happen. If it does, it's a bug. */
+ throw new RuntimeException(e);
+ }
+ long consensusTime = consensusDate.getTime();
+ if (consensusTime >= timestampTooOld &&
+ consensusTime < timestampFrom)
+ tooOldConsensuses.add(consensusFile);
+ else if (consensusTime >= timestampFrom &&
+ consensusTime <= timestamp)
+ relevantConsensuses.add(consensusFile);
+ else if (consensusTime > timestamp &&
+ consensusTime <= timestampTooNew)
+ tooNewConsensuses.add(consensusFile);
+ }
+ }
+ }
+ SortedSet<File> allConsensuses = new TreeSet<File>();
+ allConsensuses.addAll(tooOldConsensuses);
+ allConsensuses.addAll(relevantConsensuses);
+ allConsensuses.addAll(tooNewConsensuses);
+ if (allConsensuses.isEmpty()) {
+ out.println(" <p>No relay lists found!</p>\n"
+ + " <p>Result is INDECISIVE!</p>\n"
+ + " <p>We cannot make any statement whether there was "
+ + "a Tor relay running on IP address "
+ + relayIP + " at " + timestampStr + "! We "
+ + "did not find any relevant relay lists preceding the given "
+ + "time. If you think this is an error on our side, please "
+ + "<a href=\"mailto:tor-assistants@xxxxxxxxxxxxx\">contact "
+ + "us</a>!</p>\n");
+ writeFooter(out);
+ return;
+ }
+
+ // parse consensuses to find descriptors belonging to the IP address
+ SortedSet<File> positiveConsensusesNoTarget = new TreeSet<File>();
+ Set<String> addressesInSameNetwork = new HashSet<String>();
+ SortedMap<String, Set<File>> relevantDescriptors =
+ new TreeMap<String, Set<File>>();
+ SimpleDateFormat validAfterTimeFormat = new SimpleDateFormat(
+ "yyyy-MM-dd HH:mm:ss");
+ validAfterTimeFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ for (File consensus : allConsensuses) {
+ if (relevantConsensuses.contains(consensus)) {
+ String validAfterString = consensus.getName().substring(0,
+ "yyyy-MM-dd-HH-mm-ss".length());
+ Date validAfterDate = null;
+ try {
+ validAfterDate = consensusTimeFormat.parse(validAfterString);
+ } catch (ParseException e) {
+ /* This should never happen. If it does, it's a bug. */
+ throw new RuntimeException(e);
+ }
+ long validAfterTime = validAfterDate.getTime();
+ String validAfterDatetime = validAfterTimeFormat.format(
+ validAfterTime);
+ out.println(" <br/><tt>valid-after <b>"
+ + "<a href=\"consensus?valid-after="
+ + validAfterString + "\" target=\"_blank\">"
+ + validAfterDatetime + "</b></a></tt><br/>");
+ }
+ BufferedReader br = new BufferedReader(new FileReader(consensus));
+ String line;
+ while ((line = br.readLine()) != null) {
+ if (!line.startsWith("r "))
+ continue;
+ String[] parts = line.split(" ");
+ String address = parts[6];
+ if (address.equals(relayIP)) {
+ String hex = String.format("%040x", new BigInteger(1,
+ Base64.decodeBase64(parts[3] + "==")));
+ if (!relevantDescriptors.containsKey(hex))
+ relevantDescriptors.put(hex, new HashSet<File>());
+ relevantDescriptors.get(hex).add(consensus);
+ positiveConsensusesNoTarget.add(consensus);
+ if (relevantConsensuses.contains(consensus)) {
+ out.println(" <tt>r " + parts[1] + " " + parts[2] + " "
+ + "<a href=\"serverdesc?desc-id=" + hex + "\" "
+ + "target=\"_blank\">" + parts[3] + "</a> " + parts[4]
+ + " " + parts[5] + " <b>" + parts[6] + "</b> " + parts[7]
+ + " " + parts[8] + "</tt><br/>");
+ }
+ } else {
+ if (relayIP.startsWith(address.substring(0,
+ address.lastIndexOf(".")))) {
+ addressesInSameNetwork.add(address);
+ }
+ }
+ }
+ br.close();
+ }
+ if (relevantDescriptors.isEmpty()) {
+ out.printf(" <p>None found!</p>\n"
+ + " <p>Result is NEGATIVE with moderate certainty!</p>\n"
+ + " <p>We did not find IP "
+ + "address " + relayIP + " in any of the relay lists that were "
+ + "published between %tF %tR and %tF %tR.\n\nA possible "
+ + "reason for false negatives is that the relay is using a "
+ + "different IP address when generating a descriptor than for "
+ + "exiting to the Internet. We hope to provide better checks "
+ + "for this case in the future.</p>\n", calTooOld, calTooOld,
+ calTooNew, calTooNew);
+ if (!addressesInSameNetwork.isEmpty()) {
+ out.println(" <p>The following other IP addresses of Tor "
+ + "relays were found in the mentioned relay lists that "
+ + "are in the same /24 network and that could be related to "
+ + "IP address " + relayIP + ":</p>\n");
+ for (String s : addressesInSameNetwork) {
+ out.println(" <p>" + s + "</p>\n");
+ }
+ }
+ writeFooter(out);
+ return;
+ }
+
+ // print out result
+ Set<File> matches = positiveConsensusesNoTarget;
+ if (matches.contains(relevantConsensuses.last())) {
+ out.println(" <p>Result is POSITIVE with high certainty!"
+ + "</p>\n"
+ + " <p>We found one or more relays on IP address "
+ + relayIP
+ + " in the most recent relay list preceding " + timestampStr
+ + " that clients were likely to know.</p>\n");
+ } else {
+ boolean inOtherRelevantConsensus = false,
+ inTooOldConsensuses = false,
+ inTooNewConsensuses = false;
+ for (File f : matches)
+ if (relevantConsensuses.contains(f))
+ inOtherRelevantConsensus = true;
+ else if (tooOldConsensuses.contains(f))
+ inTooOldConsensuses = true;
+ else if (tooNewConsensuses.contains(f))
+ inTooNewConsensuses = true;
+ if (inOtherRelevantConsensus) {
+ out.println(" <p>Result is POSITIVE "
+ + "with moderate certainty!</p>\n");
+ out.println("<p>We found one or more relays on IP address "
+ + relayIP
+ + ", but not in the relay list immediately preceding "
+ + timestampStr + ". A possible reason for the relay being "
+ + "missing in the last relay list preceding the given time might "
+ + "be that some of the directory authorities had difficulties "
+ + "connecting to the relay. However, clients might still have "
+ + "used the relay.</p>\n");
+ } else {
+ out.println(" <p>Result is NEGATIVE "
+ + "with high certainty!</p>\n");
+ out.println(" <p>We did not find any relay on IP address "
+ + relayIP
+ + " in the relay lists 3 hours preceding " + timestampStr
+ + ".</p>\n");
+ if (inTooOldConsensuses || inTooNewConsensuses) {
+ if (inTooOldConsensuses && !inTooNewConsensuses) {
+ out.println(" <p>Note that we found a matching relay in "
+ + "relay lists that were published between 5 and 3 "
+ + "hours before " + timestampStr + ".</p>\n");
+ } else if (!inTooOldConsensuses && inTooNewConsensuses) {
+ out.println(" <p>Note that we found a matching relay in "
+ + "relay lists that were published up to 2 hours after "
+ + timestampStr + ".</p>\n");
+ } else {
+ out.println(" <p>Note that we found a matching relay in "
+ + "relay lists that were published between 5 and 3 "
+ + "hours before and in relay lists that were published up "
+ + "to 2 hours after " + timestampStr + ".</p>\n");
+ }
+ out.println("<p>Make sure that the timestamp you provided is "
+ + "in the correct timezone: UTC (or GMT).</p>");
+ }
+ writeFooter(out);
+ return;
+ }
+ }
+
+ /* Second part: target */
+ out.println("<br/><a id=\"exit\"/><h3>Was this relay configured to "
+ + "permit exiting to a given target?</h3>");
+
+ if (serverDescriptorDirectories.isEmpty()) {
+ out.println("<p><font color=\"red\"><b>Warning: </b></font>This "
+ + "server doesn't have any relay descriptors available. If "
+ + "this problem persists, please "
+ + "<a href=\"mailto:tor-assistants@xxxxxxxxxxxxx\">let us "
+ + "know</a>!</p>\n");
+ writeFooter(out);
+ return;
+ }
+
+ out.println(" <form action=\"exonerator.html#exit\">\n"
+ + " <input type=\"hidden\" name=\"timestamp\"\n"
+ + " value=\"" + timestampStr + "\"/>\n"
+ + " <input type=\"hidden\" name=\"ip\" "
+ + "value=\"" + relayIP + "\"/>\n"
+ + " <table>\n"
+ + " <tr>\n"
+ + " <td align=\"right\">Target address:</td>\n"
+ + " <td><input type=\"text\" name=\"targetaddr\""
+ + (targetIP.length() > 0 ? " value=\"" + targetIP + "\"" :
+ (TEST_MODE ? " value=\"209.85.129.104\"" : ""))
+ + "\"/>"
+ + (targetAddrWarning.length() > 0 ? "<br/><font color=\"red\">"
+ + targetAddrWarning + "</font>" : "")
+ + "</td>\n"
+ + " <td><i>(Ex.: 4.3.2.1)</i></td>\n"
+ + " </tr>\n"
+ + " <tr>\n"
+ + " <td align=\"right\">Target port:</td>\n"
+ + " <td><input type=\"text\" name=\"targetport\""
+ + (targetPort.length() > 0 ? " value=\"" + targetPort + "\"" :
+ (TEST_MODE ? " value=\"80\"" : ""))
+ + "/>"
+ + (targetPortWarning.length() > 0 ? "<br/><font color=\"red\">"
+ + targetPortWarning + "</font>" : "")
+ + "</td>\n"
+ + " <td><i>(Ex.: 80)</i></td>\n"
+ + " </tr>\n"
+ + " <tr>\n"
+ + " <td/>\n"
+ + " <td>\n"
+ + " <input type=\"submit\">\n"
+ + " <input type=\"reset\">\n"
+ + " </td>\n"
+ + " <td/>\n"
+ + " </tr>\n"
+ + " </table>\n"
+ + " </form>\n");
+
+ if (targetIP.length() < 1) {
+ writeFooter(out);
+ return;
+ }
+
+ // parse router descriptors to check exit policies
+ out.println("<p>Searching the relay descriptors published by the relay "
+ + "on IP address " + relayIP + " to find out whether this relay "
+ + "permitted exiting to " + target + ". You may follow the links "
+ + "above to the relay descriptors and grep them for the lines "
+ + "printed below to confirm that results are correct.</p>");
+ SortedSet<File> positiveConsensuses = new TreeSet<File>();
+ Set<String> missingDescriptors = new HashSet<String>();
+ Set<String> descriptors = relevantDescriptors.keySet();
+ for (String descriptor : descriptors) {
+ for (File directory : serverDescriptorDirectories) {
+ File subDirectory = new File(directory.getAbsolutePath() + "/"
+ + descriptor.substring(0, 1) + "/"
+ + descriptor.substring(1, 2));
+ if (subDirectory.exists()) {
+ File descriptorFile = new File(subDirectory.getAbsolutePath()
+ + "/" + descriptor);
+ if (!descriptorFile.exists()) {
+ continue;
+ }
+ missingDescriptors.remove(descriptor);
+ BufferedReader br = new BufferedReader(
+ new FileReader(descriptorFile));
+ String line, routerLine = null, publishedLine = null;
+ StringBuilder acceptRejectLines = new StringBuilder();
+ boolean foundMatch = false;
+ while ((line = br.readLine()) != null) {
+ if (line.startsWith("router ")) {
+ routerLine = line;
+ } else if (line.startsWith("published ")) {
+ publishedLine = line;
+ } else if (line.startsWith("reject ") ||
+ line.startsWith("accept ")) {
+ if (foundMatch) {
+ out.println("<tt> " + line + "</tt><br/>");
+ continue;
+ }
+ boolean ruleAccept = line.split(" ")[0].equals("accept");
+ String ruleAddress = line.split(" ")[1].split(":")[0];
+ if (!ruleAddress.equals("*")) {
+ if (!ruleAddress.contains("/") &&
+ !ruleAddress.equals(targetIP)) {
+ acceptRejectLines.append("<tt> " + line + "</tt><br/>\n");
+ continue; // IP address does not match
+ }
+ String[] ruleIPParts = ruleAddress.split("/")[0].
+ split("\\.");
+ int ruleNetwork = ruleAddress.contains("/") ?
+ Integer.parseInt(ruleAddress.split("/")[1]) : 32;
+ for (int i = 0; i < 4; i++) {
+ if (ruleNetwork == 0) {
+ break;
+ } else if (ruleNetwork >= 8) {
+ if (ruleIPParts[i].equals(targetIPParts[i])) {
+ ruleNetwork -= 8;
+ } else {
+ break;
+ }
+ } else {
+ int mask = 255 ^ 255 >>> ruleNetwork;
+ if ((Integer.parseInt(ruleIPParts[i]) & mask) ==
+ (Integer.parseInt(targetIPParts[i]) & mask)) {
+ ruleNetwork = 0;
+ }
+ break;
+ }
+ }
+ if (ruleNetwork > 0) {
+ acceptRejectLines.append("<tt> " + line + "</tt><br/>\n");
+ continue; // IP address does not match
+ }
+ }
+ String rulePort = line.split(" ")[1].split(":")[1];
+ if (targetPort.length() < 1 && !ruleAccept &&
+ !rulePort.equals("*")) {
+ acceptRejectLines.append("<tt> " + line + "</tt><br/>\n");
+ continue; // with no port given, we only consider
+ // reject :* rules as matching
+ }
+ if (targetPort.length() > 0) {
+ if (!rulePort.equals("*") &&
+ !targetPort.equals(rulePort)) {
+ acceptRejectLines.append("<tt> " + line + "</tt><br/>\n");
+ continue; // ports do not match
+ }
+ }
+ boolean relevantMatch = false;
+ for (File f : relevantDescriptors.get(descriptor))
+ if (relevantConsensuses.contains(f))
+ relevantMatch = true;
+ if (relevantMatch) {
+ String[] routerParts = routerLine.split(" ");
+ out.println("<br/><tt>" + routerParts[0] + " "
+ + routerParts[1] + " <b>" + routerParts[2] + "</b> "
+ + routerParts[3] + " " + routerParts[4] + " "
+ + routerParts[5] + "</tt><br/>");
+ String[] publishedParts = publishedLine.split(" ");
+ out.println("<tt>" + publishedParts[0] + " <b>"
+ + publishedParts[1] + " " + publishedParts[2]
+ + "</b></tt><br/>");
+ out.println(acceptRejectLines.toString());
+ out.println("<tt><b>" + line + "</b></tt><br/>");
+ foundMatch = true;
+ }
+ if (ruleAccept) {
+ positiveConsensuses.addAll(
+ relevantDescriptors.get(descriptor));
+ }
+ }
+ }
+ br.close();
+ }
+ }
+ }
+
+ // print out result
+// TODO don't repeat results from above
+ matches = positiveConsensuses;
+ if (matches.contains(relevantConsensuses.last())) {
+ out.println(" <p>Result is POSITIVE with high certainty!</p>\n"
+ + " <p>We found one or more relays on IP address "
+ + relayIP + " permitting exit to " + target
+ + " in the most recent relay list preceding " + timestampStr
+ + " that clients were likely to know.</p>\n");
+ writeFooter(out);
+ return;
+ }
+ boolean resultIndecisive = target.length() > 0
+ && !missingDescriptors.isEmpty();
+ if (resultIndecisive) {
+ out.println(" <p>Result is INDECISIVE!</p>\n"
+ + " <p>At least one referenced descriptor could not be found. This "
+ + "is a rare case, but one that (apparently) happens. We cannot "
+ + "make any good statement about exit relays without these "
+ + "descriptors. The following descriptors are missing:</p>");
+ for (String desc : missingDescriptors)
+ out.println(" <p>" + desc + "</p>\n");
+ }
+ boolean inOtherRelevantConsensus = false, inTooOldConsensuses = false,
+ inTooNewConsensuses = false;
+ for (File f : matches)
+ if (relevantConsensuses.contains(f))
+ inOtherRelevantConsensus = true;
+ else if (tooOldConsensuses.contains(f))
+ inTooOldConsensuses = true;
+ else if (tooNewConsensuses.contains(f))
+ inTooNewConsensuses = true;
+ if (inOtherRelevantConsensus) {
+ if (!resultIndecisive) {
+ out.println(" <p>Result is POSITIVE "
+ + "with moderate certainty!</p>\n");
+ }
+ out.println("<p>We found one or more relays on IP address "
+ + relayIP + " permitting exit to " + target
+ + ", but not in the relay list immediately preceding "
+ + timestampStr + ". A possible reason for the relay being "
+ + "missing in the last relay list preceding the given time might "
+ + "be that some of the directory authorities had difficulties "
+ + "connecting to the relay. However, clients might still have "
+ + "used the relay.</p>\n");
+ } else {
+ if (!resultIndecisive) {
+ out.println(" <p>Result is NEGATIVE "
+ + "with high certainty!</p>\n");
+ }
+ out.println(" <p>We did not find any relay on IP address "
+ + relayIP + " permitting exit to " + target
+ + " in the relay list 3 hours preceding " + timestampStr
+ + ".</p>\n");
+ if (inTooOldConsensuses || inTooNewConsensuses) {
+ if (inTooOldConsensuses && !inTooNewConsensuses)
+ out.println(" <p>Note that we found a matching relay in "
+ + "relay lists that were published between 5 and 3 "
+ + "hours before " + timestampStr + ".</p>\n");
+ else if (!inTooOldConsensuses && inTooNewConsensuses)
+ out.println(" <p>Note that we found a matching relay in "
+ + "relay lists that were published up to 2 hours after "
+ + timestampStr + ".</p>\n");
+ else
+ out.println(" <p>Note that we found a matching relay in "
+ + "relay lists that were published between 5 and 3 "
+ + "hours before and in relay lists that were published up "
+ + "to 2 hours after " + timestampStr + ".</p>\n");
+ out.println("<p>Make sure that the timestamp you provided is "
+ + "in the correct timezone: UTC (or GMT).</p>");
+ }
+ }
+ if (target != null) {
+ if (positiveConsensuses.isEmpty() &&
+ !positiveConsensusesNoTarget.isEmpty())
+ out.println(" <p>Note that although the found relay(s) did "
+ + "not permit exiting to " + target + ", there have been one "
+ + "or more relays running at the given time.</p>");
+ }
+
+ /* Finish writing response. */
+ writeFooter(out);
+ }
+}
+
diff --git a/src/org/torproject/ernie/web/ServerDescriptorServlet.java b/src/org/torproject/ernie/web/ServerDescriptorServlet.java
new file mode 100644
index 0000000..056ce9c
--- /dev/null
+++ b/src/org/torproject/ernie/web/ServerDescriptorServlet.java
@@ -0,0 +1,86 @@
+package org.torproject.ernie.web;
+
+import javax.servlet.*;
+import javax.servlet.http.*;
+import java.io.*;
+import java.math.*;
+import java.text.*;
+import java.util.*;
+import java.util.regex.*;
+
+public class ServerDescriptorServlet extends HttpServlet {
+
+ public void doGet(HttpServletRequest request,
+ HttpServletResponse response) throws IOException,
+ ServletException {
+
+ String descIdParameter = request.getParameter("desc-id");
+
+ /* Check if we have a descriptors directory. */
+ // TODO make this configurable!
+ File archiveDirectory = new File("/srv/metrics.torproject.org/archives");
+ if (!archiveDirectory.exists() || !archiveDirectory.isDirectory()) {
+ /* Oops, we don't have any descriptors to serve. */
+// TODO change to internal server error
+ response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ return;
+ }
+
+ /* Check desc-id parameter. */
+ if (descIdParameter == null || descIdParameter.length() < 4) {
+// TODO is there something like "wrong parameter"?
+ response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ return;
+ }
+ Pattern descIdPattern = Pattern.compile("^[0-9a-f]+$");
+ Matcher descIdMatcher = descIdPattern.matcher(descIdParameter);
+ if (!descIdMatcher.matches()) {
+// TODO is there something like "wrong parameter"?
+ response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ return;
+ }
+
+ for (File directory : archiveDirectory.listFiles()) {
+ if (!directory.isDirectory() ||
+ !directory.getName().startsWith("server-descriptors-")) {
+ continue;
+ }
+ File subDirectory = new File(directory.getAbsolutePath() + "/"
+ + descIdParameter.substring(0, 1) + "/"
+ + descIdParameter.substring(1, 2));
+ if (subDirectory.exists()) {
+ for (File serverDescriptorFile : subDirectory.listFiles()) {
+ if (!serverDescriptorFile.getName().startsWith(descIdParameter)) {
+ continue;
+ }
+
+ /* Found it! Read file from disk and write it to response. */
+ BufferedInputStream input = null;
+ BufferedOutputStream output = null;
+ try {
+ response.setContentType("text/plain");
+ response.setHeader("Content-Length", String.valueOf(
+ serverDescriptorFile.length()));
+ response.setHeader("Content-Disposition",
+ "inline; filename=\"" + serverDescriptorFile.getName() + "\"");
+ input = new BufferedInputStream(new FileInputStream(
+ serverDescriptorFile), 1024);
+ output = new BufferedOutputStream(response.getOutputStream(), 1024);
+ byte[] buffer = new byte[1024];
+ int length;
+ while ((length = input.read(buffer)) > 0) {
+ output.write(buffer, 0, length);
+ }
+ } finally {
+ output.close();
+ input.close();
+ }
+ }
+ }
+ }
+
+ /* Not found. */
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ }
+}
+
--
1.6.5