[tor-commits] [metrics-web/master] Fetch CollecTor's index.json for our own directory listings.

karsten at torproject.org karsten at torproject.org
Tue Sep 19 07:34:10 UTC 2017


commit 05f8a74fb1d3b1c074f106c1d2045092545288d2
Author: Karsten Loesing <karsten.loesing at gmx.net>
Date:   Sun Aug 20 20:19:34 2017 +0200

    Fetch CollecTor's index.json for our own directory listings.
    
    Implements #22836.
---
 website/build.xml                                  |   6 ++
 .../torproject/metrics/web/CollecTorServlet.java   |  54 +++++++++--
 .../metrics/web/CollectorDirectoryProvider.java    |  79 +++++++++++++++
 .../torproject/metrics/web/DirectoryListing.java   |  90 +++++++++++++++++
 website/src/main/resources/etc/web.xml             |   1 +
 website/src/main/resources/web/WEB-INF/bottom.jsp  |   2 +-
 .../main/resources/web/WEB-INF/collector-files.jsp |  40 ++++++++
 .../src/main/resources/web/WEB-INF/collector.jsp   | 108 ++++++++++-----------
 website/src/main/resources/web/WEB-INF/top.jsp     |  52 +++++-----
 .../metrics/web/DirectoryListingTest.java          |  81 ++++++++++++++++
 10 files changed, 425 insertions(+), 88 deletions(-)

diff --git a/website/build.xml b/website/build.xml
index dfa0b18..07bf84e 100644
--- a/website/build.xml
+++ b/website/build.xml
@@ -1,10 +1,13 @@
 <project default="war" name="metrics-web" basedir=".">
 
+  <property name="metricslibversion" value="2.0.0"/>
+
   <property name="libs" value="../shared/lib"/>
 
   <include file="../shared/build-base.xml" as="basetask"/>
   <target name="clean" depends="basetask.clean"/>
   <target name="compile" depends="basetask.compile"/>
+  <target name="test" depends="basetask.test"/>
 
   <patternset id="compile.libs" >
     <include name="postgresql-jdbc3-9.2.jar"/>
@@ -19,6 +22,9 @@
     <include name="commons-codec-1.10.jar"/>
     <include name="commons-lang-2.6.jar"/>
     <include name="gson-2.4.jar"/>
+    <include name="metrics-lib-${metricslibversion}.jar"/>
+    <include name="commons-compress-1.13.jar"/>
+    <include name="slf4j-api-1.7.22.jar"/>
   </patternset>
 
   <path id="classpath">
diff --git a/website/src/main/java/org/torproject/metrics/web/CollecTorServlet.java b/website/src/main/java/org/torproject/metrics/web/CollecTorServlet.java
index 74ca163..2b0a13a 100644
--- a/website/src/main/java/org/torproject/metrics/web/CollecTorServlet.java
+++ b/website/src/main/java/org/torproject/metrics/web/CollecTorServlet.java
@@ -4,28 +4,68 @@
 package org.torproject.metrics.web;
 
 import java.io.IOException;
+import java.util.List;
+import java.util.Map;
 
+import javax.servlet.ServletConfig;
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+/** Servlet for CollecTor's "home" page and for CollecTor's directory listings
+ * based on the periodically fetched index.json file. */
 public class CollecTorServlet extends AnyServlet {
 
   private static final long serialVersionUID = -7054057945737357463L;
 
+  /** Host name of the CollecTor host with trailing slash omitted. */
+  private static final String COLLECTOR_HOST
+      = "https://collector.torproject.org";
+
+  /** Parsed and formatted directory listings. */
+  private CollectorDirectoryProvider collectorDirectory;
+
+  /** Initializes this servlet by retrieving the CollecTor host name from the
+   * configuration file and starting the periodic index.json downloader. */
   @Override
-  public void init() throws ServletException {
-    super.init();
+  public void init(ServletConfig config) throws ServletException {
+    super.init(config);
+    this.collectorDirectory = new CollectorDirectoryProvider(COLLECTOR_HOST);
   }
 
+  /** Handles requests for either CollecTor's "home" page or for directory
+   * listings. */
   @Override
   public void doGet(HttpServletRequest request,
       HttpServletResponse response) throws IOException, ServletException {
-
-    /* Forward the request to the JSP that does all the hard work. */
-    request.setAttribute("categories", this.categories);
-    request.getRequestDispatcher("WEB-INF/collector.jsp").forward(request,
-        response);
+    String requestedPath = request.getRequestURI();
+    if (requestedPath.contains("/collector")) {
+      /* Possibly truncate any path prefix (like "/metrics") added by deploying
+       * this webapp as metrics.war rather than, say, ROOT.war. */
+      requestedPath = requestedPath.substring(requestedPath.indexOf(
+          "/collector"));
+    }
+    Map<String, List<String[]>> index;
+    if ("/collector.html".equals(requestedPath)) {
+      request.setAttribute("categories", this.categories);
+      request.getRequestDispatcher("WEB-INF/collector.jsp").forward(request,
+          response);
+    } else if (null == (index = this.collectorDirectory.getIndex())) {
+      response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+          "Index of CollecTor files unavailable.");
+    } else if (!requestedPath.endsWith("/")
+        && index.containsKey(requestedPath + "/")) {
+      response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
+      response.setHeader("Location", requestedPath + "/");
+    } else if (index.containsKey(requestedPath)) {
+      request.setAttribute("categories", this.categories);
+      request.setAttribute("files", index.get(requestedPath));
+      request.getRequestDispatcher("/WEB-INF/collector-files.jsp").forward(
+          request, response);
+    } else {
+      response.sendError(HttpServletResponse.SC_NOT_FOUND,
+          "Unknown directory: " + requestedPath);
+    }
   }
 }
 
diff --git a/website/src/main/java/org/torproject/metrics/web/CollectorDirectoryProvider.java b/website/src/main/java/org/torproject/metrics/web/CollectorDirectoryProvider.java
new file mode 100644
index 0000000..9cd1e1e
--- /dev/null
+++ b/website/src/main/java/org/torproject/metrics/web/CollectorDirectoryProvider.java
@@ -0,0 +1,79 @@
+/* Copyright 2017 The Tor Project
+ * See LICENSE for licensing information */
+
+package org.torproject.metrics.web;
+
+import org.torproject.descriptor.index.DirectoryNode;
+import org.torproject.descriptor.index.FileNode;
+import org.torproject.descriptor.index.IndexNode;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/** Periodically fetches a remote index.json file and provides formatted
+ * directory listings for all contained directories and subdirectories. */
+public class CollectorDirectoryProvider implements Runnable {
+
+  /** Host name of the host serving the remote index.json with trailing slash
+   * omitted. */
+  private String host;
+
+  /** Scheduler for periodically downloading the remote index.json file. */
+  private final ScheduledExecutorService scheduler =
+      Executors.newScheduledThreadPool(1);
+
+  /** Last known directory listings. */
+  private final AtomicReference<Map<String, List<String[]>>> index
+      = new AtomicReference<>(null);
+
+  CollectorDirectoryProvider(String host) {
+    this.host = host;
+    this.scheduler.scheduleAtFixedRate(this, 0, 1, TimeUnit.MINUTES);
+  }
+
+  /** Returns the index object in a thread-safe way, blocking the invoking
+   * thread at most 10 seconds if no index object is available. */
+  Map<String, List<String[]>> getIndex() {
+    if (null == this.index.get()) {
+      long waitingSinceMillis = System.currentTimeMillis();
+      do {
+        try {
+          this.wait(200L);
+        } catch (InterruptedException e) {
+          /* Ignore. */
+        }
+      } while (null == index.get()
+          && System.currentTimeMillis() < waitingSinceMillis + 10000L);
+    }
+    return this.index.get();
+  }
+
+  /** Fetch the remote index.json and extract all we need to know to later
+   * produce directory listings as requested. */
+  @Override
+  public void run() {
+    IndexNode indexNode;
+    try {
+      indexNode = IndexNode.fetchIndex(this.host + "/index/index.json.gz");
+    } catch (Exception e) {
+      /* If we failed to fetch the remote index.json this time, abort the
+       * update and don't override what we possibly fetched last time. If this
+       * is a temporary problem, one of the next runs will update the index. If
+       * it's a permanent problem, we'll at least serve the last known files.
+       * Unless it's a permanent problem right from when we started in which
+       * case there's nothing we can do other than return 500. */
+      return;
+    }
+    this.index.set(new DirectoryListing(indexNode));
+  }
+
+}
+
diff --git a/website/src/main/java/org/torproject/metrics/web/DirectoryListing.java b/website/src/main/java/org/torproject/metrics/web/DirectoryListing.java
new file mode 100644
index 0000000..6dd09e4
--- /dev/null
+++ b/website/src/main/java/org/torproject/metrics/web/DirectoryListing.java
@@ -0,0 +1,90 @@
+/* Copyright 2017 The Tor Project
+ * See LICENSE for licensing information */
+
+package org.torproject.metrics.web;
+
+import org.torproject.descriptor.index.DirectoryNode;
+import org.torproject.descriptor.index.FileNode;
+import org.torproject.descriptor.index.IndexNode;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+/** Map with all directory listings for all directories and subdirectories
+ * contained in an index.json file. */
+public class DirectoryListing extends HashMap<String, List<String[]>>
+    implements Map<String, List<String[]>> {
+
+  private IndexNode index;
+
+  DirectoryListing(IndexNode index) {
+    this.index = index;
+    extractDirectoryListings();
+  }
+
+  /** Extracts directory listing from an index node by visiting all nodes. */
+  private void extractDirectoryListings() {
+    Map<DirectoryNode, String> directoryNodes = new HashMap<>();
+    this.put("/collector/",
+        formatTableEntries("", "/", this.index.directories, this.index.files));
+    for (DirectoryNode directory : this.index.directories) {
+      directoryNodes.put(directory, "/");
+    }
+    while (!directoryNodes.isEmpty()) {
+      DirectoryNode currentDirectoryNode =
+          directoryNodes.keySet().iterator().next();
+      String parentPath = directoryNodes.remove(currentDirectoryNode);
+      if (null != currentDirectoryNode.directories) {
+        for (DirectoryNode subDirectoryNode
+            : currentDirectoryNode.directories) {
+          directoryNodes.put(subDirectoryNode, String.format("%s%s/",
+              parentPath, currentDirectoryNode.path));
+        }
+      }
+      this.put(String
+          .format("/collector%s%s/", parentPath, currentDirectoryNode.path),
+          formatTableEntries(parentPath, currentDirectoryNode.path + "/",
+          currentDirectoryNode.directories, currentDirectoryNode.files));
+    }
+  }
+
+  /** Formats table entries for a given directory. */
+  private List<String[]> formatTableEntries(String parentPath, String path,
+      SortedSet<DirectoryNode> directories, SortedSet<FileNode> files) {
+    List<String[]> tableEntries = new ArrayList<>();
+    tableEntries.add(new String[] { "Parent Directory",
+        String.format("/collector%s",
+        parentPath.isEmpty() ? ".html" : parentPath), "", "" });
+    if (null != directories) {
+      for (DirectoryNode subDirectoryNode : directories) {
+        tableEntries.add(new String[] { subDirectoryNode.path,
+            String.format("/collector%s%s%s/", parentPath, path,
+            subDirectoryNode.path), "", "" });
+      }
+    }
+    if (null != files) {
+      for (FileNode fileNode : new TreeSet<>(files).descendingSet()) {
+        tableEntries.add(new String[] { fileNode.path,
+            String.format("%s%s%s%s", this.index.path, parentPath,
+            path, fileNode.path), fileNode.lastModified,
+            formatBytes(fileNode.size) });
+      }
+    }
+    return tableEntries;
+  }
+
+  /** Formats a number of bytes to units B, KiB, MiB, etc. */
+  static String formatBytes(long bytes) {
+    if (bytes < 1024) {
+      return bytes + " B";
+    }
+    int exp = (int) (Math.log(bytes) / Math.log(1024));
+    char pre = "KMGTPE".charAt(exp - 1);
+    return String.format("%.1f %siB", bytes / Math.pow(1024, exp), pre);
+  }
+}
+
diff --git a/website/src/main/resources/etc/web.xml b/website/src/main/resources/etc/web.xml
index 38a30bb..830e8e7 100644
--- a/website/src/main/resources/etc/web.xml
+++ b/website/src/main/resources/etc/web.xml
@@ -303,6 +303,7 @@
   <servlet-mapping>
     <servlet-name>CollecTorServlet</servlet-name>
     <url-pattern>/collector.html</url-pattern>
+    <url-pattern>/collector/*</url-pattern>
   </servlet-mapping>
 
   <servlet>
diff --git a/website/src/main/resources/web/WEB-INF/bottom.jsp b/website/src/main/resources/web/WEB-INF/bottom.jsp
index 835b286..6719adc 100644
--- a/website/src/main/resources/web/WEB-INF/bottom.jsp
+++ b/website/src/main/resources/web/WEB-INF/bottom.jsp
@@ -9,7 +9,7 @@
       <div class="col-xs-6">
         <p class="pull-right">
 
-           <a href="about.html#contact">Contact</a>
+           <a href="/about.html#contact">Contact</a>
 
         </p>
       </div>
diff --git a/website/src/main/resources/web/WEB-INF/collector-files.jsp b/website/src/main/resources/web/WEB-INF/collector-files.jsp
new file mode 100644
index 0000000..da57144
--- /dev/null
+++ b/website/src/main/resources/web/WEB-INF/collector-files.jsp
@@ -0,0 +1,40 @@
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
+<jsp:include page="top.jsp">
+  <jsp:param name="pageTitle" value="Sources – Tor Metrics"/>
+  <jsp:param name="navActive" value="Sources"/>
+</jsp:include>
+
+    <div class="container">
+      <ul class="breadcrumb">
+        <li><a href="/">Home</a></li>
+        <li><a href="/sources.html">Sources</a></li>
+        <li><a href="/collector.html">CollecTor</a></li>
+      </ul>
+    </div>
+
+    <div class="container">
+      <div class="row">
+        <div class="col-xs-12">
+          <table class="table">
+            <thead>
+              <tr>
+                <th>Name</th>
+                <th>Last modified</th>
+                <th>Size</th>
+              </tr>
+            </thead>
+            <tbody>
+              <c:forEach var="file" items="${files}"><tr>
+                <td><a href="${file[1]}">${file[0]}</a></td>
+                <td>${file[2]}</td>
+                <td>${file[3]}</td>
+              </tr></c:forEach>
+            </tbody>
+          </table>
+        </div><!-- col -->
+      </div><!-- row -->
+    </div><!-- container -->
+
+<jsp:include page="bottom.jsp"/>
+
diff --git a/website/src/main/resources/web/WEB-INF/collector.jsp b/website/src/main/resources/web/WEB-INF/collector.jsp
index f57ccb8..871415c 100644
--- a/website/src/main/resources/web/WEB-INF/collector.jsp
+++ b/website/src/main/resources/web/WEB-INF/collector.jsp
@@ -32,8 +32,8 @@
               network, or if you're developing an application that uses
               Tor network data, this is your place to start.
               </p>
-<a class="btn btn-primary btn-lg" style="margin: 10px" href="https://collector.torproject.org/recent/" target="_blank"><i class="fa fa-chevron-right" aria-hidden="true"></i> Browse Recent Descriptors</a>
-<a class="btn btn-primary btn-lg" style="margin: 10px" href="https://collector.torproject.org/archive/" target="_blank"><i class="fa fa-chevron-right" aria-hidden="true"></i> Browse Archived Descriptors</a>
+<a class="btn btn-primary btn-lg" style="margin: 10px" href="/collector/recent/"><i class="fa fa-chevron-right" aria-hidden="true"></i> Browse Recent Descriptors</a>
+<a class="btn btn-primary btn-lg" style="margin: 10px" href="/collector/archive/"><i class="fa fa-chevron-right" aria-hidden="true"></i> Browse Archived Descriptors</a>
               </div><!-- text-center -->
 
 
@@ -65,53 +65,53 @@
 <tr>
   <td><a href="#type-server-descriptor">Relay Server Descriptors</a></td>
   <td><code>@type server-descriptor 1.0</code></td>
-  <td><a href="https://collector.torproject.org/recent/relay-descriptors/server-descriptors/" target="_blank" class="btn btn-primary btn-xs pull-left"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
-      <a href="https://collector.torproject.org/archive/relay-descriptors/server-descriptors/" target="_blank" class="btn btn-primary btn-xs pull-right"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a></td>
+  <td><a href="/collector/recent/relay-descriptors/server-descriptors/" class="btn btn-primary btn-xs pull-left"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
+      <a href="/collector/archive/relay-descriptors/server-descriptors/" class="btn btn-primary btn-xs pull-right"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a></td>
 </tr>
 <tr>
   <td><a href="#type-extra-info">Relay Extra-info Descriptors</a></td>
   <td><code>@type extra-info 1.0</code></td>
-  <td><a href="https://collector.torproject.org/recent/relay-descriptors/extra-infos/" target="_blank" class="btn btn-primary btn-xs pull-left"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
-      <a href="https://collector.torproject.org/archive/relay-descriptors/extra-infos/" target="_blank" class="btn btn-primary btn-xs pull-right"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a></td>
+  <td><a href="/collector/recent/relay-descriptors/extra-infos/" class="btn btn-primary btn-xs pull-left"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
+      <a href="/collector/archive/relay-descriptors/extra-infos/" class="btn btn-primary btn-xs pull-right"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a></td>
 </tr>
 <tr>
   <td><a href="#type-network-status-consensus-3">Network Status Consensuses</a></td>
   <td><code>@type network-status-consensus-3 1.0</code></td>
-  <td><a href="https://collector.torproject.org/recent/relay-descriptors/consensuses/" target="_blank" class="btn btn-primary btn-xs pull-left"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
-      <a href="https://collector.torproject.org/archive/relay-descriptors/consensuses/" target="_blank" class="btn btn-primary btn-xs pull-right"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a></td>
+  <td><a href="/collector/recent/relay-descriptors/consensuses/" class="btn btn-primary btn-xs pull-left"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
+      <a href="/collector/archive/relay-descriptors/consensuses/" class="btn btn-primary btn-xs pull-right"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a></td>
 </tr>
 <tr>
   <td><a href="#type-network-status-vote-3">Network Status Votes</a></td>
   <td><code>@type network-status-vote-3 1.0</code></td>
-  <td><a href="https://collector.torproject.org/recent/relay-descriptors/votes/" target="_blank" class="btn btn-primary btn-xs pull-left"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
-      <a href="https://collector.torproject.org/archive/relay-descriptors/votes/" target="_blank" class="btn btn-primary btn-xs pull-right"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a></td>
+  <td><a href="/collector/recent/relay-descriptors/votes/" class="btn btn-primary btn-xs pull-left"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
+      <a href="/collector/archive/relay-descriptors/votes/" class="btn btn-primary btn-xs pull-right"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a></td>
 </tr>
 <tr>
   <td><a href="#type-dir-key-certificate-3">Directory Key Certificates</a></td>
   <td><code>@type dir-key-certificate-3 1.0</code></td>
-  <td><a href="https://collector.torproject.org/archive/relay-descriptors/" target="_blank" class="btn btn-primary btn-xs pull-right"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a></td>
+  <td><a href="/collector/archive/relay-descriptors/" class="btn btn-primary btn-xs pull-right"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a></td>
 </tr>
 <tr>
   <td><a href="#type-network-status-microdesc-consensus-3">Microdescriptor Consensuses</a></td>
   <td><code>@type network-status-microdesc-consensus-3 1.0</code></td>
-  <td><a href="https://collector.torproject.org/recent/relay-descriptors/microdescs/consensus-microdesc/" target="_blank" class="btn btn-primary btn-xs pull-left"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
-      <a href="https://collector.torproject.org/archive/relay-descriptors/microdescs/" target="_blank" class="btn btn-primary btn-xs pull-right"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a></td>
+  <td><a href="/collector/recent/relay-descriptors/microdescs/consensus-microdesc/" class="btn btn-primary btn-xs pull-left"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
+      <a href="/collector/archive/relay-descriptors/microdescs/" class="btn btn-primary btn-xs pull-right"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a></td>
 </tr>
 <tr>
   <td><a href="#type-microdescriptor">Microdescriptors</a></td>
   <td><code>@type microdescriptor 1.0</code></td>
-  <td><a href="https://collector.torproject.org/recent/relay-descriptors/microdescs/micro/" target="_blank" class="btn btn-primary btn-xs pull-left"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
-      <a href="https://collector.torproject.org/archive/relay-descriptors/microdescs/" target="_blank" class="btn btn-primary btn-xs pull-right"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a></td>
+  <td><a href="/collector/recent/relay-descriptors/microdescs/micro/" class="btn btn-primary btn-xs pull-left"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
+      <a href="/collector/archive/relay-descriptors/microdescs/" class="btn btn-primary btn-xs pull-right"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a></td>
 </tr>
 <tr>
   <td><a href="#type-network-status-2">Version 2 Network Statuses</a></td>
   <td><code>@type network-status-2 1.0</code></td>
-  <td><a href="https://collector.torproject.org/archive/relay-descriptors/statuses/" target="_blank" class="btn btn-primary btn-xs pull-right"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a></td>
+  <td><a href="/collector/archive/relay-descriptors/statuses/" class="btn btn-primary btn-xs pull-right"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a></td>
 </tr>
 <tr>
   <td><a href="#type-directory">Version 1 Directories</a></td>
   <td><code>@type directory 1.0</code></td>
-  <td><a href="https://collector.torproject.org/archive/relay-descriptors/tor/" target="_blank" class="btn btn-primary btn-xs pull-right"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a></td>
+  <td><a href="/collector/archive/relay-descriptors/tor/" class="btn btn-primary btn-xs pull-right"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a></td>
 </tr>
 <tr class="tableHeadline">
   <td colspan="3"><b><a href="#bridge-descriptors">Tor Bridge Descriptors</a></b></td>
@@ -119,20 +119,20 @@
 <tr>
   <td><a href="#type-bridge-network-status">Bridge Network Statuses</a></td>
   <td><code>@type bridge-network-status 1.2</code></td>
-  <td><a href="https://collector.torproject.org/recent/bridge-descriptors/statuses/" target="_blank" class="btn btn-primary btn-xs pull-left"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
-      <a href="https://collector.torproject.org/archive/bridge-descriptors/statuses/" target="_blank" class="btn btn-primary btn-xs pull-right"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a></td>
+  <td><a href="/collector/recent/bridge-descriptors/statuses/" class="btn btn-primary btn-xs pull-left"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
+      <a href="/collector/archive/bridge-descriptors/statuses/" class="btn btn-primary btn-xs pull-right"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a></td>
 </tr>
 <tr>
   <td><a href="#type-bridge-server-descriptor">Bridge Server Descriptors</a></td>
   <td><code>@type bridge-server-descriptor 1.2</code></td>
-  <td><a href="https://collector.torproject.org/recent/bridge-descriptors/server-descriptors/" target="_blank" class="btn btn-primary btn-xs pull-left"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
-      <a href="https://collector.torproject.org/archive/bridge-descriptors/server-descriptors/" target="_blank" class="btn btn-primary btn-xs pull-right"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a></td>
+  <td><a href="/collector/recent/bridge-descriptors/server-descriptors/" class="btn btn-primary btn-xs pull-left"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
+      <a href="/collector/archive/bridge-descriptors/server-descriptors/" class="btn btn-primary btn-xs pull-right"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a></td>
 </tr>
 <tr>
   <td><a href="#type-bridge-extra-info">Bridge Extra-info Descriptors</a></td>
   <td><code>@type bridge-extra-info 1.3</code></td>
-  <td><a href="https://collector.torproject.org/recent/bridge-descriptors/extra-infos/" target="_blank" class="btn btn-primary btn-xs pull-left"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
-      <a href="https://collector.torproject.org/archive/bridge-descriptors/extra-infos/" target="_blank" class="btn btn-primary btn-xs pull-right"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a></td>
+  <td><a href="/collector/recent/bridge-descriptors/extra-infos/" class="btn btn-primary btn-xs pull-left"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
+      <a href="/collector/archive/bridge-descriptors/extra-infos/" class="btn btn-primary btn-xs pull-right"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a></td>
 </tr>
 <tr class="tableHeadline">
   <td colspan="3"><b><a href="#tor-hidden-service-descriptors">Tor Hidden Service Descriptors</a></b></td>
@@ -148,7 +148,7 @@
 <tr>
   <td><a href="#type-bridge-pool-assignment">Bridge Pool Assignments</a></td>
   <td><code>@type bridge-pool-assignment 1.0</code></td>
-  <td><a href="https://collector.torproject.org/archive/bridge-pool-assignments/" target="_blank" class="btn btn-primary btn-xs pull-right"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a></td>
+  <td><a href="/collector/archive/bridge-pool-assignments/" class="btn btn-primary btn-xs pull-right"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a></td>
 </tr>
 <tr class="tableHeadline">
   <td colspan="3"><b><a href="#exit-lists">TorDNSEL's Exit Lists</a></b></td>
@@ -156,8 +156,8 @@
 <tr>
   <td><a href="#type-tordnsel">Exit Lists</a></td>
   <td><code>@type tordnsel 1.0</code></td>
-  <td><a href="https://collector.torproject.org/recent/exit-lists/" target="_blank" class="btn btn-primary btn-xs pull-left"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
-      <a href="https://collector.torproject.org/archive/exit-lists/" target="_blank" class="btn btn-primary btn-xs pull-right"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a></td>
+  <td><a href="/collector/recent/exit-lists/" class="btn btn-primary btn-xs pull-left"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
+      <a href="/collector/archive/exit-lists/" class="btn btn-primary btn-xs pull-right"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a></td>
 </tr>
 <tr class="tableHeadline">
   <td colspan="3"><b><a href="#torperf">Torperf's and OnionPerf's Performance Data</a></b></td>
@@ -165,8 +165,8 @@
 <tr>
   <td><a href="#type-torperf">Torperf Measurement Results</a></td>
   <td><code>@type torperf 1.1</code></td>
-  <td><a href="https://collector.torproject.org/recent/torperf/" target="_blank" class="btn btn-primary btn-xs pull-left"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
-      <a href="https://collector.torproject.org/archive/torperf/" target="_blank" class="btn btn-primary btn-xs pull-right"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a></td>
+  <td><a href="/collector/recent/torperf/" class="btn btn-primary btn-xs pull-left"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
+      <a href="/collector/archive/torperf/" class="btn btn-primary btn-xs pull-right"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a></td>
 </tr>
 </tbody>
 </table>
@@ -213,8 +213,8 @@ earlier protocol
 
 <h3 id="type-server-descriptor" class="hover">Relay Server Descriptors
 <small><code>@type server-descriptor 1.0</code></small>
-<a href="https://collector.torproject.org/recent/relay-descriptors/server-descriptors/" target="_blank" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
-<a href="https://collector.torproject.org/archive/relay-descriptors/server-descriptors/" target="_blank" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a>
+<a href="/collector/recent/relay-descriptors/server-descriptors/" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
+<a href="/collector/archive/relay-descriptors/server-descriptors/" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a>
 <a href="#type-server-descriptor" class="anchor">#</a>
 </h3>
 
@@ -231,8 +231,8 @@ file.
 
 <h3 id="type-extra-info" class="hover">Relay Extra-info Descriptors
 <small><code>@type extra-info 1.0</code></small>
-<a href="https://collector.torproject.org/recent/relay-descriptors/extra-infos/" target="_blank" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
-<a href="https://collector.torproject.org/archive/relay-descriptors/extra-infos/" target="_blank" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a>
+<a href="/collector/recent/relay-descriptors/extra-infos/" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
+<a href="/collector/archive/relay-descriptors/extra-infos/" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a>
 <a href="#type-extra-info" class="anchor">#</a>
 </h3>
 
@@ -249,8 +249,8 @@ file.
 
 <h3 id="type-network-status-consensus-3" class="hover">Network Status Consensuses
 <small><code>@type network-status-consensus-3 1.0</code></small>
-<a href="https://collector.torproject.org/recent/relay-descriptors/consensuses/" target="_blank" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
-<a href="https://collector.torproject.org/archive/relay-descriptors/consensuses/" target="_blank" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a>
+<a href="/collector/recent/relay-descriptors/consensuses/" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
+<a href="/collector/archive/relay-descriptors/consensuses/" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a>
 <a href="#type-network-status-consensus-3" class="anchor">#</a>
 </h3>
 
@@ -265,8 +265,8 @@ flags, heuristics used for relay selection, etc.
 
 <h3 id="type-network-status-vote-3" class="hover">Network Status Votes
 <small><code>@type network-status-vote-3 1.0</code></small>
-<a href="https://collector.torproject.org/recent/relay-descriptors/votes/" target="_blank" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
-<a href="https://collector.torproject.org/archive/relay-descriptors/votes/" target="_blank" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a>
+<a href="/collector/recent/relay-descriptors/votes/" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
+<a href="/collector/archive/relay-descriptors/votes/" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a>
 <a href="#type-network-status-vote-3" class="anchor">#</a>
 </h3>
 
@@ -278,7 +278,7 @@ Vote documents are by far the largest documents provided here.
 
 <h3 id="type-dir-key-certificate-3" class="hover">Directory Key Certificates
 <small><code>@type dir-key-certificate-3 1.0</code></small>
-<a href="https://collector.torproject.org/archive/relay-descriptors/" target="_blank" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a>
+<a href="/collector/archive/relay-descriptors/" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a>
 <a href="#type-dir-key-certificate-3" class="anchor">#</a>
 </h3>
 
@@ -291,8 +291,8 @@ available in a single descriptor archive tarball.
 
 <h3 id="type-network-status-microdesc-consensus-3" class="hover">Microdescriptor Consensuses
 <small><code>@type network-status-microdesc-consensus-3 1.0</code></small>
-<a href="https://collector.torproject.org/recent/relay-descriptors/microdescs/" target="_blank" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
-<a href="https://collector.torproject.org/archive/relay-descriptors/microdescs/" target="_blank" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a>
+<a href="/collector/recent/relay-descriptors/microdescs/" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
+<a href="/collector/archive/relay-descriptors/microdescs/" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a>
 <a href="#type-network-status-microdesc-consensus-3" class="anchor">#</a>
 </h3>
 
@@ -309,8 +309,8 @@ together.
 
 <h3 id="type-microdescriptor" class="hover">Microdescriptors
 <small><code>@type microdescriptor 1.0</code></small>
-<a href="https://collector.torproject.org/recent/relay-descriptors/microdescs/" target="_blank" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
-<a href="https://collector.torproject.org/archive/relay-descriptors/microdescs/" target="_blank" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a>
+<a href="/collector/recent/relay-descriptors/microdescs/" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
+<a href="/collector/archive/relay-descriptors/microdescs/" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a>
 <a href="#type-microdescriptor" class="anchor">#</a>
 </h3>
 
@@ -328,7 +328,7 @@ file.
 
 <h3 id="type-network-status-2" class="hover">Version 2 Network Statuses
 <small><code>@type network-status-2 1.0</code></small>
-<a href="https://collector.torproject.org/archive/relay-descriptors/statuses/" target="_blank" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a>
+<a href="/collector/archive/relay-descriptors/statuses/" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a>
 <a href="#type-network-status-2" class="anchor">#</a>
 </h3>
 
@@ -343,7 +343,7 @@ We stopped archiving version 2 network statuses in 2012.
 
 <h3 id="type-directory" class="hover">Version 1 Directories
 <small><code>@type directory 1.0</code></small>
-<a href="https://collector.torproject.org/archive/relay-descriptors/tor/" target="_blank" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a>
+<a href="/collector/archive/relay-descriptors/tor/" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a>
 <a href="#type-directory" class="anchor">#</a>
 </h3>
 
@@ -372,8 +372,8 @@ The sanitizing steps are specified in detail on a separate
 
 <h3 id="type-bridge-network-status" class="hover">Bridge Network Statuses
 <small><code>@type bridge-network-status 1.2</code></small>
-<a href="https://collector.torproject.org/recent/bridge-descriptors/statuses/" target="_blank" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
-<a href="https://collector.torproject.org/archive/bridge-descriptors/" target="_blank" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a>
+<a href="/collector/recent/bridge-descriptors/statuses/" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
+<a href="/collector/archive/bridge-descriptors/" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a>
 <a href="#type-bridge-network-status" class="anchor">#</a>
 </h3>
 
@@ -397,8 +397,8 @@ authority which produced the document, to the header.</li>
 
 <h3 id="type-bridge-server-descriptor" class="hover">Bridge Server descriptors
 <small><code>@type bridge-server-descriptor 1.2</code></small>
-<a href="https://collector.torproject.org/recent/bridge-descriptors/server-descriptors/" target="_blank" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
-<a href="https://collector.torproject.org/archive/bridge-descriptors/" target="_blank" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a>
+<a href="/collector/recent/bridge-descriptors/server-descriptors/" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
+<a href="/collector/archive/bridge-descriptors/" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a>
 <a href="#type-bridge-server-descriptor" class="anchor">#</a>
 </h3>
 
@@ -431,8 +431,8 @@ ports.</li>
 
 <h3 id="type-bridge-extra-info" class="hover">Bridge Extra-info Descriptors
 <small><code>@type bridge-extra-info 1.3</code></small>
-<a href="https://collector.torproject.org/recent/bridge-descriptors/extra-infos/" target="_blank" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
-<a href="https://collector.torproject.org/archive/bridge-descriptors/" target="_blank" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a>
+<a href="/collector/recent/bridge-descriptors/extra-infos/" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
+<a href="/collector/archive/bridge-descriptors/" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a>
 <a href="#type-bridge-extra-info" class="anchor">#</a>
 </h3>
 
@@ -514,7 +514,7 @@ statistical analysis.
 
 <h3 id="type-bridge-pool-assignment" class="hover">Bridge Pool Assignments
 <small><code>@type bridge-pool-assignment 1.0</code></small>
-<a href="https://collector.torproject.org/archive/bridge-pool-assignments/" target="_blank" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a>
+<a href="/collector/archive/bridge-pool-assignments/" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a>
 <a href="#type-bridge-pool-assignment" class="anchor">#</a>
 </h3>
 
@@ -573,8 +573,8 @@ when exiting through them.
 
 <h3 id="type-tordnsel" class="hover">Exit Lists
 <small><code>@type tordnsel 1.0</code></small>
-<a href="https://collector.torproject.org/recent/exit-lists/" target="_blank" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
-<a href="https://collector.torproject.org/archive/exit-lists/" target="_blank" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a>
+<a href="/collector/recent/exit-lists/" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
+<a href="/collector/archive/exit-lists/" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a>
 <a href="#type-tordnsel" class="anchor">#</a>
 </h3>
 
@@ -617,8 +617,8 @@ over the Tor network and notes how long substeps take.
 
 <h3 id="type-torperf" class="hover">Torperf and OnionPerf Measurement Results
 <small><code>@type torperf 1.1</code></small>
-<a href="https://collector.torproject.org/recent/torperf/" target="_blank" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
-<a href="https://collector.torproject.org/archive/torperf/" target="_blank" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a>
+<a href="/collector/recent/torperf/" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> recent</a>
+<a href="/collector/archive/torperf/" class="btn btn-primary btn-xs"><i class="fa fa-chevron-right" aria-hidden="true"></i> archive</a>
 <a href="#type-torperf" class="anchor">#</a>
 </h3>
 
diff --git a/website/src/main/resources/web/WEB-INF/top.jsp b/website/src/main/resources/web/WEB-INF/top.jsp
index c013780..e5e95eb 100644
--- a/website/src/main/resources/web/WEB-INF/top.jsp
+++ b/website/src/main/resources/web/WEB-INF/top.jsp
@@ -7,7 +7,7 @@
   <title>${param.pageTitle}</title>
 
   <meta charset="utf-8">
-  <link href="images/favicon.ico" type="image/x-icon" rel="shortcut icon">
+  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
 
   <!-- yes, we are handheld friendly :) -->
   <meta name="HandheldFriendly" content="True">
@@ -15,31 +15,31 @@
   <meta name="apple-mobile-web-app-capable" content="yes">
 
   <!-- icons for mobile devices -->
-  <link rel="apple-touch-icon" href="images/apple-touch-icon-152x152.png">
-  <link rel="shortcut icon" href="images/android-icon.png" sizes="196x196">
-  <meta name="msapplication-square70x70logo" content="images/smalltile.png">
-  <meta name="msapplication-square150x150logo" content="images/mediumtile.png">
-  <meta name="msapplication-wide310x150logo" content="images/widetile.png">
-  <meta name="msapplication-square310x310logo" content="images/largetile.png">
+  <link rel="apple-touch-icon" href="/images/apple-touch-icon-152x152.png">
+  <link rel="shortcut icon" href="/images/android-icon.png" sizes="196x196">
+  <meta name="msapplication-square70x70logo" content="/images/smalltile.png">
+  <meta name="msapplication-square150x150logo" content="/images/mediumtile.png">
+  <meta name="msapplication-wide310x150logo" content="/images/widetile.png">
+  <meta name="msapplication-square310x310logo" content="/images/largetile.png">
 
   <!-- jQuery -->
-  <script src="js/jquery-3.2.1.min.js"></script>
+  <script src="/js/jquery-3.2.1.min.js"></script>
 
   <!-- Bootstrap -->
-  <link rel="stylesheet" href="css/bootstrap.min.css">
-  <script src="js/bootstrap.min.js"></script>
+  <link rel="stylesheet" href="/css/bootstrap.min.css">
+  <script src="/js/bootstrap.min.js"></script>
 
   <!-- Fonts -->
-  <link rel="stylesheet" href="css/font-awesome.min.css">
-  <link rel="stylesheet" href="fonts/source-sans-pro.css">
+  <link rel="stylesheet" href="/css/font-awesome.min.css">
+  <link rel="stylesheet" href="/fonts/source-sans-pro.css">
 
   <!-- Prism -->
-  <link rel="stylesheet" href="css/prism.css">
-  <script src="js/prism.js"></script>
+  <link rel="stylesheet" href="/css/prism.css">
+  <script src="/js/prism.js"></script>
 
   <!-- custom styles and javascript -->
-  <link rel="stylesheet" href="css/style.css">
-  <script src="js/script.js"></script>
+  <link rel="stylesheet" href="/css/style.css">
+  <script src="/js/script.js"></script>
 
 </head>
 
@@ -69,7 +69,7 @@ document.write('<div class="topButton" style="display:none;"><a href="#top"><i c
         <span class="icon-bar"></span>
         <span class="icon-bar"></span>
       </label>
-      <a class="navbar-brand visible-xs" href="/"><img src="images/tor-metrics-white.png" width="232" height="50" alt="Tor Metrics"></a>
+      <a class="navbar-brand visible-xs" href="/"><img src="/images/tor-metrics-white.png" width="232" height="50" alt="Tor Metrics"></a>
     </div>
 
     <!-- Collect the nav links, forms, and other content for toggling -->
@@ -80,18 +80,18 @@ document.write('<div class="topButton" style="display:none;"><a href="#top"><i c
         <li class="visible-xs section-header">Metrics</li>
         <li class="visible-xs<c:if test="${'Home'.equals(param.navActive)}"> active</c:if>"><a href="/"><i class="fa fa-home fa-fw" aria-hidden="true"></i> Home</a></li>
         <c:forEach var="category" items="${categories}">
-        <li class="visible-xs<c:if test="${category[1].equals(param.navActive)}"> active</c:if><c:if test="${fn:length(category[0]) == 0}"> disabled</c:if>"><a<c:if test="${fn:length(category[0]) > 0}"> href="${category[0]}.html"</c:if>><i class="fa ${category[3]} fa-fw" aria-hidden="true"></i> ${category[1]}</a></li>
+        <li class="visible-xs<c:if test="${category[1].equals(param.navActive)}"> active</c:if><c:if test="${fn:length(category[0]) == 0}"> disabled</c:if>"><a<c:if test="${fn:length(category[0]) > 0}"> href="/${category[0]}.html"</c:if>><i class="fa ${category[3]} fa-fw" aria-hidden="true"></i> ${category[1]}</a></li>
         </c:forEach>
         <!-- /end of primary copy -->
 
         <!-- secondary navigation items -->
         <li class="visible-xs section-header">More</li>
-        <li <c:if test="${'News'.equals(param.navActive)}"> class="active"</c:if>><a href="news.html"><i class="fa fa-newspaper-o fa-fw hidden-sm" aria-hidden="true"></i> News</a></li>
-        <li <c:if test="${'Sources'.equals(param.navActive)}"> class="active"</c:if>><a href="sources.html"><i class="fa fa-archive fa-fw hidden-sm" aria-hidden="true"></i> Sources</a></li>
-        <li <c:if test="${'Operation'.equals(param.navActive)}"> class="active"</c:if>><a href="operation.html"><i class="fa fa-cogs fa-fw hidden-sm" aria-hidden="true"></i> Operation</a></li>
-        <li <c:if test="${'Development'.equals(param.navActive)}"> class="active"</c:if>><a href="development.html"><i class="fa fa-code fa-fw hidden-sm" aria-hidden="true"></i> Development</a></li>
-        <li <c:if test="${'Research'.equals(param.navActive)}"> class="active"</c:if>><a href="research.html"><i class="fa fa-university fa-fw hidden-sm" aria-hidden="true"></i> Research</a></li>
-        <li <c:if test="${'About'.equals(param.navActive)}"> class="active"</c:if>><a href="about.html"><i class="fa fa-lightbulb-o fa-fw hidden-sm" aria-hidden="true"></i> About</a></li>
+        <li <c:if test="${'News'.equals(param.navActive)}"> class="active"</c:if>><a href="/news.html"><i class="fa fa-newspaper-o fa-fw hidden-sm" aria-hidden="true"></i> News</a></li>
+        <li <c:if test="${'Sources'.equals(param.navActive)}"> class="active"</c:if>><a href="/sources.html"><i class="fa fa-archive fa-fw hidden-sm" aria-hidden="true"></i> Sources</a></li>
+        <li <c:if test="${'Operation'.equals(param.navActive)}"> class="active"</c:if>><a href="/operation.html"><i class="fa fa-cogs fa-fw hidden-sm" aria-hidden="true"></i> Operation</a></li>
+        <li <c:if test="${'Development'.equals(param.navActive)}"> class="active"</c:if>><a href="/development.html"><i class="fa fa-code fa-fw hidden-sm" aria-hidden="true"></i> Development</a></li>
+        <li <c:if test="${'Research'.equals(param.navActive)}"> class="active"</c:if>><a href="/research.html"><i class="fa fa-university fa-fw hidden-sm" aria-hidden="true"></i> Research</a></li>
+        <li <c:if test="${'About'.equals(param.navActive)}"> class="active"</c:if>><a href="/about.html"><i class="fa fa-lightbulb-o fa-fw hidden-sm" aria-hidden="true"></i> About</a></li>
         <!-- /secondary navigation items -->
 
       </ul>
@@ -102,7 +102,7 @@ document.write('<div class="topButton" style="display:none;"><a href="#top"><i c
 
 <!-- page header for every single page -->
 <div class="page-header hidden-xs">
-  <a href="/"><img src="images/tor-metrics-white at 2x.png" width="232" height="50" alt="Tor Metrics" id="metrics-wordmark"></a>
+  <a href="/"><img src="/images/tor-metrics-white at 2x.png" width="232" height="50" alt="Tor Metrics" id="metrics-wordmark"></a>
   <div>
     <p>
       <i>“Tor metrics are the ammunition that lets Tor and other security advocates argue for a more private and secure Internet from a position of data, rather than just dogma or perspective.”<br><small>— Bruce Schneier (June 1, 2016)</small></i>
@@ -119,7 +119,7 @@ document.write('<div class="topButton" style="display:none;"><a href="#top"><i c
       <ul class="nav navbar-nav">
         <li <c:if test="${'Home'.equals(param.navActive)}"> class="active"</c:if>><a href="/"><i class="fa fa-home fa-fw hidden-sm" aria-hidden="true"></i> Home</a></li>
         <c:forEach var="category" items="${categories}">
-        <li class="<c:if test="${category[1].equals(param.navActive)}"> active</c:if><c:if test="${fn:length(category[0]) == 0}"> disabled</c:if>"><a<c:if test="${fn:length(category[0]) > 0}"> href="${category[0]}.html"</c:if>><i class="fa ${category[3]} fa-fw hidden-sm" aria-hidden="true"></i> ${category[1]}</a></li>
+        <li class="<c:if test="${category[1].equals(param.navActive)}"> active</c:if><c:if test="${fn:length(category[0]) == 0}"> disabled</c:if>"><a<c:if test="${fn:length(category[0]) > 0}"> href="/${category[0]}.html"</c:if>><i class="fa ${category[3]} fa-fw hidden-sm" aria-hidden="true"></i> ${category[1]}</a></li>
         </c:forEach>
       </ul>
     </div><!-- /.navbar-collapse -->
diff --git a/website/src/test/java/org/torproject/metrics/web/DirectoryListingTest.java b/website/src/test/java/org/torproject/metrics/web/DirectoryListingTest.java
new file mode 100644
index 0000000..5c6b37a
--- /dev/null
+++ b/website/src/test/java/org/torproject/metrics/web/DirectoryListingTest.java
@@ -0,0 +1,81 @@
+/* Copyright 2017 The Tor Project
+ * See LICENSE for licensing information */
+
+package org.torproject.metrics.web;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.torproject.descriptor.index.IndexNode;
+
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+public class DirectoryListingTest {
+
+  @Test
+  public void testFormatBytes() {
+    long[] input = new long[] { -1024L, -1L, 0L, 1L, 1023L, // B
+        1024L, 1025L, 1048575L, // KiB
+        1048576L, 1048577L, 1073741823L, // MiB
+        1073741824L, 1073741825L, 1099511627775L, // GiB
+        32099511627776L, 1099511627777L, 1125899906842623L, // TiB
+        1125899906842624L, 1125899906842625L }; // PiB
+    String[] expectedOutput = new String[] { "-1024 B", "-1 B", "0 B", "1 B",
+        "1023 B", "1.0 KiB", "1.0 KiB", "1024.0 KiB", "1.0 MiB", "1.0 MiB",
+        "1024.0 MiB", "1.0 GiB", "1.0 GiB", "1024.0 GiB", "29.2 TiB", "1.0 TiB",
+        "1.0 PiB", // <- Would have expected 1024.0 TiB, but who cares?
+        "1.0 PiB", "1.0 PiB" };
+    assertEquals(expectedOutput.length, input.length);
+    for (int i = 0; i < input.length; i++) {
+      assertEquals("Mismatch for input " + input[i], expectedOutput[i],
+          DirectoryListing.formatBytes(input[i]));
+    }
+  }
+
+  private static final String jsonIndex
+      = "{\"index_created\":\"2016-02-02 00:02\","
+      + "\"path\":\"https://some.collector.url\","
+      + "\"directories\":[{\"path\":\"a1\","
+      + "\"directories\":[{\"path\":\"p1\","
+      + "\"files\":[{\"path\":\"file1\",\"size\":624156,"
+      + "\"last_modified\":\"2012-01-01 13:13\"},{\"path\":\"file2\","
+      + "\"size\":1010648,"
+      + "\"last_modified\":\"2012-02-02 14:14\"}]},{\"path\":\"p2\","
+      + "\"files\":[{\"path\":\"file3\",\"size\":624156,"
+      + "\"last_modified\":\"2012-03-03 15:15\"}]}]}]}";
+
+  @Test
+  public void testListing() throws Exception {
+    DirectoryListing dl = new DirectoryListing(IndexNode
+        .fetchIndex(new ByteArrayInputStream(jsonIndex.getBytes())));
+    assertEquals(4, dl.size());
+    for (String key : new String[]{"/collector/a1/", "/collector/",
+        "/collector/a1/p2/", "/collector/a1/p1/"}) {
+      assertTrue("Missing: " + key, dl.keySet().contains(key));
+    }
+    assertEquals("[Parent Directory, /collector.html, , ]",
+        Arrays.toString(dl.get("/collector/").get(0)));
+    assertEquals(3, dl.get("/collector/a1/").size());
+    assertEquals("[Parent Directory, /collector/, , ]",
+        Arrays.toString(dl.get("/collector/a1/").get(0)));
+    assertEquals("[p1, /collector/a1/p1/, , ]",
+        Arrays.toString(dl.get("/collector/a1/").get(1)));
+    assertEquals("[p2, /collector/a1/p2/, , ]",
+        Arrays.toString(dl.get("/collector/a1/").get(2)));
+    assertEquals("[Parent Directory, /collector/a1/, , ]",
+        Arrays.toString(dl.get("/collector/a1/p1/").get(0)));
+    assertEquals(2, dl.get("/collector/a1/p2/").size());
+    assertEquals("[file3, https://some.collector.url/a1/p2/file3, "
+        + "2012-03-03 15:15, 609.5 KiB]",
+        Arrays.toString(dl.get("/collector/a1/p2/").get(1)));
+  }
+}
+





More information about the tor-commits mailing list