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 singletons to keep track of BridgeDB metrics such as
94 # "number of failed HTTPS bridge requests."
95 httpsMetrix
= metrics
.HTTPSMetrics()
96 internalMetrix
= metrics
.InternalMetrics()
99 def stringifyRequestArgs(args
):
100 """Turn the given HTTP request arguments from bytes to str.
102 :param dict args: A dictionary of request arguments.
104 :returns: A dictionary of request arguments.
107 # Convert all key/value pairs from bytes to str.
109 for arg
, values
in args
.items():
110 arg
= arg
if isinstance(arg
, str) else arg
.decode("utf-8")
111 values
= [value
.decode("utf-8") if isinstance(value
, bytes
)
112 else value
for value
in values
]
113 str_args
[arg
] = values
118 def replaceErrorPage(request
, error
, template_name
=None, html
=True):
119 """Create a general error page for displaying in place of tracebacks.
121 Log the error to BridgeDB's logger, and then display a very plain "Sorry!
122 Something went wrong!" page to the client.
124 :type request: :api:`twisted.web.http.Request`
125 :param request: A ``Request`` object containing the HTTP method, full
126 URI, and any URL/POST arguments and headers present.
127 :type error: :exc:`Exception`
128 :param error: Any exeption which has occurred while attempting to retrieve
129 a template, render a page, or retrieve a resource.
130 :param str template_name: A string describing which template/page/resource
131 was being used when the exception occurred, i.e. ``'index.html'``.
132 :param bool html: If ``True``, return one of two HTML error pages. First,
133 we attempt to render a fancier error page. If that rendering failed,
134 or if **html** is ``False``, then we return a very simple HTML page
135 (without CSS, Javascript, images, etc.) which simply says
136 ``"Sorry! Something went wrong with your request."``
138 :returns: A bytes object containing some content to serve to the client
139 (rather than serving a Twisted traceback).
141 logging
.error("Error while attempting to render %s: %s"
142 % (template_name
or 'template',
143 mako
.exceptions
.text_error_template().render()))
145 # TRANSLATORS: Please DO NOT translate the following words and/or phrases in
146 # any string (regardless of capitalization and/or punctuation):
149 # "pluggable transport"
150 # "pluggable transports"
155 errorMessage
= _("Sorry! Something went wrong with your request.")
158 return errorMessage
.encode("utf-8")
161 rendered
= resource500
.render(request
)
162 except Exception as err
:
163 logging
.exception(err
)
164 rendered
= errorMessage
.encode("utf-8")
169 def redirectMaliciousRequest(request
):
170 '''Setting the reponse code to 400 (Bad Request)'''
171 logging
.debug("Setting response code to 400 for %s" % getClientIP(request
))
172 request
.setResponseCode(400)
177 def getSortedLangList(rebuild
=False):
179 Build and return a list of tuples that contains all of BridgeDB's supported
180 languages, e.g.: [("az", "Azərbaycan"), ("ca", "Català"), ..., ].
182 :param rebuild bool: Force a rebuild of ``supported_langs`` if the argument
183 is set to ``True``. The default is ``False``.
185 :returns: A list of tuples of the form (language-locale, language). The
186 list is sorted alphabetically by language. We use this list to
187 provide a language switcher in BridgeDB's web interface.
190 # If we already compiled our languages, return them right away.
191 global supported_langs
192 if supported_langs
and not rebuild
:
193 return supported_langs
194 logging
.debug("Building supported languages for language switcher.")
197 for l
in translations
.getSupportedLangs():
199 # We don't support 'en_GB', and 'en' and 'en_US' are the same. 'zh_HK'
200 # is very similar to 'zh_TW' and we also lack translators for it, so we
201 # drop the locale: <https://bugs.torproject.org/26543#comment:17>
202 if l
in ("en_GB", "en_US", "zh_HK"):
206 langDict
[l
] = "%s" % (babel
.core
.Locale
.parse(l
).display_name
.capitalize())
207 except Exception as err
:
208 logging
.warning("Failed to create language switcher option for %s: %s" % (l
, err
))
210 # Sort languages alphabetically.
211 supported_langs
= sorted(langDict
.items(), key
=operator
.itemgetter(1))
213 return supported_langs
216 class MaliciousRequest(Exception):
217 """Raised when we received a possibly malicious request."""
220 class CSPResource(resource
.Resource
):
221 """A resource which adds a ``'Content-Security-Policy:'`` header.
223 :vartype reportViolations: bool
224 :var reportViolations: Use the Content Security Policy in `report-only`_
225 mode, causing CSP violations to be reported back to the server (at
226 :attr:`reportURI`, where the details of the violation will be logged).
228 :vartype reportURI: str
229 :var reportURI: If :attr:`reportViolations` is ``True``, Content Security
230 Policy violations will be sent as JSON-encoded POST request to this
231 URI. (default: ``'csp-violation'``)
234 https://w3c.github.io/webappsec/specs/content-security-policy/#content-security-policy-report-only-header-field
236 reportViolations
= False
237 reportURI
= 'csp-violation'
239 def __init__(self
, includeSelf
=False, enabled
=True, reportViolations
=False,
240 useForwardedHeader
=False):
241 """Create a new :api:`twisted.web.resource.Resource` which adds a
242 ``'Content-Security-Policy:'`` header.
244 If enabled, the default Content Security Policy is::
253 where ``FQDN`` the value returned from the :func:`getFQDN` function
254 (which uses the ``SERVER_PUBLIC_FQDN`` config file option).
256 If the **includeSelf** parameter is enabled, then ``"'self'"``
257 (literally, a string containing the word ``self``, surrounded by
258 single-quotes) will be appended to the ``FQDN``.
260 :param str fqdn: The public, fully-qualified domain name
261 of the HTTP server that will serve this resource.
262 :param bool includeSelf: Append ``'self'`` after the **fqdn** in the
263 Content Security Policy.
264 :param bool enabled: If ``False``, all Content Security Policy
265 headers, including those used in report-only mode, will not be
266 sent. If ``True``, Content Security Policy headers (regardless of
267 whether report-only mode is dis-/en-abled) will be sent.
269 :param bool reportViolations: Use the Content Security Policy in
270 report-only mode, causing CSP violations to be reported back to
271 the server (at :attr:`reportURI`, where the details of the
272 violation will be logged). (default: ``False``)
273 :param bool useForwardedHeader: If ``True``, then we will attempt to
274 obtain the client's IP address from the ``X-Forwarded-For`` HTTP
275 header. This *only* has an effect if **reportViolations** is also
276 set to ``True`` — the client's IP address is logged along with any
277 CSP violation reports which the client sent via HTTP POST requests
278 to our :attr:`reportURI`. (default: ``False``)
280 resource
.Resource
.__init
__(self
)
282 self
.fqdn
= getFQDN()
283 self
.enabled
= enabled
284 self
.useForwardedHeader
= useForwardedHeader
285 self
.csp
= ("default-src 'none'; "
289 "img-src {0} data:; "
293 self
.fqdn
= " ".join([self
.fqdn
, "'self'"])
296 self
.reportViolations
= reportViolations
298 def setCSPHeader(self
, request
):
299 """Set the CSP header for a **request**.
301 If this :class:`CSPResource` is :attr:`enabled`, then use
302 :api:`twisted.web.http.Request.setHeader` to send an HTTP
303 ``'Content-Security-Policy:'`` header for any response made to the
304 **request** (or a ``'Content-Security-Policy-Report-Only:'`` header,
305 if :attr:`reportViolations` is enabled).
307 :type request: :api:`twisted.web.http.Request`
308 :param request: A ``Request`` object for :attr:`reportViolationURI`.
310 self
.fqdn
= self
.fqdn
or getFQDN() # Update the FQDN if it changed.
312 if self
.enabled
and self
.fqdn
:
313 if not self
.reportViolations
:
314 request
.setHeader("Content-Security-Policy",
315 self
.csp
.format(self
.fqdn
))
317 logging
.debug("Sending report-only CSP header...")
318 request
.setHeader("Content-Security-Policy-Report-Only",
319 self
.csp
.format(self
.fqdn
) +
320 "report-uri /%s" % self
.reportURI
)
322 def render_POST(self
, request
):
323 """If we're in debug mode, log a Content Security Policy violation.
325 :type request: :api:`twisted.web.http.Request`
326 :param request: A ``Request`` object for :attr:`reportViolationURI`.
329 client
= getClientIP(request
, self
.useForwardedHeader
)
330 report
= request
.content
.read(2048)
332 logging
.warning("Content-Security-Policy violation report from %s: %r"
333 % (client
or "UNKNOWN CLIENT", report
))
334 except Exception as err
:
335 logging
.error("Error while attempting to log CSP report: %s" % err
)
337 # Redirect back to the original resource after the report was logged:
338 return redirectTo(request
.uri
, request
)
341 class ErrorResource(CSPResource
):
342 """A resource which explains that BridgeDB is undergoing maintenance, or
343 that some other (unexpected) error has occured.
347 def __init__(self
, template
=None, code
=200):
348 """Create a :api:`twisted.web.resource.Resource` for an error page."""
349 CSPResource
.__init
__(self
)
350 self
.template
= template
353 def render_GET(self
, request
):
354 self
.setCSPHeader(request
)
355 request
.setHeader("Content-Type", "text/html; charset=utf-8")
356 request
.setResponseCode(self
.code
)
357 request
.args
= stringifyRequestArgs(request
.args
)
360 template
= lookup
.get_template(self
.template
)
361 rendered
= template
.render()
362 except Exception as err
:
363 rendered
= replaceErrorPage(request
, err
, html
=False)
367 render_POST
= render_GET
369 resource400
= ErrorResource('error-400.html', code
=400)
370 resource404
= ErrorResource('error-404.html', code
=404)
371 resource500
= ErrorResource('error-500.html', code
=500)
372 maintenance
= ErrorResource('error-503.html', code
=503)
375 class CustomErrorHandlingResource(resource
.Resource
):
376 """A :api:`twisted.web.resource.Resource` which wraps the
377 :api:`twisted.web.resource.Resource.getChild` method in order to use
378 custom error handling pages.
380 def getChild(self
, path
, request
):
381 logging
.debug("[404] %s" % request
.uri
)
385 class TranslatedTemplateResource(CustomErrorHandlingResource
, CSPResource
):
386 """A generalised resource which uses gettext translations and Mako
391 def __init__(self
, template
=None, showFaq
=True):
392 """Create a new :api:`Resource <twisted.web.resource.Resource>` for a
393 Mako-templated webpage.
395 gettext
.install("bridgedb")
396 CSPResource
.__init
__(self
)
397 self
.template
= template
398 self
.showFaq
= showFaq
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 showFaq
=self
.showFaq
)
414 except Exception as err
: # pragma: no cover
415 rendered
= replaceErrorPage(request
, err
)
416 request
.setHeader("Content-Type", "text/html; charset=utf-8")
419 render_POST
= render_GET
422 class IndexResource(TranslatedTemplateResource
):
423 """The parent resource of all other documents hosted by the webserver."""
426 """Create a :api:`twisted.web.resource.Resource` for the index page."""
427 TranslatedTemplateResource
.__init
__(self
, 'index.html')
430 class OptionsResource(TranslatedTemplateResource
):
431 """A resource with additional options which a client may use to specify the
432 which bridge types should be returned by :class:`BridgesResource`.
435 """Create a :api:`twisted.web.resource.Resource` for the options page."""
436 TranslatedTemplateResource
.__init
__(self
, 'options.html')
439 class InfoResource(TranslatedTemplateResource
):
441 TranslatedTemplateResource
.__init
__(self
, 'info.html', showFaq
=False)
444 class HowtoResource(TranslatedTemplateResource
):
445 """A resource which explains how to use bridges."""
448 """Create a :api:`twisted.web.resource.Resource` for the HowTo page."""
449 TranslatedTemplateResource
.__init
__(self
, 'howto.html')
452 class CaptchaProtectedResource(CustomErrorHandlingResource
, CSPResource
):
453 """A general resource protected by some form of CAPTCHA."""
457 def __init__(self
, publicKey
=None, secretKey
=None,
458 useForwardedHeader
=False, protectedResource
=None):
459 CSPResource
.__init
__(self
)
460 self
.publicKey
= publicKey
461 self
.secretKey
= secretKey
462 self
.useForwardedHeader
= useForwardedHeader
463 self
.resource
= protectedResource
465 def getClientIP(self
, request
):
466 """Get the client's IP address from the ``'X-Forwarded-For:'``
467 header, or from the :api:`request <twisted.web.server.Request>`.
469 :type request: :api:`twisted.web.http.Request`
470 :param request: A ``Request`` for a
471 :api:`twisted.web.resource.Resource`.
472 :rtype: ``None`` or :any:`str`
473 :returns: The client's IP address, if it was obtainable.
475 return getClientIP(request
, self
.useForwardedHeader
)
477 def getCaptchaImage(self
, request
=None):
478 """Get a CAPTCHA image.
481 :returns: A 2-tuple of ``(image, challenge)``, where ``image`` is a
482 JPEG-encoded image of type bytes, and ``challenge`` is a unique
483 string. If unable to retrieve a CAPTCHA, returns a tuple
484 containing (b'', '').
488 def extractClientSolution(self
, request
):
489 """Extract the client's CAPTCHA solution from a POST request.
491 This is used after receiving a POST request from a client (which
492 should contain their solution to the CAPTCHA), to extract the solution
493 and challenge strings.
495 :type request: :api:`twisted.web.http.Request`
496 :param request: A ``Request`` object for 'bridges.html'.
497 :returns: A redirect for a request for a new CAPTCHA if there was a
498 problem. Otherwise, returns a 2-tuple of bytes, the first is the
499 client's CAPTCHA solution from the text input area, and the second
500 is the challenge string.
503 challenge
= request
.args
['captcha_challenge_field'][0]
504 response
= request
.args
['captcha_response_field'][0]
505 except Exception as error
:
506 raise MaliciousRequest(
507 ("Client CAPTCHA solution to HTTPS distributor server "
508 "didn't include correct HTTP arguments: %s" % error
))
509 return (challenge
, response
)
511 def checkSolution(self
, request
):
512 """Override this method to check a client's CAPTCHA solution.
515 :returns: ``True`` if the client correctly solved the CAPTCHA;
520 def render_GET(self
, request
):
521 """Retrieve a CAPTCHA and serve it to the client.
523 :type request: :api:`twisted.web.http.Request`
524 :param request: A ``Request`` object for a page which should be
525 protected by a CAPTCHA.
527 :returns: A rendered HTML page containing a CAPTCHA challenge image
528 for the client to solve.
530 self
.setCSPHeader(request
)
531 request
.args
= stringifyRequestArgs(request
.args
)
534 image
, challenge
= self
.getCaptchaImage(request
)
537 langs
= translations
.getLocaleFromHTTPRequest(request
)
538 rtl
= translations
.usingRTLLang(langs
)
539 # TODO: this does not work for versions of IE < 8.0
540 imgstr
= b
'data:image/jpeg;base64,%s' % base64
.b64encode(image
)
541 template
= lookup
.get_template('captcha.html')
542 rendered
= template
.render(strings
,
546 langOverride
=translations
.isLangOverridden(request
),
547 imgstr
=imgstr
.decode("utf-8"),
548 challenge_field
=challenge
)
549 except Exception as err
:
550 rendered
= replaceErrorPage(request
, err
, 'captcha.html')
552 request
.setHeader("Content-Type", "text/html; charset=utf-8")
555 def render_POST(self
, request
):
556 """Process a client's CAPTCHA solution.
558 If the client's CAPTCHA solution is valid (according to
559 :meth:`checkSolution`), process and serve their original
560 request. Otherwise, redirect them back to a new CAPTCHA page.
562 :type request: :api:`twisted.web.http.Request`
563 :param request: A ``Request`` object, including POST arguments which
564 should include two key/value pairs: one key being
565 ``'captcha_challenge_field'``, and the other,
566 ``'captcha_response_field'``. These POST arguments should be
567 obtained from :meth:`render_GET`.
569 :returns: A rendered HTML page containing a ReCaptcha challenge image
570 for the client to solve.
572 self
.setCSPHeader(request
)
573 request
.setHeader("Content-Type", "text/html; charset=utf-8")
574 request
.args
= stringifyRequestArgs(request
.args
)
577 if self
.checkSolution(request
) is True:
578 httpsMetrix
.recordValidHTTPSRequest(request
)
579 return self
.resource
.render(request
)
580 except ValueError as err
:
581 logging
.debug(str(err
))
582 except MaliciousRequest
as err
:
583 logging
.debug(str(err
))
584 # Make them wait a bit, then redirect them to a "daring
585 # work of art" as pennance for their sins.
586 d
= task
.deferLater(reactor
, 1, lambda: request
)
587 d
.addCallback(redirectMaliciousRequest
)
588 httpsMetrix
.recordInvalidHTTPSRequest(request
)
590 except Exception as err
:
591 logging
.debug(str(err
))
592 httpsMetrix
.recordInvalidHTTPSRequest(request
)
593 return replaceErrorPage(request
, err
)
595 httpsMetrix
.recordInvalidHTTPSRequest(request
)
596 logging
.debug("Client failed a CAPTCHA; returning redirect to %s"
598 return redirectTo(request
.uri
, request
)
601 class GimpCaptchaProtectedResource(CaptchaProtectedResource
):
602 """A web resource which uses a local cache of CAPTCHAs, generated with
603 gimp-captcha_, to protect another resource.
605 .. _gimp-captcha: https://github.com/isislovecruft/gimp-captcha
608 def __init__(self
, hmacKey
=None, captchaDir
='', **kwargs
):
609 """Protect a resource via this one, using a local CAPTCHA cache.
611 :param str secretkey: A PKCS#1 OAEP-padded, private RSA key, used for
612 verifying the client's solution to the CAPTCHA. See
613 :func:`bridgedb.crypto.getRSAKey` and the
614 ``GIMP_CAPTCHA_RSA_KEYFILE`` config setting.
615 :param str publickey: A PKCS#1 OAEP-padded, public RSA key, used for
616 creating the ``captcha_challenge_field`` string to give to a
618 :param bytes hmacKey: The master HMAC key, used for validating CAPTCHA
619 challenge strings in :meth:`captcha.GimpCaptcha.check`. The file
620 where this key is stored can be set via the
621 ``GIMP_CAPTCHA_HMAC_KEYFILE`` option in the config file.
622 :param str captchaDir: The directory where the cached CAPTCHA images
623 are stored. See the ``GIMP_CAPTCHA_DIR`` config setting.
624 :param bool useForwardedHeader: If ``True``, obtain the client's IP
625 address from the ``X-Forwarded-For`` HTTP header.
626 :type protectedResource: :api:`twisted.web.resource.Resource`
627 :param protectedResource: The resource to serve if the client
628 successfully passes the CAPTCHA challenge.
629 :param str serverPublicFQDN: The public, fully-qualified domain name
630 of the HTTP server that will serve this resource.
632 CaptchaProtectedResource
.__init
__(self
, **kwargs
)
633 self
.hmacKey
= hmacKey
634 self
.captchaDir
= captchaDir
636 def checkSolution(self
, request
):
637 """Process a solved CAPTCHA via :meth:`bridgedb.captcha.GimpCaptcha.check`.
639 :type request: :api:`twisted.web.http.Request`
640 :param request: A ``Request`` object, including POST arguments which
641 should include two key/value pairs: one key being
642 ``'captcha_challenge_field'``, and the other,
643 ``'captcha_response_field'``. These POST arguments should be
644 obtained from :meth:`render_GET`.
646 :returns: True, if the CAPTCHA solution was valid; False otherwise.
649 challenge
, solution
= self
.extractClientSolution(request
)
650 clientIP
= self
.getClientIP(request
)
651 clientHMACKey
= crypto
.getHMAC(self
.hmacKey
, clientIP
)
654 valid
= captcha
.GimpCaptcha
.check(challenge
, solution
,
655 self
.secretKey
, clientHMACKey
)
656 except captcha
.CaptchaExpired
as error
:
660 logging
.debug("%sorrect captcha from %r: %r."
661 % ("C" if valid
else "Inc", clientIP
, solution
))
664 def getCaptchaImage(self
, request
):
665 """Get a random CAPTCHA image from our **captchaDir**.
667 Creates a :class:`~bridgedb.captcha.GimpCaptcha`, and calls its
668 :meth:`~bridgedb.captcha.GimpCaptcha.get` method to return a random
669 CAPTCHA and challenge string.
671 :type request: :api:`twisted.web.http.Request`
672 :param request: A client's initial request for some other resource
673 which is protected by this one (i.e. protected by a CAPTCHA).
674 :returns: A 2-tuple of ``(image, challenge)``, where::
675 - ``image`` is a string holding a binary, JPEG-encoded image.
676 - ``challenge`` is a unique string associated with the request.
678 # Create a new HMAC key, specific to requests from this client:
679 clientIP
= self
.getClientIP(request
)
680 clientHMACKey
= crypto
.getHMAC(self
.hmacKey
, clientIP
)
681 capt
= captcha
.GimpCaptcha(self
.publicKey
, self
.secretKey
,
682 clientHMACKey
, self
.captchaDir
)
685 except captcha
.GimpCaptchaError
as error
:
687 except Exception as error
: # pragma: no cover
688 logging
.error("Unhandled error while retrieving Gimp captcha!")
689 logging
.exception(error
)
691 return (capt
.image
, capt
.challenge
)
693 def render_GET(self
, request
):
694 """Get a random CAPTCHA from our local cache directory and serve it to
697 :type request: :api:`twisted.web.http.Request`
698 :param request: A ``Request`` object for a page which should be
699 protected by a CAPTCHA.
701 :returns: A rendered HTML page containing a ReCaptcha challenge image
702 for the client to solve.
704 return CaptchaProtectedResource
.render_GET(self
, request
)
706 def render_POST(self
, request
):
707 """Process a client's CAPTCHA solution.
709 If the client's CAPTCHA solution is valid (according to
710 :meth:`checkSolution`), process and serve their original
711 request. Otherwise, redirect them back to a new CAPTCHA page.
713 :type request: :api:`twisted.web.http.Request`
714 :param request: A ``Request`` object, including POST arguments which
715 should include two key/value pairs: one key being
716 ``'captcha_challenge_field'``, and the other,
717 ``'captcha_response_field'``. These POST arguments should be
718 obtained from :meth:`render_GET`.
720 :returns: A rendered HTML page containing a ReCaptcha challenge image
721 for the client to solve.
723 return CaptchaProtectedResource
.render_POST(self
, request
)
726 class ReCaptchaProtectedResource(CaptchaProtectedResource
):
727 """A web resource which uses the reCaptcha_ service.
729 .. _reCaptcha: http://www.google.com/recaptcha
732 def __init__(self
, remoteIP
=None, **kwargs
):
733 CaptchaProtectedResource
.__init
__(self
, **kwargs
)
734 self
.remoteIP
= remoteIP
736 def _renderDeferred(self
, checkedRequest
):
737 """Render this resource asynchronously.
739 :type checkedRequest: tuple
740 :param checkedRequest: A tuple of ``(bool, request)``, as returned
741 from :meth:`checkSolution`.
744 valid
, request
= checkedRequest
745 except Exception as err
:
746 logging
.error("Error in _renderDeferred(): %s" % err
)
749 logging
.debug("Attemping to render %svalid request %r"
750 % ('' if valid
else 'in', request
))
753 rendered
= self
.resource
.render(request
)
754 except Exception as err
: # pragma: no cover
755 rendered
= replaceErrorPage(request
, err
)
757 logging
.info("Client failed a CAPTCHA; redirecting to %s"
759 rendered
= redirectTo(request
.uri
, request
)
762 request
.write(rendered
.encode('utf-8') if isinstance(rendered
, str) else rendered
)
764 except Exception as err
: # pragma: no cover
765 logging
.exception(err
)
769 def getCaptchaImage(self
, request
):
770 """Get a CAPTCHA image from the remote reCaptcha server.
772 :type request: :api:`twisted.web.http.Request`
773 :param request: A client's initial request for some other resource
774 which is protected by this one (i.e. protected by a CAPTCHA).
775 :returns: A 2-tuple of ``(image, challenge)``, where::
776 - ``image`` is a string holding a binary, JPEG-encoded image.
777 - ``challenge`` is a unique string associated with the request.
779 capt
= captcha
.ReCaptcha(self
.publicKey
, self
.secretKey
)
783 except Exception as error
:
784 logging
.fatal("Connection to Recaptcha server failed: %s" % error
)
786 if capt
.image
is None:
787 logging
.warn("No CAPTCHA image received from ReCaptcha server!")
789 return (capt
.image
, capt
.challenge
)
791 def getRemoteIP(self
):
792 """Mask the client's real IP address with a faked one.
794 The fake client IP address is sent to the reCaptcha server, and it is
795 either the public IP address of bridges.torproject.org (if the config
796 option ``RECAPTCHA_REMOTE_IP`` is configured), or a random IP.
799 :returns: A fake IP address to report to the reCaptcha API server.
802 remoteIP
= self
.remoteIP
804 # generate a random IP for the captcha submission
805 remoteIP
= IPv4Address(random
.randint(0, 2**32-1)).compressed
809 def checkSolution(self
, request
):
810 """Process a solved CAPTCHA by sending it to the ReCaptcha server.
812 The client's IP address is not sent to the ReCaptcha server; instead,
813 a completely random IP is generated and sent instead.
815 :type request: :api:`twisted.web.http.Request`
816 :param request: A ``Request`` object, including POST arguments which
817 should include two key/value pairs: one key being
818 ``'captcha_challenge_field'``, and the other,
819 ``'captcha_response_field'``. These POST arguments should be
820 obtained from :meth:`render_GET`.
821 :rtupe: :api:`twisted.internet.defer.Deferred`
822 :returns: A deferred which will callback with a tuple in the following
824 (:type:`bool`, :api:`twisted.web.server.Request`)
825 If the CAPTCHA solution was valid, a tuple will contain::
827 Otherwise, it will contain::
830 challenge
, response
= self
.extractClientSolution(request
)
831 clientIP
= self
.getClientIP(request
)
832 remoteIP
= self
.getRemoteIP()
834 logging
.debug("Captcha from %r. Parameters: %r"
835 % (clientIP
, request
.args
))
837 def checkResponse(solution
, request
):
838 """Check the :class:`txrecaptcha.RecaptchaResponse`.
840 :type solution: :class:`txrecaptcha.RecaptchaResponse`.
841 :param solution: The client's CAPTCHA solution, after it has been
842 submitted to the reCaptcha API server.
844 # This valid CAPTCHA result from this function cannot be reliably
845 # unittested, because it's callbacked to from the deferred
846 # returned by ``txrecaptcha.submit``, the latter of which would
847 # require networking (as well as automated CAPTCHA
848 # breaking). Hence, the 'no cover' pragma.
849 if solution
.is_valid
: # pragma: no cover
850 logging
.info("Valid CAPTCHA solution from %r." % clientIP
)
851 httpsMetrix
.recordValidHTTPSRequest(request
)
852 return (True, request
)
854 logging
.info("Invalid CAPTCHA solution from %r: %r"
855 % (clientIP
, solution
.error_code
))
856 httpsMetrix
.recordInvalidHTTPSRequest(request
)
857 return (False, request
)
859 d
= txrecaptcha
.submit(challenge
, response
, self
.secretKey
,
860 remoteIP
).addCallback(checkResponse
, request
)
863 def render_GET(self
, request
):
864 """Retrieve a ReCaptcha from the API server and serve it to the client.
866 :type request: :api:`twisted.web.http.Request`
867 :param request: A ``Request`` object for 'bridges.html'.
869 :returns: A rendered HTML page containing a ReCaptcha challenge image
870 for the client to solve.
872 return CaptchaProtectedResource
.render_GET(self
, request
)
874 def render_POST(self
, request
):
875 """Process a client's CAPTCHA solution.
877 If the client's CAPTCHA solution is valid (according to
878 :meth:`checkSolution`), process and serve their original
879 request. Otherwise, redirect them back to a new CAPTCHA page.
881 :type request: :api:`twisted.web.http.Request`
882 :param request: A ``Request`` object, including POST arguments which
883 should include two key/value pairs: one key being
884 ``'captcha_challenge_field'``, and the other,
885 ``'captcha_response_field'``. These POST arguments should be
886 obtained from :meth:`render_GET`.
887 :returns: :api:`twisted.web.server.NOT_DONE_YET`, in order to handle
888 the ``Deferred`` returned from :meth:`checkSolution`. Eventually,
889 when the ``Deferred`` request is done being processed,
890 :meth:`_renderDeferred` will handle rendering and displaying the
893 self
.setCSPHeader(request
)
894 request
.args
= stringifyRequestArgs(request
.args
)
895 d
= self
.checkSolution(request
)
896 d
.addCallback(self
._renderDeferred
)
900 class BridgesResource(CustomErrorHandlingResource
, CSPResource
):
901 """This resource displays bridge lines in response to a request."""
905 def __init__(self
, distributor
, schedule
, N
=1, useForwardedHeader
=False,
906 includeFingerprints
=True):
907 """Create a new resource for displaying bridges to a client.
909 :type distributor: :class:`HTTPSDistributor`
910 :param distributor: The mechanism to retrieve bridges for this
912 :type schedule: :class:`~bridgedb.schedule.ScheduledInterval`
913 :param schedule: The time period used to tweak the bridge selection
915 :param int N: The number of bridges to hand out per query.
916 :param bool useForwardedHeader: Whether or not we should use the the
917 X-Forwarded-For header instead of the source IP address.
918 :param bool includeFingerprints: Do we include the bridge's
919 fingerprint in the response?
921 gettext
.install("bridgedb")
922 CSPResource
.__init
__(self
)
923 self
.distributor
= distributor
924 self
.schedule
= schedule
925 self
.nBridgesToGive
= N
926 self
.useForwardedHeader
= useForwardedHeader
927 self
.includeFingerprints
= includeFingerprints
929 def render(self
, request
):
930 """Render a response for a client HTTP request.
932 Presently, this method merely wraps :meth:`getBridgeRequestAnswer` to
933 catch any unhandled exceptions which occur (otherwise the server will
934 display the traceback to the client). If an unhandled exception *does*
935 occur, the client will be served the default "No bridges currently
936 available" HTML response page.
938 :type request: :api:`twisted.web.http.Request`
939 :param request: A ``Request`` object containing the HTTP method, full
940 URI, and any URL/POST arguments and headers present.
942 :returns: A plaintext or HTML response to serve.
944 self
.setCSPHeader(request
)
945 request
.args
= stringifyRequestArgs(request
.args
)
948 response
= self
.getBridgeRequestAnswer(request
)
949 except Exception as err
:
950 logging
.exception(err
)
951 response
= self
.renderAnswer(request
)
953 return response
.encode('utf-8') if isinstance(response
, str) else response
955 def getClientIP(self
, request
):
956 """Get the client's IP address from the ``'X-Forwarded-For:'``
957 header, or from the :api:`request <twisted.web.server.Request>`.
959 :type request: :api:`twisted.web.http.Request`
960 :param request: A ``Request`` object for a
961 :api:`twisted.web.resource.Resource`.
962 :rtype: ``None`` or :any:`str`
963 :returns: The client's IP address, if it was obtainable.
965 return getClientIP(request
, self
.useForwardedHeader
)
967 def getBridgeRequestAnswer(self
, request
):
968 """Respond to a client HTTP request for bridges.
970 :type request: :api:`twisted.web.http.Request`
971 :param request: A ``Request`` object containing the HTTP method, full
972 URI, and any URL/POST arguments and headers present.
974 :returns: A plaintext or HTML response to serve.
977 interval
= self
.schedule
.intervalStart(time
.time())
978 ip
= self
.getClientIP(request
)
980 logging
.info("Replying to web request from %s. Parameters were %r"
981 % (ip
, request
.args
))
983 # Convert all key/value pairs from bytes to str.
985 for arg
, values
in request
.args
.items():
986 arg
= arg
if isinstance(arg
, str) else arg
.decode("utf-8")
987 values
= [value
.decode("utf-8") if isinstance(value
, bytes
) else value
for value
in values
]
988 str_args
[arg
] = values
989 request
.args
= str_args
992 bridgeRequest
= HTTPSBridgeRequest()
993 bridgeRequest
.client
= ip
994 bridgeRequest
.isValid(True)
995 bridgeRequest
.withIPversion(request
.args
)
996 bridgeRequest
.withPluggableTransportType(request
.args
)
997 bridgeRequest
.withoutBlockInCountry(request
)
998 bridgeRequest
.generateFilters()
1000 bridges
= self
.distributor
.getBridges(bridgeRequest
, interval
)
1001 bridgeLines
= [replaceControlChars(bridge
.getBridgeLine(
1002 bridgeRequest
, self
.includeFingerprints
)) for bridge
in bridges
]
1004 internalMetrix
.recordHandoutsPerBridge(bridgeRequest
, bridges
)
1006 if antibot
.isRequestFromBot(request
):
1007 transports
= bridgeRequest
.transports
1008 # Return either a decoy bridge or no bridge.
1009 if len(transports
) > 2:
1010 logging
.warning("More than one transport requested")
1011 return self
.renderAnswer(request
)
1012 ttype
= "vanilla" if len(transports
) == 0 else transports
[0]
1013 return self
.renderAnswer(request
, antibot
.getDecoyBridge(ttype
, bridgeRequest
.ipVersion
))
1015 return self
.renderAnswer(request
, bridgeLines
)
1017 def getResponseFormat(self
, request
):
1018 """Determine the requested format for the response.
1020 :type request: :api:`twisted.web.http.Request`
1021 :param request: A ``Request`` object containing the HTTP method, full
1022 URI, and any URL/POST arguments and headers present.
1023 :rtype: ``None`` or str
1024 :returns: The argument of the first occurence of the ``format=`` HTTP
1025 GET parameter, if any were present. (The only one which currently
1026 has any effect is ``format=plain``, see note in
1027 :meth:`renderAnswer`.) Otherwise, returns ``None``.
1029 format
= request
.args
.get("format", None)
1030 if format
and len(format
):
1031 format
= format
[0] # Choose the first arg
1034 def renderAnswer(self
, request
, bridgeLines
=None):
1035 """Generate a response for a client which includes **bridgesLines**.
1037 .. note: The generated response can be plain or HTML. A plain response
1040 voltron 1.2.3.4:1234 ABCDEF01234567890ABCDEF01234567890ABCDEF
1041 voltron 5.5.5.5:5555 0123456789ABCDEF0123456789ABCDEF01234567
1043 That is, there is no HTML, what you see is what you get, and what
1044 you get is suitable for pasting directly into Tor Launcher (or
1045 into a torrc, if you prepend ``"Bridge "`` to each line). The
1046 plain format can be requested from BridgeDB's web service by
1047 adding an ``&format=plain`` HTTP GET parameter to the URL. Also
1048 note that you won't get a QRCode, usage instructions, error
1049 messages, or any other fanciness if you use the plain format.
1051 :type request: :api:`twisted.web.http.Request`
1052 :param request: A ``Request`` object containing the HTTP method, full
1053 URI, and any URL/POST arguments and headers present.
1054 :type bridgeLines: list or None
1055 :param bridgeLines: A list of strings used to configure a Tor client
1056 to use a bridge. If ``None``, then the returned page will instead
1057 explain that there were no bridges of the type they requested,
1058 with instructions on how to proceed.
1060 :returns: A plaintext or HTML response to serve.
1063 format
= self
.getResponseFormat(request
)
1066 internalMetrix
.recordEmptyHTTPSResponse()
1068 if format
== 'plain':
1069 request
.setHeader("Content-Type", "text/plain")
1071 rendered
= '\n'.join(bridgeLines
).encode('utf-8')
1072 except Exception as err
:
1073 rendered
= replaceErrorPage(request
, err
, html
=False)
1075 request
.setHeader("Content-Type", "text/html; charset=utf-8")
1077 qrjpeg
= generateQR(bridgeLines
)
1080 qrcode
= b
'data:image/jpeg;base64,%s' % base64
.b64encode(qrjpeg
)
1081 qrcode
= qrcode
.decode("utf-8")
1084 langs
= translations
.getLocaleFromHTTPRequest(request
)
1085 rtl
= translations
.usingRTLLang(langs
)
1086 template
= lookup
.get_template('bridges.html')
1087 rendered
= template
.render(strings
,
1088 getSortedLangList(),
1091 langOverride
=translations
.isLangOverridden(request
),
1094 except Exception as err
:
1095 rendered
= replaceErrorPage(request
, err
)
1097 return rendered
.encode("utf-8") if isinstance(rendered
, str) else rendered
1100 def addWebServer(config
, distributor
):
1101 """Set up a web server for HTTP(S)-based bridge distribution.
1103 :type config: :class:`bridgedb.persistent.Conf`
1104 :param config: A configuration object from
1105 :mod:`bridgedb.main`. Currently, we use these options::
1106 HTTP_UNENCRYPTED_PORT
1107 HTTP_UNENCRYPTED_BIND_IP
1108 HTTP_USE_IP_FROM_FORWARDED_HEADER
1109 HTTPS_N_BRIDGES_PER_ANSWER
1110 HTTPS_INCLUDE_FINGERPRINTS
1115 HTTPS_USE_IP_FROM_FORWARDED_HEADER
1116 HTTPS_ROTATION_PERIOD
1121 GIMP_CAPTCHA_ENABLED
1123 GIMP_CAPTCHA_HMAC_KEYFILE
1124 GIMP_CAPTCHA_RSA_KEYFILE
1129 :type distributor: :class:`bridgedb.distributors.https.distributor.HTTPSDistributor`
1130 :param distributor: A bridge distributor.
1131 :raises SystemExit: if the servers cannot be started.
1132 :rtype: :api:`twisted.web.server.Site`
1133 :returns: A webserver.
1136 fwdHeaders
= config
.HTTP_USE_IP_FROM_FORWARDED_HEADER
1137 numBridges
= config
.HTTPS_N_BRIDGES_PER_ANSWER
1138 fprInclude
= config
.HTTPS_INCLUDE_FINGERPRINTS
1140 logging
.info("Starting web servers...")
1142 setFQDN(config
.SERVER_PUBLIC_FQDN
)
1144 index
= IndexResource()
1145 options
= OptionsResource()
1146 howto
= HowtoResource()
1147 info
= InfoResource()
1148 robots
= static
.File(os
.path
.join(TEMPLATE_DIR
, 'robots.txt'))
1149 assets
= static
.File(os
.path
.join(TEMPLATE_DIR
, 'assets/'))
1150 csp
= CSPResource(enabled
=config
.CSP_ENABLED
,
1151 includeSelf
=config
.CSP_INCLUDE_SELF
,
1152 reportViolations
=config
.CSP_REPORT_ONLY
,
1153 useForwardedHeader
=fwdHeaders
)
1155 root
= CustomErrorHandlingResource()
1156 root
.putChild(b
'', index
)
1157 root
.putChild(b
'robots.txt', robots
)
1158 root
.putChild(b
'assets', assets
)
1159 root
.putChild(b
'options', options
)
1160 root
.putChild(b
'howto', howto
)
1161 root
.putChild(b
'info', info
)
1162 root
.putChild(b
'maintenance', maintenance
)
1163 root
.putChild(b
'error', resource500
)
1164 root
.putChild(b
'malicious', resource400
)
1165 root
.putChild(CSPResource
.reportURI
, csp
)
1167 if config
.RECAPTCHA_ENABLED
:
1168 publicKey
= config
.RECAPTCHA_PUB_KEY
1169 secretKey
= config
.RECAPTCHA_SEC_KEY
1170 captcha
= partial(ReCaptchaProtectedResource
,
1171 remoteIP
=config
.RECAPTCHA_REMOTEIP
)
1172 elif config
.GIMP_CAPTCHA_ENABLED
:
1173 # Get the master HMAC secret key for CAPTCHA challenges, and then
1174 # create a new HMAC key from it for use on the server.
1175 captchaKey
= crypto
.getKey(config
.GIMP_CAPTCHA_HMAC_KEYFILE
)
1176 hmacKey
= crypto
.getHMAC(captchaKey
, "Captcha-Key")
1177 # Load or create our encryption keys:
1178 secretKey
, publicKey
= crypto
.getRSAKey(config
.GIMP_CAPTCHA_RSA_KEYFILE
)
1179 captcha
= partial(GimpCaptchaProtectedResource
,
1181 captchaDir
=config
.GIMP_CAPTCHA_DIR
)
1183 if config
.HTTPS_ROTATION_PERIOD
:
1184 count
, period
= config
.HTTPS_ROTATION_PERIOD
.split()
1185 sched
= ScheduledInterval(int(count
), period
)
1187 sched
= Unscheduled()
1189 bridges
= BridgesResource(distributor
, sched
, numBridges
, fwdHeaders
,
1190 includeFingerprints
=fprInclude
)
1192 # Protect the 'bridges' page with a CAPTCHA, if configured to do so:
1193 protected
= captcha(publicKey
=publicKey
,
1194 secretKey
=secretKey
,
1195 useForwardedHeader
=fwdHeaders
,
1196 protectedResource
=bridges
)
1197 root
.putChild(b
'bridges', protected
)
1198 logging
.info("Protecting resources with %s." % captcha
.func
.__name
__)
1200 root
.putChild(b
'bridges', bridges
)
1203 site
.displayTracebacks
= False
1205 if config
.HTTP_UNENCRYPTED_PORT
: # pragma: no cover
1206 ip
= config
.HTTP_UNENCRYPTED_BIND_IP
or ""
1207 port
= config
.HTTP_UNENCRYPTED_PORT
or 80
1209 reactor
.listenTCP(port
, site
, interface
=ip
)
1210 except CannotListenError
as error
:
1211 raise SystemExit(error
)
1212 logging
.info("Started HTTP server on %s:%d" % (str(ip
), int(port
)))
1214 if config
.HTTPS_PORT
: # pragma: no cover
1215 ip
= config
.HTTPS_BIND_IP
or ""
1216 port
= config
.HTTPS_PORT
or 443
1218 from twisted
.internet
.ssl
import DefaultOpenSSLContextFactory
1219 factory
= DefaultOpenSSLContextFactory(config
.HTTPS_KEY_FILE
,
1220 config
.HTTPS_CERT_FILE
)
1221 reactor
.listenSSL(port
, site
, factory
, interface
=ip
)
1222 except CannotListenError
as error
:
1223 raise SystemExit(error
)
1224 logging
.info("Started HTTPS server on %s:%d" % (str(ip
), int(port
)))