[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