1 # -*- coding: utf-8 ; test-case-name: bridgedb.test.test_https_server -*-
3 # This file is part of BridgeDB, a Tor bridge distribution system.
5 # :authors: please see included AUTHORS file
6 # :copyright: (c) 2007-2017, The Tor Project, Inc.
7 # (c) 2013-2017, Isis Lovecruft
8 # :license: see LICENSE for licensing information
11 .. py:module:: bridgedb.distributors.https.server
12 :synopsis: Servers which interface with clients and distribute bridges
15 bridgedb.distributors.https.server
18 Servers which interface with clients and distribute bridges over HTTP(S).
20 .. inheritance-diagram:: TranslatedTemplateResource IndexResource OptionsResource HowtoResource CaptchaProtectedResource GimpCaptchaProtectedResource ReCaptchaProtectedResource BridgesResource
33 from functools
import partial
35 from ipaddr
import IPv4Address
37 import mako
.exceptions
38 from mako
.template
import Template
39 from mako
.lookup
import TemplateLookup
43 from twisted
.internet
import defer
44 from twisted
.internet
import reactor
45 from twisted
.internet
import task
46 from twisted
.internet
.error
import CannotListenError
47 from twisted
.web
import resource
48 from twisted
.web
import static
49 from twisted
.web
.server
import NOT_DONE_YET
50 from twisted
.web
.server
import Site
51 from twisted
.web
.util
import redirectTo
53 from bridgedb
import captcha
54 from bridgedb
import crypto
55 from bridgedb
import strings
56 from bridgedb
import translations
57 from bridgedb
import txrecaptcha
58 from bridgedb
import metrics
59 from bridgedb
import antibot
60 from bridgedb
.distributors
.common
.http
import setFQDN
61 from bridgedb
.distributors
.common
.http
import getFQDN
62 from bridgedb
.distributors
.common
.http
import getClientIP
63 from bridgedb
.distributors
.https
.request
import HTTPSBridgeRequest
64 from bridgedb
.parse
import headers
65 from bridgedb
.parse
.addr
import isIPAddress
66 from bridgedb
.qrcodes
import generateQR
67 from bridgedb
.safelog
import logSafely
68 from bridgedb
.schedule
import Unscheduled
69 from bridgedb
.schedule
import ScheduledInterval
70 from bridgedb
.util
import replaceControlChars
73 #: The path to the HTTPS distributor's web templates. (Should be the
74 #: "templates" directory in the same directory as this file.)
75 TEMPLATE_DIR
= os
.path
.join(os
.path
.abspath(os
.path
.dirname(__file__
)), 'templates')
77 # Setting `filesystem_checks` to False is recommended for production servers,
78 # due to potential speed increases. This means that the atimes of the Mako
79 # template files aren't rechecked every time the template is requested
80 # (otherwise, if they are checked, and the atime is newer, the template is
81 # recompiled). `collection_size` sets the number of compiled templates which
82 # are cached before the least recently used ones are removed. See:
83 # http://docs.makotemplates.org/en/latest/usage.html#using-templatelookup
84 lookup
= TemplateLookup(directories
=[TEMPLATE_DIR
],
85 output_encoding
='utf-8',
86 filesystem_checks
=False,
88 logging
.debug("Set template root to %s" % TEMPLATE_DIR
)
90 #: A list of supported language tuples. Use getSortedLangList() to read this variable.
93 # We use our metrics singleton to keep track of BridgeDB metrics such as
94 # "number of failed HTTPS bridge requests."
95 metrix
= metrics
.HTTPSMetrics()
98 def stringifyRequestArgs(args
):
99 """Turn the given HTTP request arguments from bytes to str.
101 :param dict args: A dictionary of request arguments.
103 :returns: A dictionary of request arguments.
106 # Convert all key/value pairs from bytes to str.
108 for arg
, values
in args
.items():
109 arg
= arg
if isinstance(arg
, str) else arg
.decode("utf-8")
110 values
= [value
.decode("utf-8") if isinstance(value
, bytes
)
111 else value
for value
in values
]
112 str_args
[arg
] = values
117 def replaceErrorPage(request
, error
, template_name
=None, html
=True):
118 """Create a general error page for displaying in place of tracebacks.
120 Log the error to BridgeDB's logger, and then display a very plain "Sorry!
121 Something went wrong!" page to the client.
123 :type request: :api:`twisted.web.http.Request`
124 :param request: A ``Request`` object containing the HTTP method, full
125 URI, and any URL/POST arguments and headers present.
126 :type error: :exc:`Exception`
127 :param error: Any exeption which has occurred while attempting to retrieve
128 a template, render a page, or retrieve a resource.
129 :param str template_name: A string describing which template/page/resource
130 was being used when the exception occurred, i.e. ``'index.html'``.
131 :param bool html: If ``True``, return one of two HTML error pages. First,
132 we attempt to render a fancier error page. If that rendering failed,
133 or if **html** is ``False``, then we return a very simple HTML page
134 (without CSS, Javascript, images, etc.) which simply says
135 ``"Sorry! Something went wrong with your request."``
137 :returns: A bytes object containing some content to serve to the client
138 (rather than serving a Twisted traceback).
140 logging
.error("Error while attempting to render %s: %s"
141 % (template_name
or 'template',
142 mako
.exceptions
.text_error_template().render()))
144 # TRANSLATORS: Please DO NOT translate the following words and/or phrases in
145 # any string (regardless of capitalization and/or punctuation):
148 # "pluggable transport"
149 # "pluggable transports"
154 errorMessage
= _("Sorry! Something went wrong with your request.")
157 return errorMessage
.encode("utf-8")
160 rendered
= resource500
.render(request
)
161 except Exception as err
:
162 logging
.exception(err
)
163 rendered
= errorMessage
.encode("utf-8")
168 def redirectMaliciousRequest(request
):
169 '''Redirect the client to a "daring work of art" which "in true
170 post-modern form, […] tends to raise more questions than answers."
172 logging
.debug("Redirecting %s to a daring work of art..." % getClientIP(request
))
173 request
.write(redirectTo(base64
.b64decode("aHR0cDovLzJnaXJsczFjdXAuY2Ev"), request
))
178 def getSortedLangList(rebuild
=False):
180 Build and return a list of tuples that contains all of BridgeDB's supported
181 languages, e.g.: [("az", "Azərbaycan"), ("ca", "Català"), ..., ].
183 :param rebuild bool: Force a rebuild of ``supported_langs`` if the argument
184 is set to ``True``. The default is ``False``.
186 :returns: A list of tuples of the form (language-locale, language). The
187 list is sorted alphabetically by language. We use this list to
188 provide a language switcher in BridgeDB's web interface.
191 # If we already compiled our languages, return them right away.
192 global supported_langs
193 if supported_langs
and not rebuild
:
194 return supported_langs
195 logging
.debug("Building supported languages for language switcher.")
198 for l
in translations
.getSupportedLangs():
200 # We don't support 'en_GB', and 'en' and 'en_US' are the same. 'zh_HK'
201 # is very similar to 'zh_TW' and we also lack translators for it, so we
202 # drop the locale: <https://bugs.torproject.org/26543#comment:17>
203 if l
in ("en_GB", "en_US", "zh_HK"):
207 langDict
[l
] = "%s" % (babel
.core
.Locale
.parse(l
).display_name
.capitalize())
208 except Exception as err
:
209 logging
.warning("Failed to create language switcher option for %s: %s" % (l
, err
))
211 # Sort languages alphabetically.
212 supported_langs
= sorted(langDict
.items(), key
=operator
.itemgetter(1))
214 return supported_langs
217 class MaliciousRequest(Exception):
218 """Raised when we received a possibly malicious request."""
221 class CSPResource(resource
.Resource
):
222 """A resource which adds a ``'Content-Security-Policy:'`` header.
224 :vartype reportViolations: bool
225 :var reportViolations: Use the Content Security Policy in `report-only`_
226 mode, causing CSP violations to be reported back to the server (at
227 :attr:`reportURI`, where the details of the violation will be logged).
229 :vartype reportURI: str
230 :var reportURI: If :attr:`reportViolations` is ``True``, Content Security
231 Policy violations will be sent as JSON-encoded POST request to this
232 URI. (default: ``'csp-violation'``)
235 https://w3c.github.io/webappsec/specs/content-security-policy/#content-security-policy-report-only-header-field
237 reportViolations
= False
238 reportURI
= 'csp-violation'
240 def __init__(self
, includeSelf
=False, enabled
=True, reportViolations
=False,
241 useForwardedHeader
=False):
242 """Create a new :api:`twisted.web.resource.Resource` which adds a
243 ``'Content-Security-Policy:'`` header.
245 If enabled, the default Content Security Policy is::
254 where ``FQDN`` the value returned from the :func:`getFQDN` function
255 (which uses the ``SERVER_PUBLIC_FQDN`` config file option).
257 If the **includeSelf** parameter is enabled, then ``"'self'"``
258 (literally, a string containing the word ``self``, surrounded by
259 single-quotes) will be appended to the ``FQDN``.
261 :param str fqdn: The public, fully-qualified domain name
262 of the HTTP server that will serve this resource.
263 :param bool includeSelf: Append ``'self'`` after the **fqdn** in the
264 Content Security Policy.
265 :param bool enabled: If ``False``, all Content Security Policy
266 headers, including those used in report-only mode, will not be
267 sent. If ``True``, Content Security Policy headers (regardless of
268 whether report-only mode is dis-/en-abled) will be sent.
270 :param bool reportViolations: Use the Content Security Policy in
271 report-only mode, causing CSP violations to be reported back to
272 the server (at :attr:`reportURI`, where the details of the
273 violation will be logged). (default: ``False``)
274 :param bool useForwardedHeader: If ``True``, then we will attempt to
275 obtain the client's IP address from the ``X-Forwarded-For`` HTTP
276 header. This *only* has an effect if **reportViolations** is also
277 set to ``True`` — the client's IP address is logged along with any
278 CSP violation reports which the client sent via HTTP POST requests
279 to our :attr:`reportURI`. (default: ``False``)
281 resource
.Resource
.__init
__(self
)
283 self
.fqdn
= getFQDN()
284 self
.enabled
= enabled
285 self
.useForwardedHeader
= useForwardedHeader
286 self
.csp
= ("default-src 'none'; "
290 "img-src {0} data:; "
294 self
.fqdn
= " ".join([self
.fqdn
, "'self'"])
297 self
.reportViolations
= reportViolations
299 def setCSPHeader(self
, request
):
300 """Set the CSP header for a **request**.
302 If this :class:`CSPResource` is :attr:`enabled`, then use
303 :api:`twisted.web.http.Request.setHeader` to send an HTTP
304 ``'Content-Security-Policy:'`` header for any response made to the
305 **request** (or a ``'Content-Security-Policy-Report-Only:'`` header,
306 if :attr:`reportViolations` is enabled).
308 :type request: :api:`twisted.web.http.Request`
309 :param request: A ``Request`` object for :attr:`reportViolationURI`.
311 self
.fqdn
= self
.fqdn
or getFQDN() # Update the FQDN if it changed.
313 if self
.enabled
and self
.fqdn
:
314 if not self
.reportViolations
:
315 request
.setHeader("Content-Security-Policy",
316 self
.csp
.format(self
.fqdn
))
318 logging
.debug("Sending report-only CSP header...")
319 request
.setHeader("Content-Security-Policy-Report-Only",
320 self
.csp
.format(self
.fqdn
) +
321 "report-uri /%s" % self
.reportURI
)
323 def render_POST(self
, request
):
324 """If we're in debug mode, log a Content Security Policy violation.
326 :type request: :api:`twisted.web.http.Request`
327 :param request: A ``Request`` object for :attr:`reportViolationURI`.
330 client
= getClientIP(request
, self
.useForwardedHeader
)
331 report
= request
.content
.read(2048)
333 logging
.warning("Content-Security-Policy violation report from %s: %r"
334 % (client
or "UNKNOWN CLIENT", report
))
335 except Exception as err
:
336 logging
.error("Error while attempting to log CSP report: %s" % err
)
338 # Redirect back to the original resource after the report was logged:
339 return redirectTo(request
.uri
, request
)
342 class ErrorResource(CSPResource
):
343 """A resource which explains that BridgeDB is undergoing maintenance, or
344 that some other (unexpected) error has occured.
348 def __init__(self
, template
=None, code
=200):
349 """Create a :api:`twisted.web.resource.Resource` for an error page."""
350 CSPResource
.__init
__(self
)
351 self
.template
= template
354 def render_GET(self
, request
):
355 self
.setCSPHeader(request
)
356 request
.setHeader("Content-Type", "text/html; charset=utf-8")
357 request
.setResponseCode(self
.code
)
358 request
.args
= stringifyRequestArgs(request
.args
)
361 template
= lookup
.get_template(self
.template
)
362 rendered
= template
.render()
363 except Exception as err
:
364 rendered
= replaceErrorPage(request
, err
, html
=False)
368 render_POST
= render_GET
371 resource404
= ErrorResource('error-404.html', code
=404)
372 resource500
= ErrorResource('error-500.html', code
=500)
373 maintenance
= ErrorResource('error-503.html', code
=503)
376 class CustomErrorHandlingResource(resource
.Resource
):
377 """A :api:`twisted.web.resource.Resource` which wraps the
378 :api:`twisted.web.resource.Resource.getChild` method in order to use
379 custom error handling pages.
381 def getChild(self
, path
, request
):
382 logging
.debug("[404] %s" % request
.uri
)
386 class TranslatedTemplateResource(CustomErrorHandlingResource
, CSPResource
):
387 """A generalised resource which uses gettext translations and Mako
392 def __init__(self
, template
=None):
393 """Create a new :api:`Resource <twisted.web.resource.Resource>` for a
394 Mako-templated webpage.
396 gettext
.install("bridgedb")
397 CSPResource
.__init
__(self
)
398 self
.template
= template
400 def render_GET(self
, request
):
401 self
.setCSPHeader(request
)
402 request
.args
= stringifyRequestArgs(request
.args
)
405 langs
= translations
.getLocaleFromHTTPRequest(request
)
406 rtl
= translations
.usingRTLLang(langs
)
407 template
= lookup
.get_template(self
.template
)
408 rendered
= template
.render(strings
,
412 langOverride
=translations
.isLangOverridden(request
))
413 except Exception as err
: # pragma: no cover
414 rendered
= replaceErrorPage(request
, err
)
415 request
.setHeader("Content-Type", "text/html; charset=utf-8")
418 render_POST
= render_GET
421 class IndexResource(TranslatedTemplateResource
):
422 """The parent resource of all other documents hosted by the webserver."""
425 """Create a :api:`twisted.web.resource.Resource` for the index page."""
426 TranslatedTemplateResource
.__init
__(self
, 'index.html')
429 class OptionsResource(TranslatedTemplateResource
):
430 """A resource with additional options which a client may use to specify the
431 which bridge types should be returned by :class:`BridgesResource`.
434 """Create a :api:`twisted.web.resource.Resource` for the options page."""
435 TranslatedTemplateResource
.__init
__(self
, 'options.html')
438 class HowtoResource(TranslatedTemplateResource
):
439 """A resource which explains how to use bridges."""
442 """Create a :api:`twisted.web.resource.Resource` for the HowTo page."""
443 TranslatedTemplateResource
.__init
__(self
, 'howto.html')
446 class CaptchaProtectedResource(CustomErrorHandlingResource
, CSPResource
):
447 """A general resource protected by some form of CAPTCHA."""
451 def __init__(self
, publicKey
=None, secretKey
=None,
452 useForwardedHeader
=False, protectedResource
=None):
453 CSPResource
.__init
__(self
)
454 self
.publicKey
= publicKey
455 self
.secretKey
= secretKey
456 self
.useForwardedHeader
= useForwardedHeader
457 self
.resource
= protectedResource
459 def getClientIP(self
, request
):
460 """Get the client's IP address from the ``'X-Forwarded-For:'``
461 header, or from the :api:`request <twisted.web.server.Request>`.
463 :type request: :api:`twisted.web.http.Request`
464 :param request: A ``Request`` for a
465 :api:`twisted.web.resource.Resource`.
466 :rtype: ``None`` or :any:`str`
467 :returns: The client's IP address, if it was obtainable.
469 return getClientIP(request
, self
.useForwardedHeader
)
471 def getCaptchaImage(self
, request
=None):
472 """Get a CAPTCHA image.
475 :returns: A 2-tuple of ``(image, challenge)``, where ``image`` is a
476 JPEG-encoded image of type bytes, and ``challenge`` is a unique
477 string. If unable to retrieve a CAPTCHA, returns a tuple
478 containing (b'', '').
482 def extractClientSolution(self
, request
):
483 """Extract the client's CAPTCHA solution from a POST request.
485 This is used after receiving a POST request from a client (which
486 should contain their solution to the CAPTCHA), to extract the solution
487 and challenge strings.
489 :type request: :api:`twisted.web.http.Request`
490 :param request: A ``Request`` object for 'bridges.html'.
491 :returns: A redirect for a request for a new CAPTCHA if there was a
492 problem. Otherwise, returns a 2-tuple of bytes, the first is the
493 client's CAPTCHA solution from the text input area, and the second
494 is the challenge string.
497 challenge
= request
.args
['captcha_challenge_field'][0]
498 response
= request
.args
['captcha_response_field'][0]
499 except Exception as error
:
500 raise MaliciousRequest(
501 ("Client CAPTCHA solution to HTTPS distributor server "
502 "didn't include correct HTTP arguments: %s" % error
))
503 return (challenge
, response
)
505 def checkSolution(self
, request
):
506 """Override this method to check a client's CAPTCHA solution.
509 :returns: ``True`` if the client correctly solved the CAPTCHA;
514 def render_GET(self
, request
):
515 """Retrieve a CAPTCHA and serve it to the client.
517 :type request: :api:`twisted.web.http.Request`
518 :param request: A ``Request`` object for a page which should be
519 protected by a CAPTCHA.
521 :returns: A rendered HTML page containing a CAPTCHA challenge image
522 for the client to solve.
524 self
.setCSPHeader(request
)
525 request
.args
= stringifyRequestArgs(request
.args
)
528 image
, challenge
= self
.getCaptchaImage(request
)
531 langs
= translations
.getLocaleFromHTTPRequest(request
)
532 rtl
= translations
.usingRTLLang(langs
)
533 # TODO: this does not work for versions of IE < 8.0
534 imgstr
= b
'data:image/jpeg;base64,%s' % base64
.b64encode(image
)
535 template
= lookup
.get_template('captcha.html')
536 rendered
= template
.render(strings
,
540 langOverride
=translations
.isLangOverridden(request
),
541 imgstr
=imgstr
.decode("utf-8"),
542 challenge_field
=challenge
)
543 except Exception as err
:
544 rendered
= replaceErrorPage(request
, err
, 'captcha.html')
546 request
.setHeader("Content-Type", "text/html; charset=utf-8")
549 def render_POST(self
, request
):
550 """Process a client's CAPTCHA solution.
552 If the client's CAPTCHA solution is valid (according to
553 :meth:`checkSolution`), process and serve their original
554 request. Otherwise, redirect them back to a new CAPTCHA page.
556 :type request: :api:`twisted.web.http.Request`
557 :param request: A ``Request`` object, including POST arguments which
558 should include two key/value pairs: one key being
559 ``'captcha_challenge_field'``, and the other,
560 ``'captcha_response_field'``. These POST arguments should be
561 obtained from :meth:`render_GET`.
563 :returns: A rendered HTML page containing a ReCaptcha challenge image
564 for the client to solve.
566 self
.setCSPHeader(request
)
567 request
.setHeader("Content-Type", "text/html; charset=utf-8")
568 request
.args
= stringifyRequestArgs(request
.args
)
571 if self
.checkSolution(request
) is True:
572 metrix
.recordValidHTTPSRequest(request
)
573 return self
.resource
.render(request
)
574 except ValueError as err
:
575 logging
.debug(str(err
))
576 except MaliciousRequest
as err
:
577 logging
.debug(str(err
))
578 # Make them wait a bit, then redirect them to a "daring
579 # work of art" as pennance for their sins.
580 d
= task
.deferLater(reactor
, 1, lambda: request
)
581 d
.addCallback(redirectMaliciousRequest
)
582 metrix
.recordInvalidHTTPSRequest(request
)
584 except Exception as err
:
585 logging
.debug(str(err
))
586 metrix
.recordInvalidHTTPSRequest(request
)
587 return replaceErrorPage(request
, err
)
589 metrix
.recordInvalidHTTPSRequest(request
)
590 logging
.debug("Client failed a CAPTCHA; returning redirect to %s"
592 return redirectTo(request
.uri
, request
)
595 class GimpCaptchaProtectedResource(CaptchaProtectedResource
):
596 """A web resource which uses a local cache of CAPTCHAs, generated with
597 gimp-captcha_, to protect another resource.
599 .. _gimp-captcha: https://github.com/isislovecruft/gimp-captcha
602 def __init__(self
, hmacKey
=None, captchaDir
='', **kwargs
):
603 """Protect a resource via this one, using a local CAPTCHA cache.
605 :param str secretkey: A PKCS#1 OAEP-padded, private RSA key, used for
606 verifying the client's solution to the CAPTCHA. See
607 :func:`bridgedb.crypto.getRSAKey` and the
608 ``GIMP_CAPTCHA_RSA_KEYFILE`` config setting.
609 :param str publickey: A PKCS#1 OAEP-padded, public RSA key, used for
610 creating the ``captcha_challenge_field`` string to give to a
612 :param bytes hmacKey: The master HMAC key, used for validating CAPTCHA
613 challenge strings in :meth:`captcha.GimpCaptcha.check`. The file
614 where this key is stored can be set via the
615 ``GIMP_CAPTCHA_HMAC_KEYFILE`` option in the config file.
616 :param str captchaDir: The directory where the cached CAPTCHA images
617 are stored. See the ``GIMP_CAPTCHA_DIR`` config setting.
618 :param bool useForwardedHeader: If ``True``, obtain the client's IP
619 address from the ``X-Forwarded-For`` HTTP header.
620 :type protectedResource: :api:`twisted.web.resource.Resource`
621 :param protectedResource: The resource to serve if the client
622 successfully passes the CAPTCHA challenge.
623 :param str serverPublicFQDN: The public, fully-qualified domain name
624 of the HTTP server that will serve this resource.
626 CaptchaProtectedResource
.__init
__(self
, **kwargs
)
627 self
.hmacKey
= hmacKey
628 self
.captchaDir
= captchaDir
630 def checkSolution(self
, request
):
631 """Process a solved CAPTCHA via :meth:`bridgedb.captcha.GimpCaptcha.check`.
633 :type request: :api:`twisted.web.http.Request`
634 :param request: A ``Request`` object, including POST arguments which
635 should include two key/value pairs: one key being
636 ``'captcha_challenge_field'``, and the other,
637 ``'captcha_response_field'``. These POST arguments should be
638 obtained from :meth:`render_GET`.
640 :returns: True, if the CAPTCHA solution was valid; False otherwise.
643 challenge
, solution
= self
.extractClientSolution(request
)
644 clientIP
= self
.getClientIP(request
)
645 clientHMACKey
= crypto
.getHMAC(self
.hmacKey
, clientIP
)
648 valid
= captcha
.GimpCaptcha
.check(challenge
, solution
,
649 self
.secretKey
, clientHMACKey
)
650 except captcha
.CaptchaExpired
as error
:
654 logging
.debug("%sorrect captcha from %r: %r."
655 % ("C" if valid
else "Inc", clientIP
, solution
))
658 def getCaptchaImage(self
, request
):
659 """Get a random CAPTCHA image from our **captchaDir**.
661 Creates a :class:`~bridgedb.captcha.GimpCaptcha`, and calls its
662 :meth:`~bridgedb.captcha.GimpCaptcha.get` method to return a random
663 CAPTCHA and challenge string.
665 :type request: :api:`twisted.web.http.Request`
666 :param request: A client's initial request for some other resource
667 which is protected by this one (i.e. protected by a CAPTCHA).
668 :returns: A 2-tuple of ``(image, challenge)``, where::
669 - ``image`` is a string holding a binary, JPEG-encoded image.
670 - ``challenge`` is a unique string associated with the request.
672 # Create a new HMAC key, specific to requests from this client:
673 clientIP
= self
.getClientIP(request
)
674 clientHMACKey
= crypto
.getHMAC(self
.hmacKey
, clientIP
)
675 capt
= captcha
.GimpCaptcha(self
.publicKey
, self
.secretKey
,
676 clientHMACKey
, self
.captchaDir
)
679 except captcha
.GimpCaptchaError
as error
:
681 except Exception as error
: # pragma: no cover
682 logging
.error("Unhandled error while retrieving Gimp captcha!")
683 logging
.exception(error
)
685 return (capt
.image
, capt
.challenge
)
687 def render_GET(self
, request
):
688 """Get a random CAPTCHA from our local cache directory and serve it to
691 :type request: :api:`twisted.web.http.Request`
692 :param request: A ``Request`` object for a page which should be
693 protected by a CAPTCHA.
695 :returns: A rendered HTML page containing a ReCaptcha challenge image
696 for the client to solve.
698 return CaptchaProtectedResource
.render_GET(self
, request
)
700 def render_POST(self
, request
):
701 """Process a client's CAPTCHA solution.
703 If the client's CAPTCHA solution is valid (according to
704 :meth:`checkSolution`), process and serve their original
705 request. Otherwise, redirect them back to a new CAPTCHA page.
707 :type request: :api:`twisted.web.http.Request`
708 :param request: A ``Request`` object, including POST arguments which
709 should include two key/value pairs: one key being
710 ``'captcha_challenge_field'``, and the other,
711 ``'captcha_response_field'``. These POST arguments should be
712 obtained from :meth:`render_GET`.
714 :returns: A rendered HTML page containing a ReCaptcha challenge image
715 for the client to solve.
717 return CaptchaProtectedResource
.render_POST(self
, request
)
720 class ReCaptchaProtectedResource(CaptchaProtectedResource
):
721 """A web resource which uses the reCaptcha_ service.
723 .. _reCaptcha: http://www.google.com/recaptcha
726 def __init__(self
, remoteIP
=None, **kwargs
):
727 CaptchaProtectedResource
.__init
__(self
, **kwargs
)
728 self
.remoteIP
= remoteIP
730 def _renderDeferred(self
, checkedRequest
):
731 """Render this resource asynchronously.
733 :type checkedRequest: tuple
734 :param checkedRequest: A tuple of ``(bool, request)``, as returned
735 from :meth:`checkSolution`.
738 valid
, request
= checkedRequest
739 except Exception as err
:
740 logging
.error("Error in _renderDeferred(): %s" % err
)
743 logging
.debug("Attemping to render %svalid request %r"
744 % ('' if valid
else 'in', request
))
747 rendered
= self
.resource
.render(request
)
748 except Exception as err
: # pragma: no cover
749 rendered
= replaceErrorPage(request
, err
)
751 logging
.info("Client failed a CAPTCHA; redirecting to %s"
753 rendered
= redirectTo(request
.uri
, request
)
756 request
.write(rendered
.encode('utf-8') if isinstance(rendered
, str) else rendered
)
758 except Exception as err
: # pragma: no cover
759 logging
.exception(err
)
763 def getCaptchaImage(self
, request
):
764 """Get a CAPTCHA image from the remote reCaptcha server.
766 :type request: :api:`twisted.web.http.Request`
767 :param request: A client's initial request for some other resource
768 which is protected by this one (i.e. protected by a CAPTCHA).
769 :returns: A 2-tuple of ``(image, challenge)``, where::
770 - ``image`` is a string holding a binary, JPEG-encoded image.
771 - ``challenge`` is a unique string associated with the request.
773 capt
= captcha
.ReCaptcha(self
.publicKey
, self
.secretKey
)
777 except Exception as error
:
778 logging
.fatal("Connection to Recaptcha server failed: %s" % error
)
780 if capt
.image
is None:
781 logging
.warn("No CAPTCHA image received from ReCaptcha server!")
783 return (capt
.image
, capt
.challenge
)
785 def getRemoteIP(self
):
786 """Mask the client's real IP address with a faked one.
788 The fake client IP address is sent to the reCaptcha server, and it is
789 either the public IP address of bridges.torproject.org (if the config
790 option ``RECAPTCHA_REMOTE_IP`` is configured), or a random IP.
793 :returns: A fake IP address to report to the reCaptcha API server.
796 remoteIP
= self
.remoteIP
798 # generate a random IP for the captcha submission
799 remoteIP
= IPv4Address(random
.randint(0, 2**32-1)).compressed
803 def checkSolution(self
, request
):
804 """Process a solved CAPTCHA by sending it to the ReCaptcha server.
806 The client's IP address is not sent to the ReCaptcha server; instead,
807 a completely random IP is generated and sent instead.
809 :type request: :api:`twisted.web.http.Request`
810 :param request: A ``Request`` object, including POST arguments which
811 should include two key/value pairs: one key being
812 ``'captcha_challenge_field'``, and the other,
813 ``'captcha_response_field'``. These POST arguments should be
814 obtained from :meth:`render_GET`.
815 :rtupe: :api:`twisted.internet.defer.Deferred`
816 :returns: A deferred which will callback with a tuple in the following
818 (:type:`bool`, :api:`twisted.web.server.Request`)
819 If the CAPTCHA solution was valid, a tuple will contain::
821 Otherwise, it will contain::
824 challenge
, response
= self
.extractClientSolution(request
)
825 clientIP
= self
.getClientIP(request
)
826 remoteIP
= self
.getRemoteIP()
828 logging
.debug("Captcha from %r. Parameters: %r"
829 % (clientIP
, request
.args
))
831 def checkResponse(solution
, request
):
832 """Check the :class:`txrecaptcha.RecaptchaResponse`.
834 :type solution: :class:`txrecaptcha.RecaptchaResponse`.
835 :param solution: The client's CAPTCHA solution, after it has been
836 submitted to the reCaptcha API server.
838 # This valid CAPTCHA result from this function cannot be reliably
839 # unittested, because it's callbacked to from the deferred
840 # returned by ``txrecaptcha.submit``, the latter of which would
841 # require networking (as well as automated CAPTCHA
842 # breaking). Hence, the 'no cover' pragma.
843 if solution
.is_valid
: # pragma: no cover
844 logging
.info("Valid CAPTCHA solution from %r." % clientIP
)
845 metrix
.recordValidHTTPSRequest(request
)
846 return (True, request
)
848 logging
.info("Invalid CAPTCHA solution from %r: %r"
849 % (clientIP
, solution
.error_code
))
850 metrix
.recordInvalidHTTPSRequest(request
)
851 return (False, request
)
853 d
= txrecaptcha
.submit(challenge
, response
, self
.secretKey
,
854 remoteIP
).addCallback(checkResponse
, request
)
857 def render_GET(self
, request
):
858 """Retrieve a ReCaptcha from the API server and serve it to the client.
860 :type request: :api:`twisted.web.http.Request`
861 :param request: A ``Request`` object for 'bridges.html'.
863 :returns: A rendered HTML page containing a ReCaptcha challenge image
864 for the client to solve.
866 return CaptchaProtectedResource
.render_GET(self
, request
)
868 def render_POST(self
, request
):
869 """Process a client's CAPTCHA solution.
871 If the client's CAPTCHA solution is valid (according to
872 :meth:`checkSolution`), process and serve their original
873 request. Otherwise, redirect them back to a new CAPTCHA page.
875 :type request: :api:`twisted.web.http.Request`
876 :param request: A ``Request`` object, including POST arguments which
877 should include two key/value pairs: one key being
878 ``'captcha_challenge_field'``, and the other,
879 ``'captcha_response_field'``. These POST arguments should be
880 obtained from :meth:`render_GET`.
881 :returns: :api:`twisted.web.server.NOT_DONE_YET`, in order to handle
882 the ``Deferred`` returned from :meth:`checkSolution`. Eventually,
883 when the ``Deferred`` request is done being processed,
884 :meth:`_renderDeferred` will handle rendering and displaying the
887 self
.setCSPHeader(request
)
888 request
.args
= stringifyRequestArgs(request
.args
)
889 d
= self
.checkSolution(request
)
890 d
.addCallback(self
._renderDeferred
)
894 class BridgesResource(CustomErrorHandlingResource
, CSPResource
):
895 """This resource displays bridge lines in response to a request."""
899 def __init__(self
, distributor
, schedule
, N
=1, useForwardedHeader
=False,
900 includeFingerprints
=True):
901 """Create a new resource for displaying bridges to a client.
903 :type distributor: :class:`HTTPSDistributor`
904 :param distributor: The mechanism to retrieve bridges for this
906 :type schedule: :class:`~bridgedb.schedule.ScheduledInterval`
907 :param schedule: The time period used to tweak the bridge selection
909 :param int N: The number of bridges to hand out per query.
910 :param bool useForwardedHeader: Whether or not we should use the the
911 X-Forwarded-For header instead of the source IP address.
912 :param bool includeFingerprints: Do we include the bridge's
913 fingerprint in the response?
915 gettext
.install("bridgedb")
916 CSPResource
.__init
__(self
)
917 self
.distributor
= distributor
918 self
.schedule
= schedule
919 self
.nBridgesToGive
= N
920 self
.useForwardedHeader
= useForwardedHeader
921 self
.includeFingerprints
= includeFingerprints
923 def render(self
, request
):
924 """Render a response for a client HTTP request.
926 Presently, this method merely wraps :meth:`getBridgeRequestAnswer` to
927 catch any unhandled exceptions which occur (otherwise the server will
928 display the traceback to the client). If an unhandled exception *does*
929 occur, the client will be served the default "No bridges currently
930 available" HTML response page.
932 :type request: :api:`twisted.web.http.Request`
933 :param request: A ``Request`` object containing the HTTP method, full
934 URI, and any URL/POST arguments and headers present.
936 :returns: A plaintext or HTML response to serve.
938 self
.setCSPHeader(request
)
939 request
.args
= stringifyRequestArgs(request
.args
)
942 response
= self
.getBridgeRequestAnswer(request
)
943 except Exception as err
:
944 logging
.exception(err
)
945 response
= self
.renderAnswer(request
)
947 return response
.encode('utf-8') if isinstance(response
, str) else response
949 def getClientIP(self
, request
):
950 """Get the client's IP address from the ``'X-Forwarded-For:'``
951 header, or from the :api:`request <twisted.web.server.Request>`.
953 :type request: :api:`twisted.web.http.Request`
954 :param request: A ``Request`` object for a
955 :api:`twisted.web.resource.Resource`.
956 :rtype: ``None`` or :any:`str`
957 :returns: The client's IP address, if it was obtainable.
959 return getClientIP(request
, self
.useForwardedHeader
)
961 def getBridgeRequestAnswer(self
, request
):
962 """Respond to a client HTTP request for bridges.
964 :type request: :api:`twisted.web.http.Request`
965 :param request: A ``Request`` object containing the HTTP method, full
966 URI, and any URL/POST arguments and headers present.
968 :returns: A plaintext or HTML response to serve.
971 interval
= self
.schedule
.intervalStart(time
.time())
972 ip
= self
.getClientIP(request
)
974 logging
.info("Replying to web request from %s. Parameters were %r"
975 % (ip
, request
.args
))
977 # Convert all key/value pairs from bytes to str.
979 for arg
, values
in request
.args
.items():
980 arg
= arg
if isinstance(arg
, str) else arg
.decode("utf-8")
981 values
= [value
.decode("utf-8") if isinstance(value
, bytes
) else value
for value
in values
]
982 str_args
[arg
] = values
983 request
.args
= str_args
986 bridgeRequest
= HTTPSBridgeRequest()
987 bridgeRequest
.client
= ip
988 bridgeRequest
.isValid(True)
989 bridgeRequest
.withIPversion(request
.args
)
990 bridgeRequest
.withPluggableTransportType(request
.args
)
991 bridgeRequest
.withoutBlockInCountry(request
)
992 bridgeRequest
.generateFilters()
994 bridges
= self
.distributor
.getBridges(bridgeRequest
, interval
)
995 bridgeLines
= [replaceControlChars(bridge
.getBridgeLine(
996 bridgeRequest
, self
.includeFingerprints
)) for bridge
in bridges
]
998 if antibot
.isRequestFromBot(request
):
999 transports
= bridgeRequest
.transports
1000 # Return either a decoy bridge or no bridge.
1001 if len(transports
) > 2:
1002 logging
.warning("More than one transport requested")
1003 return self
.renderAnswer(request
)
1004 ttype
= "vanilla" if len(transports
) == 0 else transports
[0]
1005 return self
.renderAnswer(request
, antibot
.getDecoyBridge(ttype
, bridgeRequest
.ipVersion
))
1007 return self
.renderAnswer(request
, bridgeLines
)
1009 def getResponseFormat(self
, request
):
1010 """Determine the requested format for the response.
1012 :type request: :api:`twisted.web.http.Request`
1013 :param request: A ``Request`` object containing the HTTP method, full
1014 URI, and any URL/POST arguments and headers present.
1015 :rtype: ``None`` or str
1016 :returns: The argument of the first occurence of the ``format=`` HTTP
1017 GET parameter, if any were present. (The only one which currently
1018 has any effect is ``format=plain``, see note in
1019 :meth:`renderAnswer`.) Otherwise, returns ``None``.
1021 format
= request
.args
.get("format", None)
1022 if format
and len(format
):
1023 format
= format
[0] # Choose the first arg
1026 def renderAnswer(self
, request
, bridgeLines
=None):
1027 """Generate a response for a client which includes **bridgesLines**.
1029 .. note: The generated response can be plain or HTML. A plain response
1032 voltron 1.2.3.4:1234 ABCDEF01234567890ABCDEF01234567890ABCDEF
1033 voltron 5.5.5.5:5555 0123456789ABCDEF0123456789ABCDEF01234567
1035 That is, there is no HTML, what you see is what you get, and what
1036 you get is suitable for pasting directly into Tor Launcher (or
1037 into a torrc, if you prepend ``"Bridge "`` to each line). The
1038 plain format can be requested from BridgeDB's web service by
1039 adding an ``&format=plain`` HTTP GET parameter to the URL. Also
1040 note that you won't get a QRCode, usage instructions, error
1041 messages, or any other fanciness if you use the plain format.
1043 :type request: :api:`twisted.web.http.Request`
1044 :param request: A ``Request`` object containing the HTTP method, full
1045 URI, and any URL/POST arguments and headers present.
1046 :type bridgeLines: list or None
1047 :param bridgeLines: A list of strings used to configure a Tor client
1048 to use a bridge. If ``None``, then the returned page will instead
1049 explain that there were no bridges of the type they requested,
1050 with instructions on how to proceed.
1052 :returns: A plaintext or HTML response to serve.
1055 format
= self
.getResponseFormat(request
)
1057 if format
== 'plain':
1058 request
.setHeader("Content-Type", "text/plain")
1060 rendered
= '\n'.join(bridgeLines
).encode('utf-8')
1061 except Exception as err
:
1062 rendered
= replaceErrorPage(request
, err
, html
=False)
1064 request
.setHeader("Content-Type", "text/html; charset=utf-8")
1066 qrjpeg
= generateQR(bridgeLines
)
1069 qrcode
= 'data:image/jpeg;base64,%s' % base64
.b64encode(qrjpeg
)
1071 langs
= translations
.getLocaleFromHTTPRequest(request
)
1072 rtl
= translations
.usingRTLLang(langs
)
1073 template
= lookup
.get_template('bridges.html')
1074 rendered
= template
.render(strings
,
1075 getSortedLangList(),
1078 langOverride
=translations
.isLangOverridden(request
),
1081 except Exception as err
:
1082 rendered
= replaceErrorPage(request
, err
)
1084 return rendered
.encode("utf-8") if isinstance(rendered
, str) else rendered
1087 def addWebServer(config
, distributor
):
1088 """Set up a web server for HTTP(S)-based bridge distribution.
1090 :type config: :class:`bridgedb.persistent.Conf`
1091 :param config: A configuration object from
1092 :mod:`bridgedb.main`. Currently, we use these options::
1093 HTTP_UNENCRYPTED_PORT
1094 HTTP_UNENCRYPTED_BIND_IP
1095 HTTP_USE_IP_FROM_FORWARDED_HEADER
1096 HTTPS_N_BRIDGES_PER_ANSWER
1097 HTTPS_INCLUDE_FINGERPRINTS
1102 HTTPS_USE_IP_FROM_FORWARDED_HEADER
1103 HTTPS_ROTATION_PERIOD
1108 GIMP_CAPTCHA_ENABLED
1110 GIMP_CAPTCHA_HMAC_KEYFILE
1111 GIMP_CAPTCHA_RSA_KEYFILE
1116 :type distributor: :class:`bridgedb.distributors.https.distributor.HTTPSDistributor`
1117 :param distributor: A bridge distributor.
1118 :raises SystemExit: if the servers cannot be started.
1119 :rtype: :api:`twisted.web.server.Site`
1120 :returns: A webserver.
1123 fwdHeaders
= config
.HTTP_USE_IP_FROM_FORWARDED_HEADER
1124 numBridges
= config
.HTTPS_N_BRIDGES_PER_ANSWER
1125 fprInclude
= config
.HTTPS_INCLUDE_FINGERPRINTS
1127 logging
.info("Starting web servers...")
1129 setFQDN(config
.SERVER_PUBLIC_FQDN
)
1131 index
= IndexResource()
1132 options
= OptionsResource()
1133 howto
= HowtoResource()
1134 robots
= static
.File(os
.path
.join(TEMPLATE_DIR
, 'robots.txt'))
1135 assets
= static
.File(os
.path
.join(TEMPLATE_DIR
, 'assets/'))
1136 keys
= static
.Data(strings
.BRIDGEDB_OPENPGP_KEY
.encode('utf-8'), 'text/plain')
1137 csp
= CSPResource(enabled
=config
.CSP_ENABLED
,
1138 includeSelf
=config
.CSP_INCLUDE_SELF
,
1139 reportViolations
=config
.CSP_REPORT_ONLY
,
1140 useForwardedHeader
=fwdHeaders
)
1142 root
= CustomErrorHandlingResource()
1143 root
.putChild(b
'', index
)
1144 root
.putChild(b
'robots.txt', robots
)
1145 root
.putChild(b
'keys', keys
)
1146 root
.putChild(b
'assets', assets
)
1147 root
.putChild(b
'options', options
)
1148 root
.putChild(b
'howto', howto
)
1149 root
.putChild(b
'maintenance', maintenance
)
1150 root
.putChild(b
'error', resource500
)
1151 root
.putChild(CSPResource
.reportURI
, csp
)
1153 if config
.RECAPTCHA_ENABLED
:
1154 publicKey
= config
.RECAPTCHA_PUB_KEY
1155 secretKey
= config
.RECAPTCHA_SEC_KEY
1156 captcha
= partial(ReCaptchaProtectedResource
,
1157 remoteIP
=config
.RECAPTCHA_REMOTEIP
)
1158 elif config
.GIMP_CAPTCHA_ENABLED
:
1159 # Get the master HMAC secret key for CAPTCHA challenges, and then
1160 # create a new HMAC key from it for use on the server.
1161 captchaKey
= crypto
.getKey(config
.GIMP_CAPTCHA_HMAC_KEYFILE
)
1162 hmacKey
= crypto
.getHMAC(captchaKey
, "Captcha-Key")
1163 # Load or create our encryption keys:
1164 secretKey
, publicKey
= crypto
.getRSAKey(config
.GIMP_CAPTCHA_RSA_KEYFILE
)
1165 captcha
= partial(GimpCaptchaProtectedResource
,
1167 captchaDir
=config
.GIMP_CAPTCHA_DIR
)
1169 if config
.HTTPS_ROTATION_PERIOD
:
1170 count
, period
= config
.HTTPS_ROTATION_PERIOD
.split()
1171 sched
= ScheduledInterval(int(count
), period
)
1173 sched
= Unscheduled()
1175 bridges
= BridgesResource(distributor
, sched
, numBridges
, fwdHeaders
,
1176 includeFingerprints
=fprInclude
)
1178 # Protect the 'bridges' page with a CAPTCHA, if configured to do so:
1179 protected
= captcha(publicKey
=publicKey
,
1180 secretKey
=secretKey
,
1181 useForwardedHeader
=fwdHeaders
,
1182 protectedResource
=bridges
)
1183 root
.putChild(b
'bridges', protected
)
1184 logging
.info("Protecting resources with %s." % captcha
.func
.__name
__)
1186 root
.putChild(b
'bridges', bridges
)
1189 site
.displayTracebacks
= False
1191 if config
.HTTP_UNENCRYPTED_PORT
: # pragma: no cover
1192 ip
= config
.HTTP_UNENCRYPTED_BIND_IP
or ""
1193 port
= config
.HTTP_UNENCRYPTED_PORT
or 80
1195 reactor
.listenTCP(port
, site
, interface
=ip
)
1196 except CannotListenError
as error
:
1197 raise SystemExit(error
)
1198 logging
.info("Started HTTP server on %s:%d" % (str(ip
), int(port
)))
1200 if config
.HTTPS_PORT
: # pragma: no cover
1201 ip
= config
.HTTPS_BIND_IP
or ""
1202 port
= config
.HTTPS_PORT
or 443
1204 from twisted
.internet
.ssl
import DefaultOpenSSLContextFactory
1205 factory
= DefaultOpenSSLContextFactory(config
.HTTPS_KEY_FILE
,
1206 config
.HTTPS_CERT_FILE
)
1207 reactor
.listenSSL(port
, site
, factory
, interface
=ip
)
1208 except CannotListenError
as error
:
1209 raise SystemExit(error
)
1210 logging
.info("Started HTTPS server on %s:%d" % (str(ip
), int(port
)))