[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

[tor-commits] [metrics-web/master] Add metrics-lib page with tutorials.



commit 66a449fef4b3e85b71a096ad769de74d32cdc30a
Author: Karsten Loesing <karsten.loesing@xxxxxxx>
Date:   Mon Mar 13 20:19:41 2017 +0100

    Add metrics-lib page with tutorials.
    
    This commits contains lots of ideas from iwakeh and RaBe.
    
    Implements #21379.
---
 website/etc/web.xml                                |  11 +
 .../torproject/metrics/web/MetricsLibServlet.java  |  31 ++
 website/web/WEB-INF/development.jsp                |   2 +-
 website/web/WEB-INF/metrics-lib.jsp                | 375 +++++++++++++++++++++
 website/web/WEB-INF/top.jsp                        |   4 +
 website/web/css/prism.css                          | 179 ++++++++++
 website/web/css/style.css                          |  40 ++-
 website/web/js/prism.js                            |   6 +
 8 files changed, 646 insertions(+), 2 deletions(-)

diff --git a/website/etc/web.xml b/website/etc/web.xml
index bd7f4f1..63dd333 100644
--- a/website/etc/web.xml
+++ b/website/etc/web.xml
@@ -273,6 +273,17 @@
   </servlet-mapping>
 
   <servlet>
+    <servlet-name>MetricsLibServlet</servlet-name>
+    <servlet-class>
+      org.torproject.metrics.web.MetricsLibServlet
+    </servlet-class>
+  </servlet>
+  <servlet-mapping>
+    <servlet-name>MetricsLibServlet</servlet-name>
+    <url-pattern>/metrics-lib.html</url-pattern>
+  </servlet-mapping>
+
+  <servlet>
     <servlet-name>ResearchServlet</servlet-name>
     <servlet-class>
       org.torproject.metrics.web.ResearchServlet
diff --git a/website/src/org/torproject/metrics/web/MetricsLibServlet.java b/website/src/org/torproject/metrics/web/MetricsLibServlet.java
new file mode 100644
index 0000000..d71ea7a
--- /dev/null
+++ b/website/src/org/torproject/metrics/web/MetricsLibServlet.java
@@ -0,0 +1,31 @@
+/* Copyright 2017 The Tor Project
+ * See LICENSE for licensing information */
+
+package org.torproject.metrics.web;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class MetricsLibServlet extends AnyServlet {
+
+  private static final long serialVersionUID = -6009422570527820853L;
+
+  @Override
+  public void init() throws ServletException {
+    super.init();
+  }
+
+  @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/metrics-lib.jsp").forward(request,
+        response);
+  }
+}
+
diff --git a/website/web/WEB-INF/development.jsp b/website/web/WEB-INF/development.jsp
index 4bd3ea3..445f77b 100644
--- a/website/web/WEB-INF/development.jsp
+++ b/website/web/WEB-INF/development.jsp
@@ -21,7 +21,7 @@
       <h2>Parsing libraries <a href="#libraries" name="libraries" class="anchor">#</a></h2>
       <p>The following libraries help you with parsing Tor network data from the <a href="https://collector.torproject.org/"; target="_blank">CollecTor</a> service.</p>
       <ul>
-        <li><a href="https://dist.torproject.org/descriptor/"; target="_blank">metrics-lib</a> is a Java library to fetch and parse Tor descriptors.</li>
+        <li><a href="metrics-lib.html">metrics-lib</a> is a Java library to fetch and parse Tor descriptors.</li>
         <li><a href="https://stem.torproject.org/"; target="_blank">Stem</a> is a Python library that parses Tor descriptors.</li>
         <li><a href="https://github.com/NullHypothesis/zoossh"; target="_blank">Zoossh</a> is a parser written in Go for Tor-specific data formats.</li>
       </ul>
diff --git a/website/web/WEB-INF/metrics-lib.jsp b/website/web/WEB-INF/metrics-lib.jsp
new file mode 100644
index 0000000..0dac339
--- /dev/null
+++ b/website/web/WEB-INF/metrics-lib.jsp
@@ -0,0 +1,375 @@
+<%@ 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="Development &ndash; Tor Metrics"/>
+  <jsp:param name="navActive" value="Development"/>
+</jsp:include>
+
+    <div class="container">
+      <ul class="breadcrumb">
+        <li><a href="/">Home</a></li>
+        <li><a href="development.html">Development</a></li>
+        <li class="active">metrics-lib</li>
+      </ul>
+    </div>
+
+<div class="container">
+
+<div class="jumbotron">
+<div class="text-center">
+<h2>metrics-lib</h2>
+<p>metrics-lib is a Java API that facilitates processing Tor network data from the <a href="https://collector.torproject.org/";>CollecTor</a> service for statistical analysis and for building services and applications.</p>
+<a class="btn btn-primary btn-lg" style="margin: 10px" href="https://dist.torproject.org/descriptor/?C=M;O=D";><i class="fa fa-chevron-right" aria-hidden="true"></i> Download Release</a>
+<a class="btn btn-primary btn-lg" style="margin: 10px" href="https://gitweb.torproject.org/metrics-lib.git/plain/CHANGELOG.md";><i class="fa fa-chevron-right" aria-hidden="true"></i> View Change Log</a>
+<!--<a class="btn btn-primary btn-lg" style="margin: 10px" href="javadoc/index.html"><i class="fa fa-chevron-right" aria-hidden="true"></i> Browse JavaDocs</a>-->
+</div><!-- text-center -->
+</div><!-- jumbotron -->
+
+</div><!-- container -->
+<br>
+
+<br>
+
+<div class="container language-java">
+      <div class="row">
+        <div class="col-xs-12">
+
+<h1>metrics-lib</h1>
+
+<p>Welcome to metrics-lib, a Java API that facilitates processing Tor network data from the <a href="https://collector.torproject.org/";>CollecTor</a> service for statistical analysis and for building services and applications.</p>
+
+<p>In the tutorials below we're explaining the basic steps to get you started with metrics-lib.</p>
+
+<h2 id="prerequisites">Prerequisites and preparation <a href="#prerequisites" class="anchor">#</a></h2>
+
+<p>The following tutorials are written with an audience in mind that knows Java and to a lesser extent how Tor works.  We explain all data used in the tutorials.  More and most up-to-date information about descriptors can be found in the <a href="https://gitweb.torproject.org/torspec.git/tree/dir-spec.txt";>Tor directory protocol specification</a> and on the <a href="https://collector.torproject.org/";>CollecTor website</a>.</p>
+
+<p>All tutorials require you to <a href="https://dist.torproject.org/descriptor/?C=M;O=D";>download</a> the latest release of metrics-lib, follow the instructions to <a href="https://gitweb.torproject.org/metrics-lib.git/tree/README.md";>verify</a> its signature, extract the tarball locally, and copy the <code>lib/</code> and the <code>generated/</code> directories to your working directory for the tutorials.</p>
+
+<h2 id="tutorial1">Tutorial 1: Download descriptors from CollecTor <a href="#tutorial1" class="anchor">#</a></h2>
+
+<p>Let's start this tutorial series by doing something really simple.  We'll use metrics-lib to download <a href="https://collector.torproject.org/recent/relay-descriptors/consensuses/";>recent consensuses from CollecTor</a> and write them to a local directory.  We're not doing anything with those consensuses yet, though we'll get back to that in a bit.</p>
+
+<p>We'll need to tell metrics-lib five pieces of information for this:</p>
+
+<ol>
+<li>the CollecTor base URL without trailing slash (<code>"https://collector.torproject.org";</code>),</li>
+<li>which remote directories to collect descriptors from (<code>new String[] { "/recent/relay-descriptors/consensuses/" }</code>),</li>
+<li>the minimum last-modified time of files to be collected (<code>0L</code>),</li>
+<li>the local directory to write files to (<code>new File("descriptors")</code>), and</li>
+<li>whether to delete all local files that do not exist remotely anymore (<code>false</code>).</li>
+</ol>
+
+<p>Create a new file <code>DownloadConsensuses.java</code> with the following content:</p>
+
+<pre><code>import org.torproject.descriptor.*;
+
+import java.io.File;
+
+public class DownloadConsensuses {
+  public static void main(String[] args) {
+
+    // Download consensuses published in the last 72 hours, which will take up to five minutes and require several hundred MB on the local disk.
+    DescriptorCollector descriptorCollector = DescriptorSourceFactory.createDescriptorCollector();
+    descriptorCollector.collectDescriptors(
+        // Download from Tor's main CollecTor instance,
+        "https://collector.torproject.org";,
+        // include only network status consensuses
+        new String[] { "/recent/relay-descriptors/consensuses/" },
+        // regardless of last-modified time,
+        0L,
+        // write to the local directory called descriptors/,
+        new File("descriptors"),
+        // and don't delete extraneous files that do not exist remotely anymore.
+        false);
+  }
+}
+</code></pre>
+
+<p>If you haven't already done so, prepare the working directory for this tutorial as described <a href="#prerequisites">above</a>.</p>
+
+<p>Compile and run the Java file:</p>
+
+<pre>
+javac -cp lib/\*:generated/dist/signed/\* DownloadConsensuses.java
+</pre>
+<pre>
+java -cp .:lib/\*:generated/dist/signed/\* DownloadConsensuses
+</pre>
+
+<p>This will take up to five minutes and require several hundred MB on the local disk.</p>
+
+<p>If you want to play a bit with this code, you could extend it to also download recent bridge extra-info descriptors from CollecTor, which are stored in <code>/recent/bridge-descriptors/extra-infos/</code> and which we'll need for tutorial 3 below.  (If you're too <strike>impatient</strike> curious, scroll down to the bottom of this page for the diff.)</p>
+
+<h2 id="tutorial2">Tutorial 2: Relay capacity by Tor version <a href="#tutorial2" class="anchor">#</a></h2>
+
+<p>If you just followed tutorial 1 above, you now have a bunch of consensuses on your disk.  Let's do something with those and look at relay capacity by Tor version.  A possible use case could be that the Tor developers debate which of the older versions to turn into long-term supported versions, and you want to contribute more facts to that discussion by telling them how much relay capacity each version provides.</p>
+
+<p>Consider the following snippet from a consensus document showing a single relay to get an idea of the underlying data:</p>
+
+<pre>
+[...]
+r PrivacyRepublic0001 XOzFwwrMSz3kYnkjI5Zwh8xT2Uc WLlCQj3gVELkwIBh3EWxG74LZ2E 2017-03-04 08:16:22 178.32.181.96 443 80
+s Exit Fast Guard HSDir Running Stable V2Dir Valid
+v Tor 0.2.8.9
+pr Cons=1-2 Desc=1-2 DirCache=1 HSDir=1 HSIntro=3 HSRend=1 Link=1-4 LinkAuth=1 Microdesc=1-2 Relay=1-2
+w Bandwidth=136000
+p reject 22,25,109-110,119,143,465,563,587,6881-6889
+[...]
+</pre>
+
+<p>We're interested in the Tor version number without patch level (<code>0.2.8</code>) and the consensus weight (<code>136000</code>).</p>
+
+<p>Create a new file <code>ConsensusWeightByVersion.java</code> with the following content:</p>
+
+<pre><code class="language-java">import org.torproject.descriptor.*;
+
+import java.io.File;
+import java.util.*;
+
+public class ConsensusWeightByVersion {
+  public static void main(String[] args) {
+
+    // Download consensuses.
+    DescriptorCollector descriptorCollector = DescriptorSourceFactory.createDescriptorCollector();
+    descriptorCollector.collectDescriptors("https://collector.torproject.org";, new String[] { "/recent/relay-descriptors/consensuses/" }, 0L, new File("descriptors"), false);
+
+    // Keep local counters for extracted descriptor data.
+    long totalBandwidth = 0L;
+    SortedMap&lt;String, Long&gt; bandwidthByVersion = new TreeMap&lt;&gt;();
+
+    // Read descriptors from disk.
+    DescriptorReader descriptorReader = DescriptorSourceFactory.createDescriptorReader();
+
+    // Add the directory with descriptors to the descriptor reader.
+    descriptorReader.addDirectory(new File("descriptors/recent/relay-descriptors/consensuses"));
+    Iterator&lt;DescriptorFile&gt; descriptorFiles = descriptorReader.readDescriptors();
+    while (descriptorFiles.hasNext()) { // Iterate over all descriptor files found.
+      DescriptorFile descriptorFile = descriptorFiles.next();
+
+      // Now, iterate over the descriptors contained in the file.
+      for (Descriptor descriptor : descriptorFile.getDescriptors()) {
+        if (!(descriptor instanceof RelayNetworkStatusConsensus)) {
+          // We're only interested in consensuses.
+          continue;
+        }
+        RelayNetworkStatusConsensus consensus = (RelayNetworkStatusConsensus) descriptor;
+        for (NetworkStatusEntry entry : consensus.getStatusEntries().values()) {
+          String version = entry.getVersion();
+          if (!version.startsWith("Tor ") || version.length() &lt; 9) {
+            // We're only interested in a.b.c type versions for this example.
+            continue;
+          }
+          // Remove the 'Tor ' prefix and anything starting at the patch level.
+          version = version.substring(4, 9);
+          long bandwidth = entry.getBandwidth();
+          totalBandwidth += bandwidth;
+          if (bandwidthByVersion.containsKey(version)) {
+            bandwidthByVersion.put(version, bandwidth + bandwidthByVersion.get(version));
+          } else {
+            bandwidthByVersion.put(version, bandwidth);
+          }
+        }
+      }
+    }
+
+    // Print out fractions of consensus weight by Tor version.
+    if (totalBandwidth &gt; 0L) {
+      for (Map.Entry&lt;String, Long&gt; e : bandwidthByVersion.entrySet()) {
+        System.out.printf("%s -&gt; %4.1f%%%n", e.getKey(), (100.0 * (double) e.getValue() / (double) totalBandwidth));
+      }
+    }
+  }
+}
+</code></pre>
+
+<p>If you haven't already done so, prepare the working directory for this tutorial as described <a href="#prerequisites">above</a>.</p>
+
+<p>Compile and run the Java file:</p>
+
+<pre>
+javac -cp lib/\*:generated/dist/signed/\* ConsensusWeightByVersion.java
+</pre>
+<pre>
+java -cp .:lib/\*:generated/dist/signed/\* ConsensusWeightByVersion
+</pre>
+
+<p>There will be some log statements, and the final output should now contain lines like the following:</p>
+
+<pre>
+0.2.4 -&gt;  4.2%
+0.2.5 -&gt;  9.4%
+0.2.6 -&gt;  6.2%
+0.2.7 -&gt;  8.3%
+0.2.8 -&gt; 15.0%
+0.2.9 -&gt; 46.7%
+0.3.0 -&gt;  9.4%
+0.3.1 -&gt;  0.8%
+</pre>
+
+<p>These are the numbers we were looking for.  Now you should know what to do to extract interesting data from consensuses.  Want to give that another try and filter relays with the <code>Exit</code> flag to learn about exit capacity by Tor version?  Hint: You'll want to check for <code>entry.getFlags().contains("Exit")</code>.  Of course, you could as well continue with the next tutorial below.  (Or you could scroll down to the bottom of this page to see the diff.)</p>
+
+<h2 id="tutorial3">Tutorial 3: Frequency of transports <a href="#tutorial3" class="anchor">#</a></h2>
+
+<p>In the previous tutorial we looked at relay descriptors, so let's now look a bit at bridge descriptors.</p>
+
+<p>Every bridge publishes its transports in its extra-info descriptors that it periodically sends to the bridge authority.  Let's count the frequency of transports.  A possible use case could be that the Pluggable Transports developers debate which of the transport name is the least pronouncable, and you want to give them numbers to talk about something much more useful instead.</p>
+
+<p>Consider this snippet from a bridge extra-info descriptor:</p>
+
+<pre>
+extra-info LeifEricson 3E0908F131AC417C48DDD835D78FB6887F4CD126
+[...]
+transport obfs2
+transport scramblesuit
+transport obfs3
+transport obfs4
+transport fte
+</pre>
+
+<p>What we need to do is extract the list of transport names (<code>obfs2</code>, <code>scramblesuit</code>, etc.) together with the bridge fingerprint (<code>3E0908F131AC417C48DDD835D78FB6887F4CD126</code>).  Considering the fingerprint is important, so that we avoid double-counting transports provided by the same bridge.</p>
+
+<p>Create a new file <code>PluggableTransports.java</code> with the following content:</p>
+
+<pre><code class="language-java">import org.torproject.descriptor.*;
+
+import java.io.File;
+import java.util.*;
+
+public class PluggableTransports {
+  public static void main(String[] args) {
+
+    DescriptorCollector descriptorCollector = DescriptorSourceFactory.createDescriptorCollector();
+    descriptorCollector.collectDescriptors("https://collector.torproject.org";, new String[] { "/recent/bridge-descriptors/extra-infos/" }, 0L, new File("descriptors"), false);
+
+    Set&lt;String&gt; observedFingerprints = new HashSet&lt;&gt;();
+    SortedMap&lt;String, Integer&gt; countedTransports = new TreeMap&lt;&gt;();
+
+    DescriptorReader descriptorReader = DescriptorSourceFactory.createDescriptorReader();
+    descriptorReader.addDirectory(new File("descriptors/recent/bridge-descriptors/extra-infos"));
+    Iterator&lt;DescriptorFile&gt; descriptorFiles = descriptorReader.readDescriptors();
+    while (descriptorFiles.hasNext()) {
+      DescriptorFile descriptorFile = descriptorFiles.next();
+      for (Descriptor descriptor : descriptorFile.getDescriptors()) {
+        if (!(descriptor instanceof BridgeExtraInfoDescriptor)) {
+          continue;
+        }
+        BridgeExtraInfoDescriptor extraInfo = (BridgeExtraInfoDescriptor) descriptor;
+        String fingerprint = extraInfo.getFingerprint();
+        if (observedFingerprints.add(fingerprint)) {
+          for (String transport : extraInfo.getTransports()) {
+            if (countedTransports.containsKey(transport)) {
+              countedTransports.put(transport, 1 + countedTransports.get(transport));
+            } else {
+              countedTransports.put(transport, 1);
+            }
+          }
+        }
+      }
+    }
+
+    if (!observedFingerprints.isEmpty()) {
+      double totalObservedFingerprints = observedFingerprints.size();
+      for (Map.Entry&lt;String, Integer&gt; e : countedTransports.entrySet()) {
+        System.out.printf("%20s -&gt; %4.1f%%%n", e.getKey(), (100.0 * (double) e.getValue() / totalObservedFingerprints));
+      }
+    }
+  }
+}
+</code></pre>
+
+<p>If you haven't already done so, prepare the working directory for this tutorial as described <a href="#prerequisites">above</a>.</p>
+
+<p>Compile and run the Java file:</p>
+
+<pre>
+javac -cp lib/\*:generated/dist/signed/\* PluggableTransports.java
+</pre>
+<pre>
+java -cp .:lib/\*:generated/dist/signed/\* PluggableTransports
+</pre>
+
+<p>The output should contain lines like the following:</p>
+
+<pre>
+                 fte -> 11.7%
+                meek ->  0.2%
+               obfs2 ->  0.8%
+               obfs3 -> 35.7%
+     obfs3_websocket ->  0.0%
+               obfs4 -> 72.8%
+        scramblesuit -> 29.6%
+           snowflake ->  0.0%
+           websocket ->  0.8%
+</pre>
+
+<p>As above, we'll leave it up to you to further expand this code.  For example, how does the result change if you count transport <i>combinations</i> rather than transports?  Hint: you won't need anything else from metrics-lib, but you'll need to add some code to order transport names and write them to a string.  (And if you'd rather look up the solution, scroll down a bit to see the diff.)</p>
+
+<h2 id="nextsteps">Next steps <a href="#nextsteps" class="anchor">#</a></h2>
+
+<p>Want to write more code that uses metrics-lib?  Be sure to read the JavaDocs while developing new services or applications using Tor network data.</p>
+
+<p>Ran into a problem, found a bug, or came up with a cool new feature?  Feel free to <a href="https://metrics.torproject.org/about.html#contact";>contact us</a>.  Alternatively, take a look at the <a href="https://trac.torproject.org/projects/tor";>bug tracker</a> and open a ticket if there's none for your issue yet.</p>
+
+<p>Interested in writing <a href="https://gitweb.torproject.org/metrics-lib.git/";>code</a> for metrics-lib?  Please take a look at the Tor Metrics team <a href="https://trac.torproject.org/projects/tor/wiki/org/teams/MetricsTeam/Volunteers";>wiki page</a> to find out how to contribute.</p>
+
+<p>Scrolled down just to see where we're hiding the solutions of the three little riddles above?  Here are the diffs:</p>
+
+<pre><code class="language-diff">diff -Nur src/DownloadConsensuses.java src/DownloadConsensuses.java
+--- src/DownloadConsensuses.java        2017-03-07 17:48:35.000000000 +0100
++++ src/DownloadConsensuses.java        2017-03-10 23:02:51.000000000 +0100
+@@ -11,7 +11,7 @@
+         // Download from Tor's main CollecTor instance,
+         "https://collector.torproject.org";,
+         // include only network status consensuses
+-        new String[] { "/recent/relay-descriptors/consensuses/" },
++        new String[] { "/recent/bridge-descriptors/extra-infos/" },
+         // regardless of last-modified time,
+         0L,
+         // write to the local directory called descriptors/,
+</code></pre>
+
+<pre><code class="language-diff">diff -Nur src/ConsensusWeightByVersion.java src/ConsensusWeightByVersion.java
+--- src/ConsensusWeightByVersion.java   2017-03-10 23:00:40.000000000 +0100
++++ src/ConsensusWeightByVersion.java   2017-03-10 23:03:18.000000000 +0100
+@@ -31,6 +31,9 @@
+         }
+         RelayNetworkStatusConsensus consensus = (RelayNetworkStatusConsensus) descriptor;
+         for (NetworkStatusEntry entry : consensus.getStatusEntries().values()) {
++          if (!entry.getFlags().contains("Exit")) {
++            continue;
++          }
+           String version = entry.getVersion();
+           if (!version.startsWith("Tor ") || version.length() < 9) {
+             // We're only interested in a.b.c type versions for this example.
+</code></pre>
+
+<pre><code class="language-diff">diff -Nur src/PluggableTransports.java src/PluggableTransports.java
+--- src/PluggableTransports.java        2017-03-10 23:01:43.000000000 +0100
++++ src/PluggableTransports.java        2017-03-10 23:03:43.000000000 +0100
+@@ -24,12 +24,11 @@
+         BridgeExtraInfoDescriptor extraInfo = (BridgeExtraInfoDescriptor) descriptor;
+         String fingerprint = extraInfo.getFingerprint();
+         if (observedFingerprints.add(fingerprint)) {
+-          for (String transport : extraInfo.getTransports()) {
+-            if (countedTransports.containsKey(transport)) {
+-              countedTransports.put(transport, 1 + countedTransports.get(transport));
+-            } else {
+-              countedTransports.put(transport, 1);
+-            }
++          String transports = new TreeSet&lt;&gt;(extraInfo.getTransports()).toString();
++          if (countedTransports.containsKey(transports)) {
++            countedTransports.put(transports, 1 + countedTransports.get(transports));
++          } else {
++            countedTransports.put(transports, 1);
+           }
+         }
+       }
+</code></pre>
+
+</div> <!-- col -->
+</div> <!-- row -->
+</div> <!-- container -->
+
+<jsp:include page="bottom.jsp"/>
+
diff --git a/website/web/WEB-INF/top.jsp b/website/web/WEB-INF/top.jsp
index f34031b..b5e6a76 100644
--- a/website/web/WEB-INF/top.jsp
+++ b/website/web/WEB-INF/top.jsp
@@ -33,6 +33,10 @@
   <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>
+
   <!-- custom styles and javascript -->
   <link rel="stylesheet" href="css/style.css">
   <script src="js/script.js"></script>
diff --git a/website/web/css/prism.css b/website/web/css/prism.css
new file mode 100644
index 0000000..54ca58b
--- /dev/null
+++ b/website/web/css/prism.css
@@ -0,0 +1,179 @@
+/* http://prismjs.com/download.html?themes=prism&languages=clike+diff+java&plugins=line-numbers */
+/**
+ * prism.js default theme for JavaScript, CSS and HTML
+ * Based on dabblet (http://dabblet.com)
+ * @author Lea Verou
+ */
+
+code[class*="language-"],
+pre[class*="language-"] {
+	color: black;
+	background: none;
+	text-shadow: 0 1px white;
+	font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
+	text-align: left;
+	white-space: pre;
+	word-spacing: normal;
+	word-break: normal;
+	word-wrap: normal;
+	line-height: 1.5;
+
+	-moz-tab-size: 4;
+	-o-tab-size: 4;
+	tab-size: 4;
+
+	-webkit-hyphens: none;
+	-moz-hyphens: none;
+	-ms-hyphens: none;
+	hyphens: none;
+}
+
+pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection,
+code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection {
+	text-shadow: none;
+	background: #b3d4fc;
+}
+
+pre[class*="language-"]::selection, pre[class*="language-"] ::selection,
+code[class*="language-"]::selection, code[class*="language-"] ::selection {
+	text-shadow: none;
+	background: #b3d4fc;
+}
+
+@media print {
+	code[class*="language-"],
+	pre[class*="language-"] {
+		text-shadow: none;
+	}
+}
+
+/* Code blocks */
+pre[class*="language-"] {
+	padding: 1em;
+	margin: .5em 0;
+	overflow: auto;
+}
+
+:not(pre) > code[class*="language-"],
+pre[class*="language-"] {
+	background: #f5f2f0;
+}
+
+/* Inline code */
+:not(pre) > code[class*="language-"] {
+	padding: .1em;
+	border-radius: .3em;
+	white-space: normal;
+}
+
+.token.comment,
+.token.prolog,
+.token.doctype,
+.token.cdata {
+	color: slategray;
+}
+
+.token.punctuation {
+	color: #999;
+}
+
+.namespace {
+	opacity: .7;
+}
+
+.token.property,
+.token.tag,
+.token.boolean,
+.token.number,
+.token.constant,
+.token.symbol,
+.token.deleted {
+	color: #905;
+}
+
+.token.selector,
+.token.attr-name,
+.token.string,
+.token.char,
+.token.builtin,
+.token.inserted {
+	color: #690;
+}
+
+.token.operator,
+.token.entity,
+.token.url,
+.language-css .token.string,
+.style .token.string {
+	color: #a67f59;
+	background: hsla(0, 0%, 100%, .5);
+}
+
+.token.atrule,
+.token.attr-value,
+.token.keyword {
+	color: #07a;
+}
+
+.token.function {
+	color: #DD4A68;
+}
+
+.token.regex,
+.token.important,
+.token.variable {
+	color: #e90;
+}
+
+.token.important,
+.token.bold {
+	font-weight: bold;
+}
+.token.italic {
+	font-style: italic;
+}
+
+.token.entity {
+	cursor: help;
+}
+
+pre.line-numbers {
+	position: relative;
+	padding-left: 3.8em;
+	counter-reset: linenumber;
+}
+
+pre.line-numbers > code {
+	position: relative;
+}
+
+.line-numbers .line-numbers-rows {
+	position: absolute;
+	pointer-events: none;
+	top: 0;
+	font-size: 100%;
+	left: -3.8em;
+	width: 3em; /* works for line-numbers below 1000 lines */
+	letter-spacing: -1px;
+	border-right: 1px solid #999;
+
+	-webkit-user-select: none;
+	-moz-user-select: none;
+	-ms-user-select: none;
+	user-select: none;
+
+}
+
+	.line-numbers-rows > span {
+		pointer-events: none;
+		display: block;
+		counter-increment: linenumber;
+	}
+
+		.line-numbers-rows > span:before {
+			content: counter(linenumber);
+			color: #999;
+			display: block;
+			padding-right: 0.8em;
+			text-align: right;
+		}
diff --git a/website/web/css/style.css b/website/web/css/style.css
index 88cc2fa..70764ad 100644
--- a/website/web/css/style.css
+++ b/website/web/css/style.css
@@ -812,4 +812,42 @@ body.noscript #navbar-toggle-checkbox:checked ~ .collapse {
 
 
 
-
+/* code highlighting */
+
+.token.selector,
+.token.attr-name,
+.token.string,
+.token.char,
+.token.builtin,
+.token.property,
+.token.tag,
+.token.boolean,
+.token.number,
+.token.constant,
+.token.symbol,
+.token.deleted,
+.token.function {
+  color: #c7254e; /* red */
+}
+
+.token.atrule,
+.token.attr-value,
+.token.keyword,
+.token.inserted {
+  color: #7d4698; /* purple */
+}
+
+.token.comment,
+.token.prolog,
+.token.doctype,
+.token.cdata {
+  color: #999; /* light gray */
+}
+
+.token.operator,
+.token.entity,
+.token.url,
+.language-css .token.string,
+.style .token.string {
+  color: #000; /* black */
+}
diff --git a/website/web/js/prism.js b/website/web/js/prism.js
new file mode 100644
index 0000000..f2b3e1e
--- /dev/null
+++ b/website/web/js/prism.js
@@ -0,0 +1,6 @@
+/* http://prismjs.com/download.html?themes=prism&languages=clike+diff+java&plugins=line-numbers */
+var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(){var e=/\blang(?:uage)?-(\w+)\b/i,t=0,n=_self.Prism={manual:_self.Prism&&_self.Prism.manual,util:{encode:function(e){return e instanceof a?new a(e.type,n.util.encode(e.content),e.alias):"Array"===n.util.type(e)?e.map(n.util.encode):e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/\u00a0/g," ")},type:function(e){return Object.prototype.toString.call(e).match(/\[object (\w+)\]/)[1]},objId:function(e){return e.__id||Object.defineProperty(e,"__id",{value:++t}),e.__id},clone:function(e){var t=n.util.type(e);switch(t){case"Object":var a={};for(var r in e)e.hasOwnProperty(r)&&(a[r]=n.util.clone(e[r]));return a;case"Array":return e.map&&e.map(function(e){return n.util.clone(e)})}return e}},languages:{extend:function(e,t){var a=n.util.clone(n.languages[e]);for(var r in t)a[r]=t[r];return a},insertBefore:function(e,t,a,r){r=r||n.languages;var l=r[e]
 ;if(2==arguments.length){a=arguments[1];for(var i in a)a.hasOwnProperty(i)&&(l[i]=a[i]);return l}var o={};for(var s in l)if(l.hasOwnProperty(s)){if(s==t)for(var i in a)a.hasOwnProperty(i)&&(o[i]=a[i]);o[s]=l[s]}return n.languages.DFS(n.languages,function(t,n){n===r[e]&&t!=e&&(this[t]=o)}),r[e]=o},DFS:function(e,t,a,r){r=r||{};for(var l in e)e.hasOwnProperty(l)&&(t.call(e,l,e[l],a||l),"Object"!==n.util.type(e[l])||r[n.util.objId(e[l])]?"Array"!==n.util.type(e[l])||r[n.util.objId(e[l])]||(r[n.util.objId(e[l])]=!0,n.languages.DFS(e[l],t,l,r)):(r[n.util.objId(e[l])]=!0,n.languages.DFS(e[l],t,null,r)))}},plugins:{},highlightAll:function(e,t){var a={callback:t,selector:'code[class*="language-"], [class*="language-"] code, code[class*="lang-"], [class*="lang-"] code'};n.hooks.run("before-highlightall",a);for(var r,l=a.elements||document.querySelectorAll(a.selector),i=0;r=l[i++];)n.highlightElement(r,e===!0,a.callback)},highlightElement:function(t,a,r){for(var l,i,o=t;o&&!e.test(o.className
 );)o=o.parentNode;o&&(l=(o.className.match(e)||[,""])[1].toLowerCase(),i=n.languages[l]),t.className=t.className.replace(e,"").replace(/\s+/g," ")+" language-"+l,o=t.parentNode,/pre/i.test(o.nodeName)&&(o.className=o.className.replace(e,"").replace(/\s+/g," ")+" language-"+l);var s=t.textContent,u={element:t,language:l,grammar:i,code:s};if(n.hooks.run("before-sanity-check",u),!u.code||!u.grammar)return u.code&&(u.element.textContent=u.code),n.hooks.run("complete",u),void 0;if(n.hooks.run("before-highlight",u),a&&_self.Worker){var g=new Worker(n.filename);g.onmessage=function(e){u.highlightedCode=e.data,n.hooks.run("before-insert",u),u.element.innerHTML=u.highlightedCode,r&&r.call(u.element),n.hooks.run("after-highlight",u),n.hooks.run("complete",u)},g.postMessage(JSON.stringify({language:u.language,code:u.code,immediateClose:!0}))}else u.highlightedCode=n.highlight(u.code,u.grammar,u.language),n.hooks.run("before-insert",u),u.element.innerHTML=u.highlightedCode,r&&r.call(t),n.hooks.
 run("after-highlight",u),n.hooks.run("complete",u)},highlight:function(e,t,r){var l=n.tokenize(e,t);return a.stringify(n.util.encode(l),r)},tokenize:function(e,t){var a=n.Token,r=[e],l=t.rest;if(l){for(var i in l)t[i]=l[i];delete t.rest}e:for(var i in t)if(t.hasOwnProperty(i)&&t[i]){var o=t[i];o="Array"===n.util.type(o)?o:[o];for(var s=0;s<o.length;++s){var u=o[s],g=u.inside,c=!!u.lookbehind,h=!!u.greedy,f=0,d=u.alias;if(h&&!u.pattern.global){var p=u.pattern.toString().match(/[imuy]*$/)[0];u.pattern=RegExp(u.pattern.source,p+"g")}u=u.pattern||u;for(var m=0,y=0;m<r.length;y+=r[m].length,++m){var v=r[m];if(r.length>e.length)break e;if(!(v instanceof a)){u.lastIndex=0;var b=u.exec(v),k=1;if(!b&&h&&m!=r.length-1){if(u.lastIndex=y,b=u.exec(e),!b)break;for(var w=b.index+(c?b[1].length:0),_=b.index+b[0].length,P=m,A=y,j=r.length;j>P&&_>A;++P)A+=r[P].length,w>=A&&(++m,y=A);if(r[m]instanceof a||r[P-1].greedy)continue;k=P-m,v=e.slice(y,A),b.index-=y}if(b){c&&(f=b[1].length);var w=b.index+f,b=
 b[0].slice(f),_=w+b.length,x=v.slice(0,w),O=v.slice(_),S=[m,k];x&&S.push(x);var N=new a(i,g?n.tokenize(b,g):b,d,b,h);S.push(N),O&&S.push(O),Array.prototype.splice.apply(r,S)}}}}}return r},hooks:{all:{},add:function(e,t){var a=n.hooks.all;a[e]=a[e]||[],a[e].push(t)},run:function(e,t){var a=n.hooks.all[e];if(a&&a.length)for(var r,l=0;r=a[l++];)r(t)}}},a=n.Token=function(e,t,n,a,r){this.type=e,this.content=t,this.alias=n,this.length=0|(a||"").length,this.greedy=!!r};if(a.stringify=function(e,t,r){if("string"==typeof e)return e;if("Array"===n.util.type(e))return e.map(function(n){return a.stringify(n,t,e)}).join("");var l={type:e.type,content:a.stringify(e.content,t,r),tag:"span",classes:["token",e.type],attributes:{},language:t,parent:r};if("comment"==l.type&&(l.attributes.spellcheck="true"),e.alias){var i="Array"===n.util.type(e.alias)?e.alias:[e.alias];Array.prototype.push.apply(l.classes,i)}n.hooks.run("wrap",l);var o=Object.keys(l.attributes).map(function(e){return e+'="'+(l.attrib
 utes[e]||"").replace(/"/g,"&quot;")+'"'}).join(" ");return"<"+l.tag+' class="'+l.classes.join(" ")+'"'+(o?" "+o:"")+">"+l.content+"</"+l.tag+">"},!_self.document)return _self.addEventListener?(_self.addEventListener("message",function(e){var t=JSON.parse(e.data),a=t.language,r=t.code,l=t.immediateClose;_self.postMessage(n.highlight(r,n.languages[a],a)),l&&_self.close()},!1),_self.Prism):_self.Prism;var r=document.currentScript||[].slice.call(document.getElementsByTagName("script")).pop();return r&&(n.filename=r.src,!document.addEventListener||n.manual||r.hasAttribute("data-manual")||("loading"!==document.readyState?window.requestAnimationFrame?window.requestAnimationFrame(n.highlightAll):window.setTimeout(n.highlightAll,16):document.addEventListener("DOMContentLoaded",n.highlightAll))),_self.Prism}();"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism);
+Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\w\W]*?\*\//,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0}],string:{pattern:/(["'])(\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/((?:\b(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/i,lookbehind:!0,inside:{punctuation:/(\.|\\)/}},keyword:/\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,"boolean":/\b(true|false)\b/,"function":/[a-z0-9_]+(?=\()/i,number:/\b-?(?:0x[\da-f]+|\d*\.?\d+(?:e[+-]?\d+)?)\b/i,operator:/--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*|\/|~|\^|%/,punctuation:/[{}[\];(),.:]/};
+Prism.languages.diff={coord:[/^(?:\*{3}|-{3}|\+{3}).*$/m,/^@@.*@@$/m,/^\d+.*$/m],deleted:/^[-<].*$/m,inserted:/^[+>].*$/m,diff:{pattern:/^!(?!!).+$/m,alias:"important"}};
+Prism.languages.java=Prism.languages.extend("clike",{keyword:/\b(abstract|continue|for|new|switch|assert|default|goto|package|synchronized|boolean|do|if|private|this|break|double|implements|protected|throw|byte|else|import|public|throws|case|enum|instanceof|return|transient|catch|extends|int|short|try|char|final|interface|static|void|class|finally|long|strictfp|volatile|const|float|native|super|while)\b/,number:/\b0b[01]+\b|\b0x[\da-f]*\.?[\da-fp\-]+\b|\b\d*\.?\d+(?:e[+-]?\d+)?[df]?\b/i,operator:{pattern:/(^|[^.])(?:\+[+=]?|-[-=]?|!=?|<<?=?|>>?>?=?|==?|&[&=]?|\|[|=]?|\*=?|\/=?|%=?|\^=?|[?:~])/m,lookbehind:!0}}),Prism.languages.insertBefore("java","function",{annotation:{alias:"punctuation",pattern:/(^|[^.])@\w+/,lookbehind:!0}});
+!function(){"undefined"!=typeof self&&self.Prism&&self.document&&Prism.hooks.add("complete",function(e){if(e.code){var t=e.element.parentNode,s=/\s*\bline-numbers\b\s*/;if(t&&/pre/i.test(t.nodeName)&&(s.test(t.className)||s.test(e.element.className))&&!e.element.querySelector(".line-numbers-rows")){s.test(e.element.className)&&(e.element.className=e.element.className.replace(s,"")),s.test(t.className)||(t.className+=" line-numbers");var n,a=e.code.match(/\n(?!$)/g),l=a?a.length+1:1,r=new Array(l+1);r=r.join("<span></span>"),n=document.createElement("span"),n.setAttribute("aria-hidden","true"),n.className="line-numbers-rows",n.innerHTML=r,t.hasAttribute("data-start")&&(t.style.counterReset="linenumber "+(parseInt(t.getAttribute("data-start"),10)-1)),e.element.appendChild(n)}}})}();

_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits