[or-cvs] [metrics-web/master 3/4] Added a Torstatus clone.

karsten at torproject.org karsten at torproject.org
Thu Dec 16 07:42:26 UTC 2010


Author: Kevin Berry <xckjb88 at gmail.com>
Date: Wed, 20 Oct 2010 10:48:40 -0400
Subject: Added a Torstatus clone.
Commit: 6fbe05d1c85fdbb2470bbab681a2d0511906d5d7

See the original Torstatus at torstatus.blutmagie.de. This version of torstatus
attempts to use the metrics-db database to portray a close-to-live view of the
Tor network from the consensus. It shows a sortable list of routers and various
router details, as well as a bandwidth graph for each router.
---
 etc/web.xml                                        |   25 ++++
 rserve/graphs.R                                    |   21 +++
 .../ernie/web/GraphParameterChecker.java           |   32 ++++
 .../torproject/ernie/web/NetworkStatusServlet.java |  132 +++++++++++++++++
 .../torproject/ernie/web/RouterDetailServlet.java  |  153 ++++++++++++++++++++
 src/org/torproject/ernie/web/TimeInterval.java     |   40 +++++
 web/WEB-INF/banner.jsp                             |    4 +
 web/WEB-INF/networkstatus.jsp                      |   41 ++++++
 web/WEB-INF/routerdetail.jsp                       |  112 ++++++++++++++
 9 files changed, 560 insertions(+), 0 deletions(-)
 create mode 100644 src/org/torproject/ernie/web/NetworkStatusServlet.java
 create mode 100644 src/org/torproject/ernie/web/RouterDetailServlet.java
 create mode 100644 src/org/torproject/ernie/web/TimeInterval.java
 create mode 100644 web/WEB-INF/networkstatus.jsp
 create mode 100644 web/WEB-INF/routerdetail.jsp

diff --git a/etc/web.xml b/etc/web.xml
index cc2b3db..0192fb0 100644
--- a/etc/web.xml
+++ b/etc/web.xml
@@ -119,6 +119,27 @@
   </servlet-mapping>
 
   <servlet>
+    <servlet-name>NetworkStatus</servlet-name>
+    <servlet-class>
+      org.torproject.ernie.web.NetworkStatusServlet
+    </servlet-class>
+  </servlet>
+  <servlet-mapping>
+    <servlet-name>NetworkStatus</servlet-name>
+    <url-pattern>/networkstatus.html</url-pattern>
+  </servlet-mapping>
+  <servlet>
+    <servlet-name>RouterDetail</servlet-name>
+    <servlet-class>
+      org.torproject.ernie.web.RouterDetailServlet
+    </servlet-class>
+  </servlet>
+  <servlet-mapping>
+    <servlet-name>RouterDetail</servlet-name>
+    <url-pattern>/routerdetail.html</url-pattern>
+  </servlet-mapping>
+
+  <servlet>
     <servlet-name>Relay</servlet-name>
     <servlet-class>
       org.torproject.ernie.web.RelayServlet
@@ -194,6 +215,10 @@
     <servlet-name>GraphImage</servlet-name>
     <url-pattern>/torperf.png</url-pattern>
   </servlet-mapping>
+  <servlet-mapping>
+    <servlet-name>GraphImage</servlet-name>
+    <url-pattern>/routerdetail.png</url-pattern>
+  </servlet-mapping>
 
   <servlet>
     <servlet-name>Csv</servlet-name>
diff --git a/rserve/graphs.R b/rserve/graphs.R
index d6a8363..8fd0d20 100644
--- a/rserve/graphs.R
+++ b/rserve/graphs.R
@@ -373,3 +373,24 @@ plot_torperf <- function(start, end, source, filesize, path) {
   ggsave(filename = path, width = 8, height = 5, dpi = 72)
 }
 
+plot_routerdetail <- function(start, end, fingerprint, path) {
+  drv <- dbDriver("PostgreSQL")
+  con <- dbConnect(drv, user = dbuser, password = dbpassword, dbname = db)
+  q <- paste("select avg(bandwidth)::integer as bw, date(validafter) ",
+      "from statusentry where fingerprint='",fingerprint,"' ",
+      "group by date(validafter);", sep = "")
+  rs <- dbSendQuery(con, q)
+  routerdetail <- fetch(rs, n = -1)
+  routerdetail <- melt(routerdetail, id="date")
+  dbDisconnect(con)
+  dbUnloadDriver(drv)
+  ggplot(routerdetail, aes(x = as.Date(date, "%Y-%m-%d"), y = value,
+    colour = variable)) + geom_line(size = 1) +
+    scale_x_date(name = paste("\nThe Tor Project - ",
+        "https://metrics.torproject.org/", sep = "")) +
+    scale_y_continuous(name = "") +
+    scale_colour_hue("", breaks = c("bw"),
+        labels = c("Bandwidth")) +
+    opts(title = paste("Bandwidth history for ", fingerprint, "\n", sep = ""))
+  ggsave(filename = path, width = 8, height = 5, dpi = 72)
+}
diff --git a/src/org/torproject/ernie/web/GraphParameterChecker.java b/src/org/torproject/ernie/web/GraphParameterChecker.java
index d4e085f..6024e1f 100644
--- a/src/org/torproject/ernie/web/GraphParameterChecker.java
+++ b/src/org/torproject/ernie/web/GraphParameterChecker.java
@@ -2,6 +2,7 @@ package org.torproject.ernie.web;
 
 import java.text.*;
 import java.util.*;
+import java.util.regex.*;
 
 /**
  * Checks request parameters passed to graph-generating servlets.
@@ -54,6 +55,8 @@ public class GraphParameterChecker {
     this.availableGraphs.put("gettor", "start,end,bundle,filename");
     this.availableGraphs.put("torperf",
          "start,end,source,filesize,filename");
+    this.availableGraphs.put("routerdetail",
+         "start,end,fingerprint,filename");
 
     this.knownParameterValues = new HashMap<String, String>();
     this.knownParameterValues.put("flag",
@@ -63,6 +66,7 @@ public class GraphParameterChecker {
     this.knownParameterValues.put("bundle", "all,en,zh_CN,fa");
     this.knownParameterValues.put("source", "siv,moria,torperf");
     this.knownParameterValues.put("filesize", "50kb,1mb,5mb");
+    this.knownParameterValues.put("fingerprint", "[0-9a-f]{40}");
   }
 
   /**
@@ -239,6 +243,34 @@ public class GraphParameterChecker {
       recognizedGraphParameters.put("filesize", filesizeParameter);
     }
 
+    /* Parse fingerprint field for the torstatus graph. Match it against
+     * a hexadecimal regular expression and make sure it is 40 characters
+     * long. */
+    if (supportedGraphParameters.contains("fingerprint")) {
+      String[] fingerprint = (String[])requestParameters.get("fingerprint");
+      if (fingerprint != null) {
+        if (!Pattern.matches(this.knownParameterValues.get("fingerprint"),
+            fingerprint[0]) || fingerprint[0].length() != 40) {
+          return null;
+        }
+      } else {
+        return null;
+      }
+
+      /* Set "mandatory" start and end parameters to this hour so it stays up to
+       * date (the start and end parameters aren't needed for this graph).
+       * Round the timestamp to the lowest hour */
+      long msDay = 1000 * 60 * 60;
+      long now = System.currentTimeMillis();
+      long nearestHour = now - (now % msDay);
+
+      String startParameter[] = { Long.toString(nearestHour) };
+      String endParameter[] = { "" };
+      recognizedGraphParameters.put("start", startParameter);
+      recognizedGraphParameters.put("end", endParameter);
+      recognizedGraphParameters.put("fingerprint", fingerprint);
+    }
+
     /* We now have a map with all required graph parameters. Return it. */
     return recognizedGraphParameters;
   }
diff --git a/src/org/torproject/ernie/web/NetworkStatusServlet.java b/src/org/torproject/ernie/web/NetworkStatusServlet.java
new file mode 100644
index 0000000..6c22fd0
--- /dev/null
+++ b/src/org/torproject/ernie/web/NetworkStatusServlet.java
@@ -0,0 +1,132 @@
+package org.torproject.ernie.web;
+
+import java.io.*;
+import java.util.*;
+import java.sql.*;
+import java.util.logging.*;
+import java.text.*;
+
+import javax.naming.*;
+import javax.servlet.*;
+import javax.servlet.http.*;
+import javax.sql.*;
+
+public class NetworkStatusServlet extends HttpServlet {
+
+  private DataSource ds;
+
+  private Logger logger;
+
+  public void init() {
+
+    /* Initialize logger. */
+    this.logger = Logger.getLogger(NetworkStatusServlet.class.toString());
+
+    /* Look up data source. */
+    try {
+      Context cxt = new InitialContext();
+      this.ds = (DataSource) cxt.lookup("java:comp/env/jdbc/tordir");
+      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 {
+
+    String sort, order;
+
+    List<Map<String, Object>> status =
+        new ArrayList<Map<String, Object>>();
+
+    Set<String> validSort = new HashSet<String>(
+        Arrays.asList(("nickname,bandwidth,orport,dirport,isbadexit,"
+            + "uptime").split(",")));
+
+    Set<String> validOrder = new HashSet<String>(
+        Arrays.asList(("desc,asc").split(",")));
+
+    /* Initialize sort and order parameters from GET */
+    try {
+      sort = request.getParameter("sort").toLowerCase();
+      order = request.getParameter("order").toLowerCase();
+    } catch (Exception e) {
+      sort = "nickname";
+      order = "asc";
+    }
+
+    /* Check and set default parameters in case of bad user data. */
+    if (!validSort.contains(sort))    { sort = "nickname"; }
+    if (!validOrder.contains(order))  { order = "desc"; }
+
+    /* Connect to the database and retrieve data set */
+    try {
+      Connection conn = this.ds.getConnection();
+      Statement statement = conn.createStatement();
+
+      String dbsort = ((sort.equals("uptime") || sort.equals("platform"))
+          ? "d." : "s.") + sort;
+      String query = "SELECT s.*, "
+          + "d.uptime AS uptime, d.platform AS platform "
+          + "FROM statusentry s "
+          + "JOIN descriptor d "
+          + "ON d.descriptor=s.descriptor "
+          + "WHERE s.validafter = "
+              + "(SELECT MAX(validafter) FROM statusentry) "
+          + "ORDER BY " + dbsort + " " + order;
+
+      ResultSet rs = statement.executeQuery(query);
+
+      while (rs.next()) {
+        Map<String, Object> row = new HashMap<String, Object>();
+        row.put("validafter", rs.getTimestamp(1));
+        row.put("nickname", rs.getString(2));
+        row.put("fingerprint", rs.getString(3));
+        row.put("descriptor", rs.getString(4));
+        row.put("published", rs.getTimestamp(5));
+        row.put("address", rs.getString(6));
+        row.put("orport", rs.getInt(7));
+        row.put("dirport", rs.getInt(8));
+        row.put("isauthority", rs.getBoolean(9));
+        row.put("isbadexit", rs.getBoolean(10));
+        row.put("isbaddirectory", rs.getBoolean(11));
+        row.put("isexit", rs.getBoolean(12));
+        row.put("isfast", rs.getBoolean(13));
+        row.put("isguard", rs.getBoolean(14));
+        row.put("ishsdir", rs.getBoolean(15));
+        row.put("isnamed", rs.getBoolean(16));
+        row.put("isstable", rs.getBoolean(17));
+        row.put("isrunning", rs.getBoolean(18));
+        row.put("isunnamed", rs.getBoolean(19));
+        row.put("isvalid", rs.getBoolean(20));
+        row.put("isv2dir", rs.getBoolean(21));
+        row.put("isv3dir", rs.getBoolean(22));
+        row.put("version", rs.getString(23));
+        row.put("bandwidth", rs.getBigDecimal(24));
+        row.put("ports", rs.getString(25));
+        row.put("rawdesc", rs.getBytes(26));
+        row.put("uptime", TimeInterval.format(
+            rs.getBigDecimal(27).intValue()));
+        row.put("platform", rs.getString(28));
+        row.put("validafterts", rs.getTimestamp(1).getTime());
+
+        status.add(row);
+      }
+
+      conn.close();
+      request.setAttribute("status", status);
+      request.setAttribute("sort", sort);
+      request.setAttribute("order", (order.equals("desc")) ? "asc" : "desc");
+
+    } catch (SQLException e) {
+      response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+      this.logger.log(Level.WARNING, "Database error", e);
+      return;
+    }
+
+    /* Forward the request to the JSP that does all the hard work. */
+    request.getRequestDispatcher("WEB-INF/networkstatus.jsp").forward(request,
+        response);
+  }
+}
diff --git a/src/org/torproject/ernie/web/RouterDetailServlet.java b/src/org/torproject/ernie/web/RouterDetailServlet.java
new file mode 100644
index 0000000..cd78d09
--- /dev/null
+++ b/src/org/torproject/ernie/web/RouterDetailServlet.java
@@ -0,0 +1,153 @@
+package org.torproject.ernie.web;
+
+import java.io.*;
+import java.util.*;
+import java.sql.*;
+import java.util.logging.*;
+import java.text.*;
+
+import javax.naming.*;
+import javax.servlet.*;
+import javax.servlet.http.*;
+import javax.sql.*;
+
+public class RouterDetailServlet extends HttpServlet {
+
+  private DataSource ds;
+
+  private Logger logger;
+
+  public void init() {
+
+    /* Initialize logger. */
+    this.logger = Logger.getLogger(NetworkStatusServlet.class.toString());
+
+    /* Look up data source. */
+    try {
+      Context cxt = new InitialContext();
+      this.ds = (DataSource) cxt.lookup("java:comp/env/jdbc/tordir");
+      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 {
+
+    String fingerprint;
+    java.sql.Timestamp validafter;
+
+    try {
+      fingerprint = request.getParameter("fingerprint");
+      validafter = new java.sql.Timestamp(
+          Long.parseLong(request.getParameter("validafter")));
+
+    } catch (Exception e) {
+      response.sendError(HttpServletResponse.SC_NOT_FOUND);
+      return;
+    }
+
+    String query = "SELECT s.*, d.uptime, d.platform, d.rawdesc as rawdescd "
+        + "FROM statusentry s "
+        + "JOIN descriptor d "
+        + "ON s.descriptor = d.descriptor "
+        + "WHERE s.fingerprint = ? "
+        + "AND s.validafter = ? "
+        + "LIMIT 1";
+
+    try {
+      Connection conn = this.ds.getConnection();
+      PreparedStatement ps = conn.prepareStatement(query);
+      ps.setString(1, fingerprint);
+      ps.setTimestamp(2, validafter);
+      ResultSet rs = ps.executeQuery();
+      if (rs.next()) {
+        request.setAttribute("validafter", rs.getTimestamp("validafter"));
+        request.setAttribute("nickname", rs.getString("nickname"));
+        request.setAttribute("fingerprint", rs.getString("fingerprint"));
+        request.setAttribute("descriptor", rs.getString("descriptor"));
+        request.setAttribute("published", rs.getTimestamp("published"));
+        request.setAttribute("address", rs.getString("address"));
+        request.setAttribute("orport", rs.getInt("orport"));
+        request.setAttribute("dirport", rs.getInt("dirport"));
+        request.setAttribute("isauthority", rs.getBoolean("isauthority"));
+        request.setAttribute("isbadexit", rs.getBoolean("isbadexit"));
+        request.setAttribute("isbaddirectory", rs.getBoolean("isbaddirectory"));
+        request.setAttribute("isexit", rs.getBoolean("isexit"));
+        request.setAttribute("isfast", rs.getBoolean("isfast"));
+        request.setAttribute("isguard", rs.getBoolean("isguard"));
+        request.setAttribute("ishsdir", rs.getBoolean("ishsdir"));
+        request.setAttribute("isnamed", rs.getBoolean("isnamed"));
+        request.setAttribute("isstable", rs.getBoolean("isstable"));
+        request.setAttribute("isrunning", rs.getBoolean("isrunning"));
+        request.setAttribute("isunnamed", rs.getBoolean("isunnamed"));
+        request.setAttribute("isvalid", rs.getBoolean("isvalid"));
+        request.setAttribute("isv2dir", rs.getBoolean("isv2dir"));
+        request.setAttribute("isv3dir", rs.getBoolean("isv3dir"));
+        request.setAttribute("version", rs.getString("version"));
+        request.setAttribute("bandwidth", rs.getBigDecimal("bandwidth"));
+        request.setAttribute("ports", rs.getString("ports"));
+        request.setAttribute("uptime", TimeInterval.format(
+            rs.getBigDecimal("uptime").intValue()));
+        request.setAttribute("platform", rs.getString("platform"));
+
+        //Find onion key and signing key
+        byte[] rawdesc = rs.getBytes("rawdescd");
+        String rawdesc_str = new String(rawdesc, "UTF-8");
+        String[] lines = rawdesc_str.split("\n");
+        String onion_key = "";
+        String signing_key = "";
+        int line = 0;
+        for (String t : lines)  {
+          if (t.startsWith("onion-key"))  {
+            int start = 0, end = 0;
+            if (lines[line+1].startsWith("-----BEGIN")) {
+              start = line + 1;
+              for (int i = line + 1; i < lines.length; i++) {
+                if (lines[i].startsWith("-----END"))  {
+                  end = i + 1;
+                  break;
+                }
+              }
+              for (int i = start; i < end; i++ )  {
+                onion_key += lines[i] + "<br/>";
+              }
+            }
+          }
+          if (t.startsWith("signing-key"))  {
+            int start = 0, end = 0;
+            if (lines[line+1].startsWith("-----BEGIN")) {
+              start = line + 1;
+              for (int i = line + 1; i < lines.length; i++) {
+                if (lines[i].startsWith("-----END"))  {
+                  end = i + 1;
+                  break;
+                }
+              }
+              for (int i = start; i < end; i++ )  {
+                signing_key += lines[i] + "<br/>";
+              }
+            }
+          }
+          line++;
+        }
+        request.setAttribute("onion_key", onion_key);
+        request.setAttribute("signing_key", signing_key);
+      } else {
+        /* There were zero results in the set */
+        response.sendError(HttpServletResponse.SC_BAD_REQUEST);
+      }
+      conn.close();
+
+    } catch (SQLException e)  {
+      response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+      this.logger.log(Level.WARNING, "Database error", e);
+      return;
+    }
+
+    /* Forward the request to the JSP that does all the hard work. */
+    request.getRequestDispatcher("WEB-INF/routerdetail.jsp").forward(request,
+        response);
+  }
+}
diff --git a/src/org/torproject/ernie/web/TimeInterval.java b/src/org/torproject/ernie/web/TimeInterval.java
new file mode 100644
index 0000000..30a95e4
--- /dev/null
+++ b/src/org/torproject/ernie/web/TimeInterval.java
@@ -0,0 +1,40 @@
+package org.torproject.ernie.web;
+
+/* The standard JDK lacks anything for processing time intervals */
+
+public class TimeInterval {
+
+  private static final int YEARS = 31536000;
+  private static final int DAYS = 86400;
+  private static final int HOURS = 3600;
+  private static final int MINUTES = 60;
+
+  /* Format an interval like YY'y' DD'd' HH:MM:SS */
+  public static String format(int seconds) {
+
+    String fmt = "";
+
+    if (seconds / YEARS > 0)  {
+      fmt += (seconds / YEARS) + "y ";
+      seconds -= ((seconds / YEARS) * YEARS);
+    }
+
+    if (seconds / DAYS > 0)  {
+      fmt += (seconds / DAYS) + "d ";
+      seconds -= ((seconds / DAYS) * DAYS);
+    }
+
+    fmt += ((seconds / HOURS < 10) ? "0" : "")
+        + (seconds / HOURS) + ":";
+    seconds -= ((seconds / HOURS) * HOURS);
+
+    fmt += ((seconds / MINUTES < 10) ? "0" : "")
+        + (seconds / MINUTES) + ":";
+    seconds -= ((seconds / MINUTES) * MINUTES);
+
+    fmt += seconds + ((seconds < 10) ? "0" : "");
+
+    return fmt;
+  }
+
+}
diff --git a/web/WEB-INF/banner.jsp b/web/WEB-INF/banner.jsp
index 335f44c..e5d2e55 100644
--- a/web/WEB-INF/banner.jsp
+++ b/web/WEB-INF/banner.jsp
@@ -35,11 +35,15 @@
             %>>Performance</a>
       </font>
     <%} else if (currentPage.endsWith("status.jsp") ||
+                 currentPage.endsWith("networkstatus.jsp") ||
                  currentPage.endsWith("exonerator.jsp") ||
                  currentPage.endsWith("relay-search.jsp") ||
                  currentPage.endsWith("consensus-health.jsp")) {
      %><br>
       <font size="2">
+        <a <%if (currentPage.endsWith("networkstatus.jsp")){
+            %>class="current"<%} else {%>href="/networkstatus.html"<%}
+            %>>Network Status</a>
         <a <%if (currentPage.endsWith("exonerator.jsp")){
             %>class="current"<%} else {%>href="/exonerator.html"<%}
             %>>ExoneraTor</a>
diff --git a/web/WEB-INF/networkstatus.jsp b/web/WEB-INF/networkstatus.jsp
new file mode 100644
index 0000000..27c854f
--- /dev/null
+++ b/web/WEB-INF/networkstatus.jsp
@@ -0,0 +1,41 @@
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<html>
+<head>
+  <title>Tor Metrics Portal: Status</title>
+  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
+  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
+  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
+</head>
+<body>
+  <div class="center">
+    <%@ include file="banner.jsp"%>
+    <div class="main-column">
+      <h2>Tor Metrics Portal: Network Status</h2>
+      <table>
+        <tr>
+          <th><a href="/networkstatus.html?sort=nickname&order=${sort=='nickname'?order:'desc'}">nickname</a></th>
+          <th><a href="/networkstatus.html?sort=bandwidth&order=${sort=='bandwidth'?order:'desc'}">bandwidth</a></th>
+          <th><a href="/networkstatus.html?sort=orport&order=${sort=='orport'?order:'desc'}">orport</a></th>
+          <th><a href="/networkstatus.html?sort=dirport&order=${sort=='dirport'?order:'desc'}">dirport</a></th>
+          <th><a href="/networkstatus.html?sort=isbadexit&order=${sort=='isbadexit'?order:'desc'}">isbadexit</a></th>
+          <th><a href="/networkstatus.html?sort=uptime&order=${sort=='uptime'?order:'desc'}">uptime</a></th>
+        </tr>
+      <c:forEach var="row" items="${status}">
+        <tr>
+          <td><a href="/routerdetail.html?fingerprint=${row['fingerprint']}&validafter=${row['validafterts']}">${row['nickname']}</a></td>
+          <td>${row['bandwidth']}</td>
+          <td>${row['orport']}</td>
+          <td>${row['dirport']}</td>
+          <td>${row['isbadexit']}</td>
+          <td>${row['uptime']}</td>
+        </tr>
+      </c:forEach>
+      </table>
+    </div>
+  </div>
+  <div class="bottom" id="bottom">
+    <%@ include file="footer.jsp"%>
+  </div>
+</body>
+</html>
diff --git a/web/WEB-INF/routerdetail.jsp b/web/WEB-INF/routerdetail.jsp
new file mode 100644
index 0000000..bf99ad0
--- /dev/null
+++ b/web/WEB-INF/routerdetail.jsp
@@ -0,0 +1,112 @@
+
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<html>
+<head>
+  <title>Tor Metrics Portal: Status</title>
+  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
+  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
+  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
+</head>
+<body>
+  <div class="center">
+    <%@ include file="banner.jsp"%>
+    <div class="main-column">
+      <h2>Tor Metrics Portal: Router Detail</h2>
+      <table>
+        <tr>
+         <td>validafter</td><td>${validafter}</td>
+        </tr>
+        <tr>
+         <td>nickname</td><td>${nickname}</td>
+        </tr>
+        <tr>
+         <td>bandwidth</td><td>${bandwidth * 0.0009765625} kBps</td>
+        </tr>
+        <tr>
+         <td>fingerprint</td><td>${fingerprint}</td>
+        </tr>
+        <tr>
+         <td>published</td><td>${published}</td>
+        </tr>
+        <tr>
+         <td>address</td><td>${address}</td>
+        </tr>
+        <tr>
+         <td>uptime</td><td>${uptime}</td>
+        </tr>
+        <tr>
+         <td>orport</td><td>${orport}</td>
+        </tr>
+        <tr>
+         <td>dirport</td><td>${dirport}</td>
+        </tr>
+        <tr>
+         <td>isauthority</td><td>${isauthority}</td>
+        </tr>
+        <tr>
+         <td>isbadexit</td><td>${isbadexit}</td>
+        </tr>
+        <tr>
+         <td>isbaddirectory</td><td>${isbaddirectory}</td>
+        </tr>
+        <tr>
+         <td>isexit</td><td>${isexit}</td>
+        </tr>
+        <tr>
+         <td>isfast</td><td>${isfast}</td>
+        </tr>
+        <tr>
+         <td>isguard</td><td>${isguard}</td>
+        </tr>
+        <tr>
+         <td>ishsdir</td><td>${ishsdir}</td>
+        </tr>
+        <tr>
+         <td>isnamed</td><td>${isnamed}</td>
+        </tr>
+        <tr>
+         <td>isstable</td><td>${isstable}</td>
+        </tr>
+        <tr>
+         <td>isrunning</td><td>${isrunning}</td>
+        </tr>
+        <tr>
+         <td>isunnamed</td><td>${isunnamed}</td>
+        </tr>
+        <tr>
+         <td>isvalid</td><td>${isvalid}</td>
+        </tr>
+        <tr>
+         <td>isv2dir</td><td>${isv2dir}</td>
+        </tr>
+        <tr>
+         <td>isv3dir</td><td>${isv3dir}</td>
+        </tr>
+        <tr>
+         <td>version</td><td>${version}</td>
+        </tr>
+        <tr>
+         <td>ports</td><td>${ports}</td>
+        </tr>
+        <tr>
+         <td>platform</td><td>${platform}</td>
+        </tr>
+        <tr>
+         <td>onion-key</td><td>${onion_key}</td>
+        </tr>
+        <tr>
+         <td>signing-key</td><td>${signing_key}</td>
+        </tr>
+      </table>
+      <img src="routerdetail.png?fingerprint=${fingerprint}"
+           width="576"
+           height="360"
+           alt="Router detail bandwidth graph for ${fingerprint}"/>
+    </div>
+  </div>
+  <div class="bottom" id="bottom">
+    <%@ include file="footer.jsp"%>
+  </div>
+</body>
+</html>
-- 
1.7.1




More information about the tor-commits mailing list