[tor-commits] [bridgedb/master] Add a Content-Security-Policy header to all webserver pages.

isis at torproject.org isis at torproject.org
Sat Jul 25 19:26:23 UTC 2015


commit 002104d3773344fbeda94f071f9e50312e7e4223
Author: Isis Lovecruft <isis at torproject.org>
Date:   Mon Jul 20 19:46:34 2015 +0000

    Add a Content-Security-Policy header to all webserver pages.
    
    In addition, there were numerous elements of inline CSS styling and
    various onclick handlers which had to be rewritten.  While I was at it,
    I rewrote the stylesheets to fix serious usability/readability problems,
    particularly when using mobile devices like tablets and phones.  Other
    problems, like the onion logo in the background sometime overwriting or
    blending with text on some pages, are now fixed as well.
    
     * ADDS a new class, `bridgedb.https.server.CSPResource`, which handles
       adding an appropriate CSP header to all pages which inherit from it.
     * FIXES #15968: https://bugs.torproject.org/15968
    
     * CHANGES the CSS styling for some pages, for some window sizes and
       devices, to be more responsive, readable, and usable.
     * ADD @media CSS queries in order to improve readability and usability on
       small devices.
     * CHANGES the captcha.html page with various visual (i.e. the functionality
       hasn't been altered) improvements.
     * CHANGES the behaviour for loading additional CSS when rendered in a
       left-to-right charset.
     * CHANGE javascript functions to update the "aria-hidden" attribute for
       the QRCode modal properly, in order to improve accessibility
       (particularly for screen reader devices).
     * ADD use of "aria-disabled" attribute, for the "Select All" button
       when JS is disabled, for the accessibility reasons ibid.
     * FIXES #16649: https://bugs.torproject.org/16649
---
 bridgedb.conf                                  |   44 ++-
 bridgedb/configure.py                          |    3 +-
 bridgedb/https/server.py                       |  183 ++++++++++-
 bridgedb/https/templates/assets/css/custom.css |  158 ---------
 bridgedb/https/templates/assets/css/main.css   |  419 +++++++++++++++++++++++-
 bridgedb/https/templates/assets/css/rtl.css    |    7 +
 bridgedb/https/templates/assets/js/bridges.js  |   83 +++++
 bridgedb/https/templates/base.html             |  142 ++++----
 bridgedb/https/templates/bridges.html          |  102 ++----
 bridgedb/https/templates/captcha.html          |   92 +++---
 bridgedb/https/templates/howto.html            |   11 +-
 bridgedb/https/templates/index.html            |   12 +-
 bridgedb/https/templates/options.html          |  124 ++++---
 scripts/setup-tests                            |    1 +
 setup.py                                       |    3 +-
 test/https_helpers.py                          |   32 +-
 test/test_https.py                             |   22 ++
 test/test_https_server.py                      |  130 +++++++-
 18 files changed, 1105 insertions(+), 463 deletions(-)

diff --git a/bridgedb.conf b/bridgedb.conf
index 2c83582..bc79b86 100644
--- a/bridgedb.conf
+++ b/bridgedb.conf
@@ -15,11 +15,16 @@
 #           for details.
 # :copyright: (c) 2007-2015 The Tor Project, Inc.
 #             (c) 2007-2015, all sentient entities within the AUTHORS file
-# :version: 0.3.2
+# :version: 0.3.3
 #===============================================================================
 #
 # CHANGELOG:
 # ~~~~~~~~~~
+# Changed in version 0.3.3 - 2015-07-22
+#   * ADD new options, CSP_ENABLED, CSP_REPORT_ONLY, and CSP_INCLUDE_SELF for
+#     setting options related to HTTP(S) Distributor Content Security Policy
+#     settings.
+#
 # Changed in version 0.3.2 - 2015-04-30
 #   * CHANGE to using BridgeDB release versions for bridgedb.conf file versions.
 #   * ADD support for specifying bridge rotation periods via the
@@ -366,6 +371,43 @@ GIMP_CAPTCHA_DIR = 'captchas'
 GIMP_CAPTCHA_HMAC_KEYFILE = 'captcha_hmac_key'
 GIMP_CAPTCHA_RSA_KEYFILE = 'captcha_rsa_key'
 
+# Content Security Policy Settings
+# --------------------------------
+
+# (boolean) If True, enable use of CSP headers.  This must be True for any
+# other CSP-related options to have any effect.
+#
+# If enabled, the default Content Security Policy (CSP) is:
+#
+#     default-src 'none' ;
+#     base-uri FQDN ;
+#     script-src FQDN ;
+#     style-src FQDN ;
+#     img-src FQDN data: ;
+#     font-src FQDN ;
+#
+# where "FQDN" is the value of the SERVER_PUBLIC_FQDN config setting.
+#
+# If CSP_INCLUDE_SELF is enabled, then "'self'" (literally, the word self
+# surrounded by single-quotes) will be appended to the value of the
+# SERVER_PUBLIC_FQDN config setting to create the "FQDN".
+
+CSP_ENABLED = True
+
+# (boolean) If True (and CSP_ENABLED is also True), then set a "report-only"
+# Content Security Policy.  This means that client agents which run into
+# problems with or cause violations of our CSP settings will report data
+# regarding the problems/violations.  This report data is then logged (at the
+# DEBUG level), along with the client's IP address (only if SAFELOGGING is
+# disabled, otherwise the client's IP address is not reported).
+
+CSP_REPORT_ONLY = False
+
+# (boolean) If True, then append "'self'" to the "FQDN" in the default CSP
+# header described above.
+
+CSP_INCLUDE_SELF = True
+
 #-------------------------------
 # Email Distribution Options    \
 #------------------------------------------------------------------------------
diff --git a/bridgedb/configure.py b/bridgedb/configure.py
index 0056107..c406582 100644
--- a/bridgedb/configure.py
+++ b/bridgedb/configure.py
@@ -120,7 +120,8 @@ def loadConfig(configFile=None, configCls=None):
         setting = getattr(config, attr, None) # Default to None
         setattr(config, attr, setting)
 
-    for attr in ["IGNORE_NETWORKSTATUS"]:
+    for attr in ["IGNORE_NETWORKSTATUS", "CSP_ENABLED", "CSP_REPORT_ONLY",
+                 "CSP_INCLUDE_SELF"]:
         setting = getattr(config, attr, True) # Default to True
         setattr(config, attr, setting)
 
diff --git a/bridgedb/https/server.py b/bridgedb/https/server.py
index 2106dbf..ef40377 100644
--- a/bridgedb/https/server.py
+++ b/bridgedb/https/server.py
@@ -73,6 +73,36 @@ lookup = TemplateLookup(directories=[TEMPLATE_DIR],
                         collection_size=500)
 logging.debug("Set template root to %s" % TEMPLATE_DIR)
 
+#: This server's public, fully-qualified domain name.
+SERVER_PUBLIC_FQDN = None
+
+
+def setFQDN(fqdn, https=True):
+    """Set the global :data:`SERVER_PUBLIC FQDN` variable.
+
+    :param str fqdn: The public, fully-qualified domain name of the HTTP
+        server that will serve this resource.
+    :param bool https: If ``True``, then ``'https://'`` will be prepended to
+        the FQDN.  This is primarily used to create a
+        ``Content-Security-Policy`` header that will only allow resources to
+        be sourced via HTTPS, otherwise, if ``False``, it allow resources to
+        be sourced via any transport protocol.
+    """
+    if https:
+        fqdn = 'https://' + fqdn
+
+    logging.info("Setting HTTP server public FQDN to %r" % fqdn)
+
+    global SERVER_PUBLIC_FQDN
+    SERVER_PUBLIC_FQDN = fqdn
+
+def getFQDN():
+    """Get the setting for the HTTP server's public FQDN from the global
+    :data:`SERVER_PUBLIC_FQDN variable.
+
+    :rtype: str or None
+    """
+    return SERVER_PUBLIC_FQDN
 
 def getClientIP(request, useForwardedHeader=False):
     """Get the client's IP address from the :header:`X-Forwarded-For`
@@ -146,7 +176,128 @@ def replaceErrorPage(error, template_name=None):
     return rendered
 
 
-class TranslatedTemplateResource(resource.Resource):
+class CSPResource(resource.Resource):
+    """A resource which adds a ``'Content-Security-Policy:'`` header.
+
+    :vartype reportViolations: bool
+    :var reportViolations: Use the Content Security Policy in `report-only`_
+        mode, causing CSP violations to be reported back to the server (at
+        :attr:`reportURI`, where the details of the violation will be logged).
+        (default: ``False``)
+    :vartype reportURI: str
+    :var reportURI: If :attr:`reportViolations` is ``True``, Content Security
+        Policy violations will be sent as JSON-encoded POST request to this
+        URI.  (default: ``'csp-violation'``)
+
+    .. _report-only:
+        https://w3c.github.io/webappsec/specs/content-security-policy/#content-security-policy-report-only-header-field
+    """
+    reportViolations = False
+    reportURI = 'csp-violation'
+
+    def __init__(self, includeSelf=False, enabled=True, reportViolations=False,
+                 useForwardedHeader=False):
+        """Create a new :api:`twisted.web.resource.Resource` which adds a
+        ``'Content-Security-Policy:'`` header.
+
+        If enabled, the default Content Security Policy is::
+
+            default-src 'none' ;
+            base-uri FQDN ;
+            script-src FQDN ;
+            style-src FQDN ;
+            img-src FQDN data: ;
+            font-src FQDN ;
+
+        where ``FQDN`` the value returned from the :func:`getFQDN` function
+        (which uses the ``SERVER_PUBLIC_FQDN`` config file option).
+
+        If the **includeSelf** parameter is enabled, then ``"'self'"``
+        (literally, a string containing the word ``self``, surrounded by
+        single-quotes) will be appended to the ``FQDN``.
+
+        :param str fqdn: The public, fully-qualified domain name
+            of the HTTP server that will serve this resource.
+        :param bool includeSelf: Append ``'self'`` after the **fqdn** in the
+            Content Security Policy.
+        :param bool enabled: If ``False``, all Content Security Policy
+            headers, including those used in report-only mode, will not be
+            sent.  If ``True``, Content Security Policy headers (regardless of
+            whether report-only mode is dis-/en-abled) will be sent.
+            (default: ``True``)
+        :param bool reportViolations: Use the Content Security Policy in
+            report-only mode, causing CSP violations to be reported back to
+            the server (at :attr:`reportURI`, where the details of the
+            violation will be logged).  (default: ``False``)
+        :param bool useForwardedHeader: If ``True``, then we will attempt to
+            obtain the client's IP address from the ``X-Forwarded-For`` HTTP
+            header.  This *only* has an effect if **reportViolations** is also
+            set to ``True`` — the client's IP address is logged along with any
+            CSP violation reports which the client sent via HTTP POST requests
+            to our :attr:`reportURI`.  (default: ``False``)
+        """
+        resource.Resource.__init__(self)
+
+        self.fqdn = getFQDN()
+        self.enabled = enabled
+        self.useForwardedHeader = useForwardedHeader
+        self.csp = ("default-src 'none'; "
+                    "base-uri {0}; "
+                    "script-src {0}; "
+                    "style-src {0}; "
+                    "img-src {0} data:; "
+                    "font-src {0}; ")
+
+        if includeSelf:
+            self.fqdn = " ".join([self.fqdn, "'self'"])
+
+        if reportViolations:
+            self.reportViolations = reportViolations
+
+    def setCSPHeader(self, request):
+        """Set the CSP header for a **request**.
+
+        If this :class:`CSPResource` is :attr:`enabled`, then use
+        :api:`twisted.web.http.Request.setHeader` to send an HTTP
+        ``'Content-Security-Policy:'`` header for any response made to the
+        **request** (or a ``'Content-Security-Policy-Report-Only:'`` header,
+        if :attr:`reportViolations` is enabled).
+
+        :type request: :api:`twisted.web.http.Request`
+        :param request: A ``Request`` object for :attr:`reportViolationURI`.
+        """
+        self.fqdn = self.fqdn or getFQDN()  # Update the FQDN if it changed.
+
+        if self.enabled and self.fqdn:
+            if not self.reportViolations:
+                request.setHeader("Content-Security-Policy",
+                                  self.csp.format(self.fqdn))
+            else:
+                logging.debug("Sending report-only CSP header...")
+                request.setHeader("Content-Security-Policy-Report-Only",
+                                  self.csp.format(self.fqdn) +
+                                  "report-uri /%s" % self.reportURI)
+
+    def render_POST(self, request):
+        """If we're in debug mode, log a Content Security Policy violation.
+
+        :type request: :api:`twisted.web.http.Request`
+        :param request: A ``Request`` object for :attr:`reportViolationURI`.
+        """
+        try:
+            client = getClientIP(request, self.useForwardedHeader)
+            report = request.content.read(2048)
+
+            logging.warning("Content-Security-Policy violation report from %s: %r"
+                            % (client or "UNKNOWN CLIENT", report))
+        except Exception as err:
+            logging.error("Error while attempting to log CSP report: %s" % err)
+
+        # Redirect back to the original resource after the report was logged:
+        return redirectTo(request.uri, request)
+
+
+class TranslatedTemplateResource(CSPResource):
     """A generalised resource which uses gettext translations and Mako
     templates.
     """
@@ -157,10 +308,11 @@ class TranslatedTemplateResource(resource.Resource):
         Mako-templated webpage.
         """
         gettext.install("bridgedb", unicode=True)
-        resource.Resource.__init__(self)
+        CSPResource.__init__(self)
         self.template = template
 
     def render_GET(self, request):
+        self.setCSPHeader(request)
         rtl = False
         try:
             langs = translations.getLocaleFromHTTPRequest(request)
@@ -200,14 +352,14 @@ class HowtoResource(TranslatedTemplateResource):
         TranslatedTemplateResource.__init__(self, 'howto.html')
 
 
-class CaptchaProtectedResource(resource.Resource):
+class CaptchaProtectedResource(CSPResource):
     """A general resource protected by some form of CAPTCHA."""
 
     isLeaf = True
 
     def __init__(self, publicKey=None, secretKey=None,
                  useForwardedHeader=False, protectedResource=None):
-        resource.Resource.__init__(self)
+        CSPResource.__init__(self)
         self.publicKey = publicKey
         self.secretKey = secretKey
         self.useForwardedHeader = useForwardedHeader
@@ -275,6 +427,8 @@ class CaptchaProtectedResource(resource.Resource):
         :returns: A rendered HTML page containing a ReCaptcha challenge image
             for the client to solve.
         """
+        self.setCSPHeader(request)
+
         rtl = False
         image, challenge = self.getCaptchaImage(request)
 
@@ -312,6 +466,7 @@ class CaptchaProtectedResource(resource.Resource):
         :returns: A rendered HTML page containing a ReCaptcha challenge image
             for the client to solve.
         """
+        self.setCSPHeader(request)
         request.setHeader("Content-Type", "text/html; charset=utf-8")
 
         if self.checkSolution(request) is True:
@@ -354,6 +509,8 @@ class GimpCaptchaProtectedResource(CaptchaProtectedResource):
         :type protectedResource: :api:`twisted.web.resource.Resource`
         :param protectedResource: The resource to serve if the client
             successfully passes the CAPTCHA challenge.
+        :param str serverPublicFQDN: The public, fully-qualified domain name
+            of the HTTP server that will serve this resource.
         """
         CaptchaProtectedResource.__init__(self, **kwargs)
         self.hmacKey = hmacKey
@@ -614,12 +771,13 @@ class ReCaptchaProtectedResource(CaptchaProtectedResource):
             :meth:`_renderDeferred` will handle rendering and displaying the
             HTML to the client.
         """
+        self.setCSPHeader(request)
         d = self.checkSolution(request)
         d.addCallback(self._renderDeferred)
         return NOT_DONE_YET
 
 
-class BridgesResource(resource.Resource):
+class BridgesResource(CSPResource):
     """This resource displays bridge lines in response to a request."""
 
     isLeaf = True
@@ -641,7 +799,7 @@ class BridgesResource(resource.Resource):
             fingerprint in the response?
         """
         gettext.install("bridgedb", unicode=True)
-        resource.Resource.__init__(self)
+        CSPResource.__init__(self)
         self.distributor = distributor
         self.schedule = schedule
         self.nBridgesToGive = N
@@ -663,6 +821,8 @@ class BridgesResource(resource.Resource):
         :rtype: str
         :returns: A plaintext or HTML response to serve.
         """
+        self.setCSPHeader(request)
+
         try:
             response = self.getBridgeRequestAnswer(request)
         except Exception as err:
@@ -815,6 +975,10 @@ def addWebServer(config, distributor):
              GIMP_CAPTCHA_DIR
              GIMP_CAPTCHA_HMAC_KEYFILE
              GIMP_CAPTCHA_RSA_KEYFILE
+             SERVER_PUBLIC_FQDN
+             CSP_ENABLED
+             CSP_REPORT_ONLY
+             CSP_INCLUDE_SELF
     :type distributor: :class:`bridgedb.https.distributor.HTTPSDistributor`
     :param distributor: A bridge distributor.
     :raises SystemExit: if the servers cannot be started.
@@ -828,12 +992,18 @@ def addWebServer(config, distributor):
 
     logging.info("Starting web servers...")
 
+    setFQDN(config.SERVER_PUBLIC_FQDN)
+
     index   = IndexResource()
     options = OptionsResource()
     howto   = HowtoResource()
     robots  = static.File(os.path.join(TEMPLATE_DIR, 'robots.txt'))
     assets  = static.File(os.path.join(TEMPLATE_DIR, 'assets/'))
     keys    = static.Data(bytes(strings.BRIDGEDB_OPENPGP_KEY), 'text/plain')
+    csp     = CSPResource(enabled=config.CSP_ENABLED,
+                          includeSelf=config.CSP_INCLUDE_SELF,
+                          reportViolations=config.CSP_REPORT_ONLY,
+                          useForwardedHeader=fwdHeaders)
 
     root = resource.Resource()
     root.putChild('', index)
@@ -842,6 +1012,7 @@ def addWebServer(config, distributor):
     root.putChild('assets', assets)
     root.putChild('options', options)
     root.putChild('howto', howto)
+    root.putChild(CSPResource.reportURI, csp)
 
     if config.RECAPTCHA_ENABLED:
         publicKey = config.RECAPTCHA_PUB_KEY
diff --git a/bridgedb/https/templates/assets/css/custom.css b/bridgedb/https/templates/assets/css/custom.css
deleted file mode 100644
index f0bde6f..0000000
--- a/bridgedb/https/templates/assets/css/custom.css
+++ /dev/null
@@ -1,158 +0,0 @@
-body {
-    padding-top: 20px;
-    padding-bottom: 40px;
-}
-
-/* Custom container */
-.container-narrow {
-    margin: 0 auto;
-    max-width: 675px;
-}
-.container-narrow > hr {
-    margin: 30px 0;
-}
-
-/* Main marketing message and sign up button */
-.jumbotron {
-    margin: 60px 0;
-    text-align: center;
-}
-.jumbotron h1 {
-    font-size: 72px;
-    line-height: 1;
-}
-.jumbotron .btn {
-    font-size: 21px;
-    padding: 14px 24px;
-}
-
-/* Supporting marketing content */
-.marketing {
-    margin: 60px 0;
-}
-.marketing p + h4 {
-    margin-top: 28px;
-}
-
-.captcha {
-    margin: auto;
-    display: block;
-    width: 250px;
-}
-
-.captcha .btn {
-    margin: auto;
-    width: 100px;
-    display: block;
-}
-
-.fixed-size-btn {
-    margin: 10px;
-    width: 200px;
-}
-
-.main-steps {
-    margin: 50px;
-}
-
-
-.main-btns {
-    margin: auto;
-    width: 450px;
-    display: block;
-}
-
-.step{
-border: 1px solid #ccc;
-border: 1px solid rgba(0, 0, 0, 0.2);
--webkit-border-radius: 6px;
--moz-border-radius: 6px;
-border-radius: 6px;
--webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
--moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
--webkit-background-clip: padding-box;
--moz-background-clip: padding;
-background-clip: padding-box;
-padding: 10px 20px 0px;
-margin-bottom: 10px;
-}
-
-.step-title {
-color: #808080;
-font-size: 18px;
-font-weight: 100;
-}
-
-.step-text {
-    font-size: 18px;
-line-height: 30px;
-margin-top: 2px;
-}
-.lead_right {
-    margin-bottom: 20px;
-    font-size: 21px;
-    font-weight: 200;
-    line-height: 30px
-}
-[class*="bdb_span"] {
-    min-height: 1px;
-    margin: 0 20px 16px 20px;
-}
-.bdb_span7 {
-    width: 560px
-}
-
-div.bridge-lines {
-    padding: 20px;
-    margin: 0px 0px 20px;
-    min-height: 20px;
-    font-family: Monaco,Menlo,Consolas,"Courier New",monospace;
-    font-size: 95%;
-    line-height: 175%;
-    color: rgb(44, 62, 80);
-    word-break: break-all;
-    word-wrap: normal;
-    white-space: nowrap;
-    overflow-x: auto;
-    z-index: 1000;
-    background-color: rgb(236, 240, 241);
-    border: 0px solid transparent;
-    border-radius: 6px 6px 6px 6px;
-    border-color: #2C3E50;
-    box-shadow: none;
-    box-sizing: border-box;
-    -moz-box-sizing: border-box;
-    cursor: copy;
-}
-
-div.bridge-lines.-webkit-scrollbar {
-    width: 9px;
-    height: 9px;
-}
-div.bridge-lines.-webkit-scrollbar-button.start.decrement {
-    display: block;
-    height: 0;
-    background-color: transparent;
-}
-div.bridge-lines.-webkit-scrollbar-button.end.increment {
-    display: block;
-    height: 0;
-    background-color: transparent;
-}
-div.bridge-lines.-webkit-scrollbar-track-piece {
-    background-color: #FAFAFA;
-    -webkit-border-radius: 0;
-    -webkit-border-bottom-right-radius: 6px;
-    -webkit-border-bottom-left-radius: 6px;
-}
-div.bridge-lines.-webkit-scrollbar-thumb.vertical {
-    height: 50px;
-    background-color: #999;
-    -webkit-border-radius: 6px;
-}
-div.bridge-lines.-webkit-scrollbar-thumb.horizontal{
-    width: 50px;
-    background-color: #999;
-    -webkit-border-radius: 6px;
-}
diff --git a/bridgedb/https/templates/assets/css/main.css b/bridgedb/https/templates/assets/css/main.css
index df34981..2b06a40 100644
--- a/bridgedb/https/templates/assets/css/main.css
+++ b/bridgedb/https/templates/assets/css/main.css
@@ -1,7 +1,6 @@
 /* Imports */
 @import url("bootstrap.min.css");
 @import url("font-awesome.min.css");
- at import url("custom.css");
 
 /* Fonts */
 @font-face {
@@ -22,3 +21,421 @@
   font-weight: 400;
   src: local('Lato Italic'), local('Lato-Italic'), url('../font/lato-italic.woff') format('woff');
 }
+
+body {
+  padding-bottom: 40px;
+}
+.background-1 {
+  background: url(/assets/tor-roots-blue.svg);
+  background-repeat: no-repeat;
+  background-attachment: scroll;
+  background-position: center 10%;
+  background-size: containx;
+}
+.background-2 {
+  background: rgba(255, 255, 255, 0.9875);
+  background-repeat: repeat;
+  background-attachment: fixed;
+  background-position: center top;
+  background-size: cover;
+}
+.masthead {
+  background: rgba(255, 255, 255, 1.0);
+}
+.footer {
+  background: rgba(255, 255, 255, 1.0);
+}
+
+/* The following is made responsive by the */
+/* media queries at the end of this file.  */
+.footer-small p,
+.footer-small a,
+.footer-small i {
+  font-size: 90%;
+}
+
+/* Custom container */
+.container-narrow {
+  margin: 0 auto;
+  max-width: 675px;
+}
+.container-narrow > hr {
+  margin: 30px 0;
+}
+.container-fluid-outer,
+.container-fluid-outer-98,
+.container-fluid-outer-96,
+.container-fluid-outer-96-lr {
+  align: center;
+  margin: 2%;
+}
+.container-fluid-outer-98 {
+  width: 98%;
+}
+.container-fluid-outer-96,
+.container-fluid-outer-96-lr {
+  width: 96%;
+}
+.container-fluid-outer-96-lr {
+  margin-top: 0%;
+  margin-bottom: 0%;
+  margin-left: 2%;
+  margin-right: 2%;
+}
+.container-fluid-outer-60-lr {
+  margin-top: 0%;
+  margin-bottom: 0%;
+  margin-left: 20%;
+  margin-right: 20%;
+}
+.container-fluid-outer-100 {
+  width: 100%;
+}
+.container-fluid-inner,
+.container-fluid-inner-2 {
+  padding: 2%;
+}
+.container-fluid-inner-5 {
+  padding: 5%;
+}
+.container-fluid-inner-10 {
+  padding: 10%;
+}
+
+/* Main marketing message and sign up button */
+.jumbotron {
+  margin: 60px 0;
+  text-align: center;
+}
+.jumbotron h1 {
+  font-size: 72px;
+  line-height: 1;
+}
+.jumbotron .btn {
+  font-size: 21px;
+  padding: 14px 24px;
+}
+
+/* Supporting marketing content */
+.marketing {
+  margin: 60px 0;
+}
+.marketing p + h4 {
+  margin-top: 28px;
+}
+
+.captcha {
+  margin: auto;
+  display: block;
+  width: 250px;
+}
+.captcha .btn {
+  margin: auto;
+  width: 100px;
+  display: block;
+}
+.fixed-size-btn {
+  margin: 10px;
+  width: 200px;
+}
+
+.main-steps {
+  margin: 50px;
+  z-index: 1000
+}
+.main-btns {
+  margin: auto;
+  width: 450px;
+  display: block;
+}
+
+.step,
+.step-semi-transparent
+{
+  border: 1px solid #ccc;
+  border: 1px solid rgba(0, 0, 0, 0.2);
+  -webkit-border-radius: 6px;
+  -moz-border-radius: 6px;
+  border-radius: 6px;
+  -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+  -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+  box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+  -webkit-background-clip: padding-box;
+  -moz-background-clip: padding;
+  background-clip: padding-box;
+  padding: 10px 20px 0px;
+  margin-bottom: 10px;
+  background-color: rgba(255, 255, 255, 1.0);
+}
+.step-semi-transparent {
+  background-color: rgba(255, 255, 255, 0.85);
+}
+.step-title {
+  color: #808080;
+  font-size: 18px;
+  font-weight: 100;
+}
+.step-text {
+  font-size: 18px;
+  line-height: 30px;
+  margin-top: 2px;
+  margin-left: 20px;
+  margin-right: 20px;
+}
+.lead_right {
+  margin-bottom: 20px;
+  font-size: 21px;
+  font-weight: 200;
+  line-height: 30px
+}
+[class*="bdb_span"] {
+  min-height: 1px;
+  margin: 0 20px 16px 20px;
+}
+.bdb_span7 {
+  width: 560px
+}
+
+#instructions {
+  align-content: left;
+}
+#advanced-options-legend {
+  font-size: 112%
+}
+#options-row-1,
+#options-row-2 {
+  width: 98%;
+  height: inherit;
+  margin: auto;
+}
+#options-row-2 {
+  margin-right: auto;
+  padding: 4%;
+}
+#options-row-1-col-1,
+#options-row-1-col-2,
+#options-row-2-col1 {
+  width: 50%;
+  height: inherit;
+}
+#options-row-2-col-1 {
+  content-align: center;
+  text-align: center;
+  width: 50%;
+  padding-right: 25%;
+}
+#options-row-1-col-1 {
+  float: left;
+}
+#options-row-1-col-2 {
+  float: right;
+}
+#options-row-1-col-1-step,
+#options-row-1-col-2-step {
+  height: inherit;
+}
+#options-row-1-col-2-step {
+  margin-right: 1%;
+}
+#options-transport-label,
+#options-ipv6-label {
+  text-align: inherit;
+  font-size: 13px;
+  padding-top: 0%;
+  padding-left: 2%;
+  padding-bottom: 5%;
+  padding-right: 2%;
+}
+#options-transport-btn-group,
+#options-ipv6-input-group {
+  float: left;
+}
+#options-ipv6-checkbox {
+  float: left;
+}
+
+#captcha-submission-container {
+  width: 90%;
+  align: center;
+  margin: auto;
+}
+#captcha-box {
+  padding: 5% 15% 5% 15%;
+}
+#captcha-box p {
+  align: center;
+}
+#captcha-box img {
+  /*width: 400px;*/
+  /*height: 125px;*/
+  width: 100%;
+  min-width: 200px;
+}
+#captcha-form-box {
+  align: center;
+  width: 50%;
+  margin: auto;
+}
+#captcha_response_field {
+  height: 47px;
+  margin-top: 0px;
+}
+#captcha-submit-button {
+  height: 47px;
+}
+
+div.bridge-lines {
+  padding: 20px;
+  margin: 0px 0px 20px;
+  min-height: 20px;
+  font-family: Monaco,Menlo,Consolas,"Courier New",monospace;
+  font-size: 95%;
+  line-height: 175%;
+  color: rgb(44, 62, 80);
+  word-break: break-all;
+  word-wrap: normal;
+  white-space: nowrap;
+  overflow-x: auto;
+  z-index: 1000;
+  background-color: rgb(236, 240, 241);
+  border: 0px solid transparent;
+  border-radius: 6px 6px 6px 6px;
+  border-color: #2C3E50;
+  box-shadow: none;
+  box-sizing: border-box;
+  -moz-box-sizing: border-box;
+}
+div.bridge-lines .cursor-copy {
+  cursor: copy;
+}
+div.bridge-lines.-webkit-scrollbar {
+  width: 9px;
+  height: 9px;
+}
+div.bridge-lines.-webkit-scrollbar-button.start.decrement {
+  display: block;
+  height: 0;
+  background-color: transparent;
+}
+div.bridge-lines.-webkit-scrollbar-button.end.increment {
+  display: block;
+  height: 0;
+  background-color: transparent;
+}
+div.bridge-lines.-webkit-scrollbar-track-piece {
+  background-color: #FAFAFA;
+  -webkit-border-radius: 0;
+  -webkit-border-bottom-right-radius: 6px;
+  -webkit-border-bottom-left-radius: 6px;
+}
+div.bridge-lines.-webkit-scrollbar-thumb.vertical {
+  height: 50px;
+  background-color: #999;
+  -webkit-border-radius: 6px;
+}
+div.bridge-lines.-webkit-scrollbar-thumb.horizontal{
+  width: 50px;
+  background-color: #999;
+  -webkit-border-radius: 6px;
+}
+#container-bridges {
+  width: auto;
+  align: center;
+  margin: auto;
+  position: relative;
+  left: 0%;
+}
+#bridgesrow2 {
+  text-align: right;
+  padding-right: 7%;
+}
+#bridgesrow2 .btn {
+  margin-bottom: 1%;
+}
+.hidden {
+  display: none;
+}
+.visible {
+  display: block;
+}
+#qrcode .modal-dialog,
+#qrcode .modal-sm {
+  width: 400px;
+}
+#qrcode img {
+  width: 350px;
+  height: 350px;
+}
+#qrcode-para {
+  text-align: center;
+}
+#howto {
+  align-content: left;
+}
+
+#uh-oh-spaghettios {
+  width: 80%;
+  margin: auto;
+}
+.uh-oh-spaghettios~p {
+  text-align: center;
+  font-size: 115%;
+}
+
+ at media (min-width: 640px) and (max-width: 980px) {
+  .footer-small p,
+  .footer-small a,
+  .footer-small i {
+    font-size: 80%;
+  }
+}
+ at media (min-width: 1px) and (max-width: 768px), handheld {
+  .navbar-nav {
+    display: none;
+  }
+}
+ at media (min-width: 1px) and (max-width: 640px), handheld {
+  body {
+    font-size: 14px;
+  }
+  .background-1,
+  .background-2 {
+    background: none;
+  }
+  .main-steps {
+    margin: 25px;
+  }
+  .step {
+    padding: 10px 0px 0px 0px;
+  }
+  .step-title,
+  .step-text {
+    font-size: 16px;
+  }
+  .step-text {
+    margin-left: 10px;
+    margin-right: 10px;
+  }
+  .footer-small p {
+    font-size: 75%;
+    text-align: center;
+  }
+  .footer-small a,
+  .footer-small i {
+    font-size: 125%;
+  }
+  #footer-bugs,
+  #footer-code,
+  #footer-changelog,
+  #footer-contact,
+  #footer-keys {
+    font-size: 0px;
+  }
+  #just-give-me-bridges-btn,
+  #get-bridges-btn,
+  .control-label {
+    font-size: 14px;
+  }
+}
+ at media (min-width: 1px) and (max-width: 480px), handheld {
+}
diff --git a/bridgedb/https/templates/assets/css/rtl.css b/bridgedb/https/templates/assets/css/rtl.css
new file mode 100644
index 0000000..3e51aa5
--- /dev/null
+++ b/bridgedb/https/templates/assets/css/rtl.css
@@ -0,0 +1,7 @@
+span,p,h3,h4 {
+    text-align: right;
+    direction: rtl;
+}
+span {
+    float: right;
+}
diff --git a/bridgedb/https/templates/assets/js/bridges.js b/bridgedb/https/templates/assets/js/bridges.js
new file mode 100644
index 0000000..6d37ce7
--- /dev/null
+++ b/bridgedb/https/templates/assets/js/bridges.js
@@ -0,0 +1,83 @@
+// Takes one argument, `element`, which should be a string specifying the id
+// of the element whose text should be selected.
+function selectText(element) {
+  try {
+    var range;
+    var selection;
+    text = document.getElementById(element);
+
+    if (document.body.createTextRange) {
+      range = document.body.createTextRange();
+      range.moveToElementText(text);
+      range.select();
+    } else if (window.getSelection) {
+      selection = window.getSelection();
+      range = document.createRange();
+      range.selectNodeContents(text);
+      selection.removeAllRanges();
+      selection.addRange(range);
+    }
+  } catch (e) {
+    console.log(e);
+  }
+}
+
+function displayOrHide(element) {
+  try {
+    e = document.getElementById(element);
+    if (e.classList.contains('hidden')) {
+      // Don't use classList.toggle() because vendors seem to handle the
+      // secondary, optional "force" parameter in different ways.
+      document.getElementById(element).classList.remove('hidden');
+      document.getElementById(element).classList.add('visible');
+      document.getElementById(element).setAttribute('aria-hidden', 'false');
+    } else if (e.classList.contains('visible')) {
+      document.getElementById(element).classList.remove('visible');
+      document.getElementById(element).classList.add('hidden');
+      document.getElementById(element).setAttribute('aria-hidden', 'true');
+    }
+  } catch (err) {
+    console.log(err);
+  }
+}
+
+window.onload = function() {
+  var selectBtn = document.getElementById('selectbtn');
+  if (selectBtn) {
+    document.getElementById('selectbtn').addEventListener('click',
+      function() {
+        selectText('bridgelines');
+      }, false);
+    // Make the 'Select All' button clickable:
+    selectBtn.classList.remove('disabled');
+    selectBtn.setAttribute('aria-disabled', 'false');
+  }
+
+  var bridgesContainer = document.getElementById('container-bridges');
+  if (bridgesContainer) {
+    document.getElementById('bridgelines').classList.add('cursor-copy');
+    document.getElementById('bridgelines').addEventListener('click',
+      function() {
+        selectText('bridgelines');
+      }, false);
+  }
+
+  var qrcodeBtn = document.getElementById('qrcodebtn');
+  if (qrcodeBtn) {
+    document.getElementById('qrcodebtn').addEventListener('click',
+      function() {
+        displayOrHide('qrcode');
+      }, false);
+    // Remove the href attribute that opens the QRCode image as a data: URL if
+    // JS is disabled:
+    document.getElementById('qrcodebtn').removeAttribute('href');
+  }
+
+  var qrModalBtn = document.getElementById('qrcode-modal-btn');
+  if (qrModalBtn) {
+    document.getElementById('qrcode-modal-btn').addEventListener('click',
+      function() {
+        displayOrHide('qrcode');
+      }, false);
+  }
+};
diff --git a/bridgedb/https/templates/base.html b/bridgedb/https/templates/base.html
index 4a0b92d..8097c60 100644
--- a/bridgedb/https/templates/base.html
+++ b/bridgedb/https/templates/base.html
@@ -5,104 +5,96 @@
 
 <!DOCTYPE html>
 <html lang="${lang}">
-<head>
-<meta charset="utf-8">
-<title>BridgeDB</title>
-<meta name="viewport" content="width=device-width, initial-scale=1.0">
-<meta name="description" content="Bridge IP database">
-<meta name="author" content="The Tor Project">
+  <head>
+    <meta charset="utf-8">
+    <title>BridgeDB</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta name="description" content="Tor Bridges">
+    <meta name="author" content="The Tor Project">
 
-<!-- Le styles -->
-<link rel="stylesheet" href="/assets/css/main.css">
-<!--[if IE 7]>
-  <link rel="stylesheet" href="/assets/css/font-awesome-ie7.min.css">
-<![endif]-->
+    <link rel="stylesheet" href="/assets/css/bootstrap.min.css">
+    <link rel="stylesheet" href="/assets/css/font-awesome.min.css">
+    <link rel="stylesheet" href="/assets/css/main.css">
+    <!--[if IE 7]>
+    <link rel="stylesheet" href="/assets/css/font-awesome-ie7.min.css">
+    <![endif]-->
+    % if rtl:
+    <link rel="stylesheet" href="/assets/css/rtl.css">
+    % endif
 
-% if rtl:
-<style>
-span,p,h3,h4 {
-    text-align: right;
-    direction: rtl;
-}
-span {
-    float: right;
-}
-</style>
-% endif
+    <script type="text/javascript" src="/assets/js/bridges.js"></script>
+  </head>
 
-</head>
-<body style="background: url(/assets/tor-roots-blue.svg);
-             background-repeat: no-repeat;
-             background-attachment: scroll;
-             background-position: 2% 100px;
-             background-size: 23%;">
-  <div class="container-narrow">
-    <div class="masthead">
-      <div class="nav nav-tabs">
-        <div class="nav navbar-header">
-          <a  class="navbar-brand" href="../">BridgeDB</a>
-        </div>
-        <ul class="nav navbar-nav pull-right">
-          <li>
-            <a href="https://www.torproject.org">The Tor Project</a>
-          </li>
-        </ul>
-      </div>
-    </div>
+<body>
+  <div class="background-1">
+    <div class="background-2">
 
+      <div class="container-narrow">
+        <div class="masthead">
+          <div class="nav nav-tabs">
+            <div class="nav navbar-header">
+              <a  class="navbar-brand" href="../">BridgeDB</a>
+            </div>
+            <ul class="nav navbar-nav pull-right">
+              <li>
+                <a href="https://www.torproject.org">The Tor Project</a>
+              </li>
+            </ul>
+          </div>
+        </div>
 
 ${next.body(strings, rtl=rtl, lang=lang, **kwargs)}
 
+        <div class="faq">
+          <div class="row-fluid marketing">
 
-<div class="faq">
-  <div class="row-fluid marketing">
-
-    <h4>${_(strings.FAQ[0])}</h4>
-    <p>
-    ${_(strings.FAQ[1]) % \
-       ("""<a href="https://www.torproject.org/docs/bridges">""", "</a>")}
-    </p>
+            <h4>${_(strings.FAQ[0])}</h4>
+            <p>
+              ${_(strings.FAQ[1]) % \
+                 ("""<a href="https://www.torproject.org/docs/bridges">""", "</a>")}
+            </p>
 
-    <h4>${_(strings.OTHER_DISTRIBUTORS[0])}</h4>
-    <p>
-    ${_(strings.OTHER_DISTRIBUTORS[1]) % \
-       ("""<a href="mailto:bridges at torproject.org">bridges at torproject.org</a>""",
-        """<a href="https://riseup.net/">Riseup</a>""",
-        """<a href="https://mail.google.com/">Gmail</a>""",
-        """<a href="https://mail.yahoo.com/">Yahoo</a>""")}
-    </p>
+            <h4>${_(strings.OTHER_DISTRIBUTORS[0])}</h4>
+            <p>
+              ${_(strings.OTHER_DISTRIBUTORS[1]) % \
+                 ("""<a href="mailto:bridges at torproject.org">bridges at torproject.org</a>""",
+                  """<a href="https://riseup.net/">Riseup</a>""",
+                  """<a href="https://mail.google.com/">Gmail</a>""",
+                  """<a href="https://mail.yahoo.com/">Yahoo</a>""")}
+            </p>
 
-    <h4>${_(strings.HELP[0])}</h4>
-    <p>
-      ${_(strings.HELP[1]) % \
-         ("""<a href="mailto:help at rt.torproject.org">help at rt.torproject.org</a>""")}
-      ${_(strings.HELP[2])}
-    </p>
+            <h4>${_(strings.HELP[0])}</h4>
+            <p>
+              ${_(strings.HELP[1]) % \
+                 ("""<a href="mailto:help at rt.torproject.org">help at rt.torproject.org</a>""")}
+              ${_(strings.HELP[2])}
+            </p>
+          </div>
+        </div> <!-- end faq -->
 
-  </div>
-</div>
-<hr>
-
-<div class="footer">
+        <div class="footer footer-small">
+        <hr>
   <p>
     <a href="https://trac.torproject.org/projects/tor/newticket?component=BridgeDB&keywords=bridgedb-reportbug&cc=isis&owner=isis">
-      <i class="icon icon-large icon-bug"> ${_("Report a Bug")}</i></a>
+      <i class="icon icon-large icon-bug"><span id="footer-bugs"> ${_("Report a Bug")}</span></i></a>
       ·  
     <a href="https://gitweb.torproject.org/bridgedb.git">
-      <i class="icon icon-large icon-code"> ${_("Source Code")}</i></a>
+      <i class="icon icon-large icon-code"><span id="footer-code"> ${_("Source Code")}</span></i></a>
       ·  
     <a href="https://gitweb.torproject.org/bridgedb.git/tree/CHANGELOG">
-      <i class="icon icon-large icon-rocket"> ${_("Changelog")}</i></a>
+      <i class="icon icon-large icon-rocket"><span id="footer-changelog"> ${_("Changelog")}</span></i></a>
       ·  
     <a href="mailto:help at rt.torproject.org">
-      <i class="icon icon-large icon-envelope"> ${_("Contact")}</i></a>
+      <i class="icon icon-large icon-envelope"><span id="footer-contact"> ${_("Contact")}</span></i></a>
       ·  
-    <a href="../keys"><i class="icon icon-large icon-key"> ${_("Public Keys")}</i></a>
+    <a href="../keys"><i class="icon icon-large icon-key"><span id="footer-keys"> ${_("Public Keys")}</span></i></a>
   </p>
   <br />
   <p>© The Tor Project</p>
-</div>
 
-</div> <!-- /container -->
+        </div> <!-- end footer -->
+      </div> <!-- /container -->
+    </div>
+  </div>
 </body>
 </html>
diff --git a/bridgedb/https/templates/bridges.html b/bridgedb/https/templates/bridges.html
index b5ac544..076f930 100644
--- a/bridgedb/https/templates/bridges.html
+++ b/bridgedb/https/templates/bridges.html
@@ -3,66 +3,19 @@
 <%inherit file="base.html"/>
 <%page args="strings, rtl=False, lang='en', answer=0, qrcode=0, **kwargs"/>
 
-  </div>
 </div>
 
-<script type="text/javascript">
-  // Takes one argument, `element`, which should be a string specifying the id
-  // of the element whose text should be selected.
-  function selectText(element) {
-    try {
-      var doc = document;
-      text = doc.getElementById(element);
-      var range;
-      var selection;
-
-      if (doc.body.createTextRange) {
-        range = document.body.createTextRange();
-        range.moveToElementText(text);
-        range.select();
-      } else if (window.getSelection) {
-        selection = window.getSelection();
-        range = document.createRange();
-        range.selectNodeContents(text);
-        selection.removeAllRanges();
-        selection.addRange(range);
-      }
-    } catch (e) {
-      window.alert(e);
-    }
-  }
-
-  function displayOrHide(element) {
-    try {
-      e = document.getElementById(element);
-
-      if (e.style.display === 'none') {
-        document.getElementById(element).style.display = 'block';
-      } else if (e.style.display === 'block') {
-        document.getElementById(element).style.display = 'none';
-      }
-    } catch (e) {
-      window.alert(e);
-    }
-  }
-</script>
-
 <div class="container-narrow">
   <div class="container-fluid">
 
 % if answer:
-    <div class="container-fluid"
-         style="width: 98%; align: center; margin: auto;">
-      <div class="container-fluid"
-           style="padding: 2%">
+    <div class="container-fluid container-fluid-outer-98">
+      <div class="container-fluid container-fluid-inner-2">
         <h2>${_(strings.BRIDGES[0])}</h2>
       </div>
     </div>
 
-    <div class="container-fluid"
-         style="width: auto; align: center; margin: auto;
-                position: relative; left: 0%;"
-         onclick="selectText('bridgelines')">
+    <div class="container-fluid" id="container-bridges">
       <div class="row" id="bridgesrow1">
         <div class="col col-lg-12">
           <div class="bridge-lines" id="bridgelines">
@@ -75,39 +28,35 @@ ${bridgeline | h,trim}<br />
       </div>
     </div>
 
-    <div class="container-fluid"
-         style="width: 100%; align: center; margin: auto;">
-      <div class="row" id="bridgesrow2" style="text-align: right; padding-right: 7%;">
+    <div class="container-fluid container-fluid-outer">
+      <div class="row" id="bridgesrow2">
         <button class="btn btn-primary disabled" id="selectbtn"
-                onclick="selectText('bridgelines')"
-                title="Select all bridge lines">
+                title="Select all bridge lines" aria-disabled="true">
           <i class="icon icon-2x icon-paste"></i>   ${_("""Select All""")}
         </button>
 % if qrcode:
         <a class="btn btn-primary" type="button" id="qrcodebtn"
-           href="${qrcode}" title="Show QRCode for bridge lines"
-           onclick="displayOrHide('qrcode')">
+           href="${qrcode}" title="Show QRCode for bridge lines">
           <i class="icon icon-2x icon-qrcode"></i>   ${_("""Show QRCode""")}
         </a>
 % endif
       </div>
 
-      <div class="modal" id="qrcode" style="display: none;">
-        <div class="modal-dialog modal-sm" style="width: 400px;">
+      <div class="modal hidden" id="qrcode" aria-hidden="true">
+        <div class="modal-dialog modal-sm">
           <div class="modal-content">
             <div class="modal-header">
-              <button type="button" class="close" aria-hidden="true"
-                      onclick="displayOrHide('qrcode')">
+              <button type="button" class="close" id="qrcode-modal-btn">
                 ×
               </button>
               <h4 class="modal-title">${_("""QRCode for your bridge lines""")}</h4>
             </div>
             <div class="modal-body">
 % if qrcode:
-              <p style="text-align: center;">
-                <img width="350" height="350"
-                     title="QRCode for your bridge lines from BridgeDB"
-                     src="${qrcode}" />
+              <p id="qrcode-para">
+                <img title="QRCode for your bridge lines from BridgeDB"
+                     src="${qrcode}"
+                     alt="" />
               </p>
 % else:
               <p class="text-danger">
@@ -131,17 +80,15 @@ ${_("""This QRCode contains your bridge lines. Scan it with a QRCode """ \
     </div>
 
     <br />
-    <br />
 
-<div class="container-fluid"
-     style="width: 100%; align: center; margin: auto;">
+<div class="container-fluid container-fluid-outer-96">
   <div class="panel panel-primary">
     <div class="panel-heading">
       <h3 class="panel-title">${_(strings.HOWTO_TBB[0])}</h3>
     </div>
     <br />
 
-    <div class="container-fluid" id="howto" style="align-content: left;">
+    <div class="container-fluid" id="howto">
       <p>
         ${_(strings.HOWTO_TBB[1]) % \
            ("""<a href="https://www.torproject.org/projects/torbrowser.html"
@@ -165,9 +112,9 @@ ${_("""This QRCode contains your bridge lines. Scan it with a QRCode """ \
 </div>
 
 % else:
-<div class="bs-component" style="width: 80%; margin: auto;">
+<div class="bs-component" id="uh-oh-spaghettios">
   <div class="alert alert-dismissable alert-danger">
-    <p style="text-align: center; font-size: 115%;">
+    <p>
       <br />
       <strong>
         <em class="primary">
@@ -180,7 +127,7 @@ ${_("""Uh oh, spaghettios!""")}
       </strong>
       <br />
     </p>
-    <p style="text-align: center;">
+    <p>
       ${_("""There currently aren't any bridges available...""")}
       ${_(""" Perhaps you should try %s going back %s and choosing a""" \
           """ different bridge type!""") % \
@@ -190,14 +137,3 @@ ${_("""Uh oh, spaghettios!""")}
 </div>
 <br />
 % endif
-
-<script type="text/javascript">
-  // Make the 'Select All' button clickable:
-  document.getElementById('selectbtn').className = "btn btn-primary";
-
-  // Remove the href attribute which opens the QRCode image as a data URL if
-  // JS is disabled:
-  document.getElementById('qrcodebtn').removeAttribute('href');
-</script>
-
-<hr />
diff --git a/bridgedb/https/templates/captcha.html b/bridgedb/https/templates/captcha.html
index ab605e9..33c1d45 100644
--- a/bridgedb/https/templates/captcha.html
+++ b/bridgedb/https/templates/captcha.html
@@ -3,61 +3,47 @@
 <%inherit file="base.html"/>
 <%page args="strings, rtl=False, lang='en', imgstr=0, captcha_challenge=0, **kwargs"/>
 
-<div class="container-narrow"
-     id="captchaSubmissionContainer"
-     style="width: 90%; align: center; margin: auto;">
-  <div class="container-fluid"
-       style="width: 100%; align: center; padding: 5%">
-    <div class="box" style="padding: 5% 15% 5% 15%;">
-      <p style="align: center;">
-        <img width="400" height="125"
-             alt="${_(strings.CAPTCHA[0])}"
-             src="${imgstr}" />
-      </p>
+<div class="container-narrow" id="captcha-submission-container">
+  <div class="container-fluid container-fluid-inner-5">
+    <div class="box" id="captcha-box">
+      <img alt="${_(strings.CAPTCHA[0])}" src="${imgstr}" />
 
-    <div class="box"
-         style="align: center; width: 50% margin: auto;">
-      <div class="container-fluid"
-           style="width: 98%; align: center; padding: 1%;">
-        <form class="bs-component"
-              id="captchaSubmission"
-              action=""
-              method="POST">
-          <fieldset>
-            <div class="form-group">
-                 <!--style="width: 100%; align: center;">-->
-              <div class="input-group" style="height: 60%;">
-                <input type="hidden"
-                       form="captchaSubmission"
-                       name="captcha_challenge_field"
-                       id="captcha_challenge_field"
-                       value="${challenge_field}"></input>
-                <input class="form-control"
-                       form="captchaSubmission"
-                       name="captcha_response_field"
-                       id="captcha_response_field"
-                       value=""
-                       autocomplete="off"
-                       type="text"
-                       placeholder="${_(strings.CAPTCHA[1])}"
-                       accesskey="t" autofocus ></input>
-                <span class="input-group-btn">
-                  <button class="btn btn-primary"
-                          form="captchaSubmission"
-                          type="submit"
-                          name="submit"
-                          value="submit"
-                          accesskey="s">
-                    <i class="icon-level-down icon-rotate-90"></i>
-                  </button>
-                </span>
-              </div>
+      <form class="bs-component"
+            id="captchaSubmission"
+            action=""
+            method="POST">
+        <fieldset>
+          <div class="form-group">
+            <div class="input-group" id="captcha-submission-group">
+              <input type="hidden"
+                     form="captchaSubmission"
+                     name="captcha_challenge_field"
+                     id="captcha_challenge_field"
+                     value="${challenge_field}"></input>
+              <input class="form-control"
+                     form="captchaSubmission"
+                     name="captcha_response_field"
+                     id="captcha_response_field"
+                     value=""
+                     autocomplete="off"
+                     type="text"
+                     placeholder="${_(strings.CAPTCHA[1])}"
+                     accesskey="t" autofocus ></input>
+              <span class="input-group-btn">
+                <button class="btn btn-primary"
+                        form="captchaSubmission"
+                        type="submit"
+                        name="submit"
+                        id="captcha-submit-button"
+                        value="submit"
+                        accesskey="s">
+                  <i class="icon-level-down icon-rotate-90"></i>
+                </button>
+              </span>
             </div>
-          </fieldset>
-        </form>
-      </div>
-    </div>
+          </div>
+        </fieldset>
+      </form>
     </div>
   </div>
 </div>
-<hr />
diff --git a/bridgedb/https/templates/howto.html b/bridgedb/https/templates/howto.html
index b10f1b2..24e4980 100644
--- a/bridgedb/https/templates/howto.html
+++ b/bridgedb/https/templates/howto.html
@@ -3,18 +3,16 @@
 <%inherit file="base.html"/>
 <%page args="strings, rtl=False, lang='en', **kwargs"/>
 
-<div class="container-fluid"
-     style="width: 98%; align: center; margin: auto;">
-<div class="container-fluid"
-     style="width: 95%; align: center;
-            padding-top: 10%; padding-left: 5%; padding-right: 5%;">
+<br />
+
+<div class="container-fluid container-fluid-outer-96">
   <div class="panel panel-primary">
     <div class="panel-heading">
       <h3 class="panel-title">${_(strings.HOWTO_TBB[0])}</h3>
     </div>
     <br />
 
-    <div class="container-fluid" id="howto" style="align-content: left;">
+    <div class="container-fluid" id="howto">
       <p>
         ${_(strings.HOWTO_TBB[1]) % \
            ("""<a href="https://www.torproject.org/projects/torbrowser.html"
@@ -36,4 +34,3 @@
     </div>
   </div>
 </div>
-</div>
diff --git a/bridgedb/https/templates/index.html b/bridgedb/https/templates/index.html
index 469b27a..269b2ae 100644
--- a/bridgedb/https/templates/index.html
+++ b/bridgedb/https/templates/index.html
@@ -4,12 +4,12 @@
 <%page args="strings, rtl=False, lang='en', **kwargs"/>
 
 <div class="main-steps">
-<div class="step row">
+<div class="step row" id="step-1">
   <div class="bdb_span7 step-text">
     <span class="lead">
       <span class="step-title">
         ${_("Step %s1%s") % ("""<u>""", """</u>""")}</span>
-      <span style="margin-left: 20px; margin-right: 20px;">
+      <span class="step-text">
         ${_("Download %s Tor Browser %s") % \
            ("""<a href="https://www.torproject.org/projects/torbrowser.html"
                   target="_blank" accesskey="1">""",
@@ -18,23 +18,23 @@
   </div>
 </div>
 
-<div class="step row">
+<div class="step row" id="step-2">
   <div class="bdb_span7 step-text">
     <span class="lead">
       <span class="step-title">
         ${_("Step %s2%s") % ("""<u>""", """</u>""")}</span>
-      <span style="margin-left: 20px; margin-right: 20px;">
+      <span class="step-text">
         ${_("Get %s bridges %s") % ("""<a href="/options" accesskey="2">""", "</a>")}</span>
     </span>
   </div>
 </div>
 
-<div class="step row">
+<div class="step row" id="step-3">
   <div class="bdb_span7 step-text">
     <span class="lead">
       <span class="step-title">
         ${_("Step %s3%s") % ("""<u>""", """</u>""")}</span>
-      <span style="margin-left: 20px; margin-right: 20px;">
+      <span class="step-text">
         ${_("""Now %s add the bridges to Tor Browser %s""") % \
            ("""<a href="/howto" accesskey="3">""",
             """</a>""")}</span>
diff --git a/bridgedb/https/templates/options.html b/bridgedb/https/templates/options.html
index 9486199..040d523 100644
--- a/bridgedb/https/templates/options.html
+++ b/bridgedb/https/templates/options.html
@@ -3,50 +3,49 @@
 <%inherit file="base.html"/>
 <%page args="strings, rtl=False, lang='en', **kwargs"/>
 
-<div class="container-fluid"
-     style="width: 96%; align: center; margin: 2%">
-  <div class="container-fluid" style="padding: 2%">
-  <p>
-    <h2>${_(strings.BRIDGES[1])}</h2>
-  </p>
-  </div>
-  <div class="container-fluid"
-       style="width: 100%; align: center; padding: 2%;">
-    <p>
-      ${_(strings.WELCOME[0]) % \
-         ("""<a href="https://www.torproject.org/docs/pluggable-transports.html">""",
-          """</a>""")}
-    </p>
-    <p>
-      ${_(strings.WELCOME[1])}
-    </p>
+<div class="container-fluid container-fluid-outer-96">
+  <!--<div class="container-fluid step-semi-transparent">-->
+    <div class="container-fluid container-fluid-inner">
+      <p>
+        <h3>${_(strings.BRIDGES[1])}</h3>
+      </p>
+    <!--</div>-->
+    <!--<div class="container-fluid container-fluid-outer-100 container-fluid-inner">-->
+      <p>
+        ${_(strings.WELCOME[0]) % \
+           ("""<a href="https://www.torproject.org/docs/pluggable-transports.html">""",
+            """</a>""")}
+      </p>
+      <p>
+        ${_(strings.WELCOME[1])}
+      </p>
 ## The '&#8212' char is a literal emdash ('―'), but it's also XML parseable.
-    <p>
-      ${_(strings.WELCOME[2]) % ("&#8212", "&#8212")}
-    </p>
-    <div class="container-fluid" style="align: center: margin: 2%;">
-      <div style="align: center; padding: 5%;">
-        <p class="bs-component">
-          <a href="./bridges">
-            <button class="btn btn-success btn-lg btn-block"
-                    type="button"
-                    accesskey="j">
+      <p>
+        ${_(strings.WELCOME[2]) % ("&#8212", "&#8212")}
+      </p>
+      <div class="container-fluid container-fluid-outer">
+        <div class="container-fluid-inner-5">
+          <p class="bs-component">
+            <a href="./bridges">
+              <button class="btn btn-success btn-lg btn-block"
+                      id="just-give-me-bridges-btn"
+                      type="button"
+                      accesskey="j">
 ## TRANSLATORS: Please make sure the '%s' surrounding single letters at the
 ##              beginning of words are present in your final translation. Thanks!
 ##              (These are used to insert HTML5 underlining tags, to mark accesskeys
 ##              for disabled users.)
-            ${_("""%sJ%sust give me bridges!""") % ("""<u>""", """</u>""")}
-            </button>
-          </a>
-        </p>
+                ${_("""%sJ%sust give me bridges!""") % ("""<u>""", """</u>""")}
+              </button>
+            </a>
+          </p>
+        </div>
       </div>
     </div>
-  </div>
-</div>
+  <!--</div>-->
 
 <!-- BEGIN "Advanced Options" panel for bridge type -->
-<div class="container-fluid"
-     style="width: 96%; align: center; margin-left: 2%; margin-right: 2%;">
+<div class="container-fluid container-fluid-outer-96-lr">
   <div class="panel panel-primary">
     <div class="panel-heading">
       <h3 class="panel-title">${_("""Advanced Options""")}</h3>
@@ -55,8 +54,8 @@
     <!-- BEGIN bridge options selection form -->
     <form class="form-horizontal" id="advancedOptions" action="bridges" method="GET">
       <fieldset>
-        <div class="container-fluid" id="instructions" style="align-content: left;">
-          <legend style="font-size: 112%">
+        <div class="container-fluid" id="instructions">
+          <legend id="advanced-options-legend">
             <br />
             <p>${_(strings.OPTIONS[0])}</p>
           </legend>
@@ -64,20 +63,19 @@
 
         <div class="container-fluid">
           <!-- BEGIN first options row -->
-          <div class="row" style="width: 98%; height: inherit; margin: auto;">
+          <div class="row" id="options-row-1">
 
             <!-- BEGIN left column, first row -->
-            <div class="container-fluid col-lg-2"
-                 style="float: left; width: 50%; height: inherit;">
-              <div class="step" style="height: inherit;">
+            <div class="container-fluid col-lg-2" id="options-row-1-col-1">
+              <div class="step" id="options-row-1-col-1-step">
                 <div class="form-group">
                   <label class="control-label"
-                         for="transport"
-                          style="text-align: inherit;">
+                         id="options-transport-label"
+                         for="transport">
                     ${_(strings.OPTIONS[2]) % ("Pluggable <u>T</u>ransport")}
                   </label>
                   <div class="container-fluid col-lg-4">
-                    <div class="btn-group" style="float: left; padding: 2%;">
+                    <div class="btn-group" id="options-transport-btn-group">
                       <select class="btn btn-primary btn-sm dropdown"
                               form="advancedOptions"
                               id="transport"
@@ -103,21 +101,17 @@
             </div> <!-- END left column, first row -->
 
             <!-- BEGIN right column, first row -->
-            <div class="container-fluid col-lg-2"
-                 style="float: right; width: 50%; height: inherit;">
-              <div class="step"
-                   style="height: inherit; margin-right: 1%;">
+            <div class="container-fluid col-lg-2" id="options-row-1-col-2">
+              <div class="step" id="options-row-1-col-2-step">
                 <div class="form-group">
                   <label class="control-label"
-                         for="ipv6"
-                         style="text-align: inherit;">
+                         id="options-ipv6-label"
+                         for="ipv6">
                     ${_(strings.OPTIONS[1])}
                   </label>
                   <div class="container-fluid col-lg-4">
-                    <div class="checkbox"
-                         style="float: left;">
-                      <div class="input-group"
-                           style="float: left; padding: 2%;">
+                    <div class="checkbox" id="options-ipv6-checkbox">
+                      <div class="input-group" id="options-ipv6-input-group">
                         <input name="ipv6"
                                id="ipv6"
                                form="advancedOptions"
@@ -140,25 +134,21 @@
       </fieldset>
 
       <!-- BEGIN second options row -->
-      <div class="row"
-           style="width: 98%; height: inherit;
-                  margin-left: auto; margin-right: auto; padding: 2%;">
-        <div class="container-fluid col-lg-2"
-             style="width: 50%; text-align: center;">
-          <p>
-            <button class="btn btn-primary btn-lg btn-block"
-                    accesskey="g">
+      <!--<div class="row" id="options-row-2">
+        <div class="container-fluid col-lg-2" id="options-row-2-col-1">-->
+      <div class="container-fluid container-fluid-outer-60-lr">
+        <div class="container-fluid container-fluid-inner-5">
+          <button class="btn btn-primary btn-lg btn-block"
+                  id="get-bridges-btn"
+                  accesskey="g">
 ## TRANSLATORS: Please make sure the '%s' surrounding single letters at the
 ##              beginning of words are present in your final translation. Thanks!
 ## TRANSLATORS: Please do NOT translate the word "bridge"!
-              ${_("""%sG%set Bridges""") % ("<u>", "</u>")}
-            </button>
-          </p>
+            ${_("""%sG%set Bridges""") % ("<u>", "</u>")}
+          </button>
         </div>
       </div>
     </form> <!-- END bridge options selection form -->
   </div> <!-- END advanced options panel (a.k.a. the lined boxy thing --
            -- surrounding the options)                                -->
 </div>
-<br />
-<hr>
diff --git a/scripts/setup-tests b/scripts/setup-tests
index e82576f..ec915d8 100755
--- a/scripts/setup-tests
+++ b/scripts/setup-tests
@@ -23,6 +23,7 @@ sed -r -i -e "s/(EMAIL_SMTP_PORT = )([1-9]{2,5})/\12525/" run/bridgedb.conf
 # won't recognize our self-signed certificate:
 sed -r -i -e "s/(HTTP_UNENCRYPTED_BIND_IP = )(None)/\1'127.0.0.1'/" run/bridgedb.conf
 sed -r -i -e "s/(HTTP_UNENCRYPTED_PORT = )(None)/\16788/" run/bridgedb.conf
+sed -r -i -e "s/(SERVER_PUBLIC_FQDN = )(.*)/\1'127.0.0.1:6788'/" run/bridgedb.conf
 leekspin -n 100
 cp -t run networkstatus-bridges cached-extrainfo* bridge-descriptors
 ./scripts/make-ssl-cert
diff --git a/setup.py b/setup.py
index dee9534..0402391 100644
--- a/setup.py
+++ b/setup.py
@@ -173,7 +173,8 @@ def get_template_files():
                         'assets/font/*.woff',
                         'assets/font/*.ttf',
                         'assets/font/*.svg',
-                        'assets/font/*.eot']
+                        'assets/font/*.eot',
+                        'assets/js/*.js']
     template_files = []
 
     for include_pattern in include_patterns:
diff --git a/test/https_helpers.py b/test/https_helpers.py
index 3fb4887..cc1ddfa 100644
--- a/test/https_helpers.py
+++ b/test/https_helpers.py
@@ -42,6 +42,9 @@ GIMP_CAPTCHA_ENABLED = True
 GIMP_CAPTCHA_DIR = 'captchas'
 GIMP_CAPTCHA_HMAC_KEYFILE = 'captcha_hmac_key'
 GIMP_CAPTCHA_RSA_KEYFILE = 'captcha_rsa_key'
+CSP_ENABLED = True
+CSP_REPORT_ONLY = True
+CSP_INCLUDE_SELF = True
 
 TEST_CONFIG_FILE = io.StringIO(unicode("""\
 SERVER_PUBLIC_FQDN = %r
@@ -66,6 +69,9 @@ GIMP_CAPTCHA_ENABLED = %r
 GIMP_CAPTCHA_DIR = %r
 GIMP_CAPTCHA_HMAC_KEYFILE = %r
 GIMP_CAPTCHA_RSA_KEYFILE = %r
+CSP_ENABLED = %r
+CSP_REPORT_ONLY = %r
+CSP_INCLUDE_SELF = %r
 """ % (SERVER_PUBLIC_FQDN,
        SERVER_PUBLIC_EXTERNAL_IP,
        HTTPS_DIST,
@@ -87,7 +93,10 @@ GIMP_CAPTCHA_RSA_KEYFILE = %r
        GIMP_CAPTCHA_ENABLED,
        GIMP_CAPTCHA_DIR,
        GIMP_CAPTCHA_HMAC_KEYFILE,
-       GIMP_CAPTCHA_RSA_KEYFILE)))
+       GIMP_CAPTCHA_RSA_KEYFILE,
+       CSP_ENABLED,
+       CSP_REPORT_ONLY,
+       CSP_INCLUDE_SELF)))
 
 
 def _createConfig(configFile=TEST_CONFIG_FILE):
@@ -119,6 +128,27 @@ class DummyRequest(requesthelper.DummyRequest):
     def __init__(self, *args, **kwargs):
         requesthelper.DummyRequest.__init__(self, *args, **kwargs)
         self.redirect = self._redirect(self)
+        self.content = io.StringIO()
+
+    def writeContent(self, data):
+        """Add some **data** to the faked body of this request.
+
+        This is useful when testing how servers handle content from POST
+        requests.
+
+        .. warn: Calling this method multiple times will overwrite any data
+            previously written.
+
+        :param str data: Some data to put in the "body" (i.e. the
+            :attr:`content`) of this request.
+        """
+        try:
+            self.content.write(type(u'')(data))
+        except UnicodeDecodeError:
+            self.content.write(type(u'')(data, 'utf-8'))
+        finally:
+            self.content.flush()
+            self.content.seek(0)
 
     def URLPath(self):
         """Fake the missing Request.URLPath too."""
diff --git a/test/test_https.py b/test/test_https.py
index 8a5754f..2a12eee 100644
--- a/test/test_https.py
+++ b/test/test_https.py
@@ -150,6 +150,28 @@ class HTTPTests(unittest.TestCase):
                                   % (fieldsPerBridge, bridge))
         return bridges
 
+    def test_content_security_policy(self):
+        """Check that the HTTP Content-Security-Policy header is set."""
+        if os.environ.get("CI"):
+            if not self.pid or not processExists(self.pid):
+                raise FailTest("Could not start BridgeDB process on CI server!")
+        else:
+            raise SkipTest(("The mechanize tests cannot handle self-signed  "
+                            "TLS certificates, and thus require opening "
+                            "another port for running a plaintext HTTP-only "
+                            "BridgeDB webserver. Because of this, these tests "
+                            "are only run on CI servers."))
+
+        self.br = mechanize.Browser()
+        self.br.set_handle_robots(False)
+        self.br.set_debug_http(True)
+        self.br.open(HTTP_ROOT)
+
+        headers = ''.join(self.br.response().info().headers)
+
+        self.assertIn("Content-Security-Policy", headers)
+        self.assertIn("default-src 'none';", headers)
+
     def test_get_obfs2_ipv4(self):
         if os.environ.get("CI"):
             if not self.pid or not processExists(self.pid):
diff --git a/test/test_https_server.py b/test/test_https_server.py
index 708f7e3..020b050 100644
--- a/test/test_https_server.py
+++ b/test/test_https_server.py
@@ -43,6 +43,34 @@ logging.disable(50)
 #server.logging.getLogger().setLevel(10)
 
 
+class SetFQDNTests(unittest.TestCase):
+    """Tests for :func:`bridgedb.https.server.setFQDN` and
+    :func:`bridgedb.https.server.setFQDN`.
+    """
+
+    def setUp(self):
+        self.originalFQDN = server.SERVER_PUBLIC_FQDN
+
+    def tearDown(self):
+        server.SERVER_PUBLIC_FQDN = self.originalFQDN
+
+    def test_setFQDN_https(self):
+        """Calling ``server.setFQDN([…], https=True)`` should prepend
+        ``"https://"`` to the module :data:`server.SERVER_PUBLIC_FQDN`
+        variable.
+        """
+        server.setFQDN('example.com', https=True)
+        self.assertEqual(server.SERVER_PUBLIC_FQDN, "https://example.com")
+
+    def test_setFQDN_http(self):
+        """Calling ``server.setFQDN([…], https=False)`` should not prepend
+        anything at all to the module :data:`server.SERVER_PUBLIC_FQDN`
+        variable.
+        """
+        server.setFQDN('example.com', https=False)
+        self.assertEqual(server.SERVER_PUBLIC_FQDN, "example.com")
+
+
 class GetClientIPTests(unittest.TestCase):
     """Tests for :func:`bridgedb.https.server.getClientIP`."""
 
@@ -96,6 +124,78 @@ class ReplaceErrorPageTests(unittest.TestCase):
         self.assertNotSubstring("vegan gümmibären", errorPage)
 
 
+class CSPResourceTests(unittest.TestCase):
+    """Tests for :class:`bridgedb.HTTPServer.CSPResource`."""
+
+    def setUp(self):
+        self.pagename = b'foo.html'
+        self.request = DummyRequest([self.pagename])
+        self.request.method = b'GET'
+
+        server.setFQDN('bridges.torproject.org')
+
+    def test_CSPResource_setCSPHeader(self):
+        """Setting the CSP header on a request should work out just peachy,
+        like no errors or other bad stuff happening.
+        """
+        resource = server.CSPResource()
+        resource.setCSPHeader(self.request)
+
+    def test_render_POST_ascii(self):
+        """Calling ``CSPResource.render_POST()`` should log whatever stuff was
+        sent in the body of the POST request.
+        """
+        self.request.method = b'POST'
+        self.request.writeContent('lah dee dah')
+        self.request.client = requesthelper.IPv4Address('TCP', '3.3.3.3', 443)
+
+        resource = server.CSPResource()
+        page = resource.render_POST(self.request)
+
+        self.assertIn('<html>', str(page))
+
+    def test_render_POST_no_client_IP(self):
+        """Calling ``CSPResource.render_POST()`` should log whatever stuff was
+        sent in the body of the POST request, regardless of whether we were
+        able to determine the client's IP address.
+        """
+        self.request.method = b'POST'
+        self.request.writeContent('lah dee dah')
+
+        resource = server.CSPResource()
+        page = resource.render_POST(self.request)
+
+        self.assertIn('<html>', str(page))
+
+    def test_render_POST_unicode(self):
+        """Calling ``CSPResource.render_POST()`` should log whatever stuff was
+        sent in the body of the POST request, even if it's unicode.
+        """
+        self.request.method = b'POST'
+        self.request.writeContent(
+            ('南京大屠杀是中国抗日战争初期侵华日军在中华民国首都南京犯下的'
+             '大規模屠殺、強姦以及纵火、抢劫等战争罪行与反人类罪行。'))
+        self.request.client = requesthelper.IPv4Address('TCP', '3.3.3.3', 443)
+
+        resource = server.CSPResource()
+        page = resource.render_POST(self.request)
+
+        self.assertIn('<html>', str(page))
+
+    def test_render_POST_weird_request(self):
+        """Calling ``CSPResource.render_POST()`` without a strange content
+        object which doesn't have a ``content`` attribute should trigger the
+        ``except Exception`` clause.
+        """
+        self.request.method = b'GET'
+        del self.request.content
+
+        resource = server.CSPResource()
+        page = resource.render_POST(self.request)
+
+        self.assertIn('<html>', str(page))
+
+
 class IndexResourceTests(unittest.TestCase):
     """Test for :class:`bridgedb.https.server.IndexResource`."""
 
@@ -687,7 +787,7 @@ class BridgesResourceTests(unittest.TestCase):
         request.headers.update({'accept-language': 'ar,en,en_US,'})
 
         page = self.bridgesResource.render(request)
-        self.assertSubstring("direction: rtl", page)
+        self.assertSubstring("rtl.css", page)
         self.assertSubstring(
             # "I need an alternative way to get bridges!"
             "أحتاج إلى وسيلة بديلة للحصول على bridges", page)
@@ -710,7 +810,7 @@ class BridgesResourceTests(unittest.TestCase):
         request.args.update({'transport': ['obfs3']})
 
         page = self.bridgesResource.render(request)
-        self.assertSubstring("direction: rtl", page)
+        self.assertSubstring("rtl.css", page)
         self.assertSubstring(
             # "How to use the above bridge lines" (since there should be
             # bridges in this response, we don't tell them about alternative
@@ -787,7 +887,7 @@ class OptionsResourceTests(unittest.TestCase):
         request.args.update({'transport': ['obfs2']})
 
         page = self.optionsResource.render(request)
-        self.assertSubstring("direction: rtl", page)
+        self.assertSubstring("rtl.css", page)
         self.assertSubstring("מהם גשרים?", page)
 
 
@@ -837,3 +937,27 @@ class HTTPSServerServiceTests(unittest.TestCase):
         config = self.config
         config.HTTPS_ROTATION_PERIOD = None
         server.addWebServer(config, self.distributor)
+
+    def test_addWebServer_CSP_ENABLED_False(self):
+        """Call :func:`bridgedb.https.server.addWebServer` with
+        ``CSP_ENABLED=False`` to test startup.
+        """
+        config = self.config
+        config.CSP_ENABLED = False
+        server.addWebServer(config, self.distributor)
+
+    def test_addWebServer_CSP_REPORT_ONLY_False(self):
+        """Call :func:`bridgedb.https.server.addWebServer` with
+        ``CSP_REPORT_ONLY=False`` to test startup.
+        """
+        config = self.config
+        config.CSP_REPORT_ONLY = False
+        server.addWebServer(config, self.distributor)
+
+    def test_addWebServer_CSP_INCLUDE_SELF_False(self):
+        """Call :func:`bridgedb.https.server.addWebServer` with
+        ``CSP_INCLUDE_SELF=False`` to test startup.
+        """
+        config = self.config
+        config.CSP_INCLUDE_SELF = False
+        server.addWebServer(config, self.distributor)





More information about the tor-commits mailing list