1 # -*- coding: utf-8 ; test-case-name: bridgedb.test.test_distributors_moat_server -*-
3 # This file is part of BridgeDB, a Tor bridge distribution system.
5 # :authors: please see included AUTHORS file
6 # :copyright: (c) 2017, The Tor Project, Inc.
7 # (c) 2017, Isis Lovecruft
8 # :license: see LICENSE for licensing information
11 .. py:module:: bridgedb.distributors.moat.server
12 :synopsis: Server which implements JSON API to interface with Tor Browser
13 clients through a meek tunnel.
15 bridgedb.distributors.moat.server
16 =================================
18 Server which implements JSON API to interface with Tor Browser clients through a
21 .. inheritance-diagram:: JsonAPIResource JsonAPIErrorResource CustomErrorHandlingResource JsonAPIDataResource CaptchaResource CaptchaCheckResource CaptchaFetchResource
25 from __future__
import print_function
32 from functools
import partial
34 from ipaddr
import IPAddress
36 from twisted
.internet
import reactor
37 from twisted
.internet
.error
import CannotListenError
38 from twisted
.web
import resource
39 from twisted
.web
.server
import Site
41 from bridgedb
import metrics
42 from bridgedb
import captcha
43 from bridgedb
import crypto
44 from bridgedb
import antibot
45 from bridgedb
.distributors
.common
.http
import setFQDN
46 from bridgedb
.distributors
.common
.http
import getFQDN
47 from bridgedb
.distributors
.common
.http
import getClientIP
48 from bridgedb
.distributors
.moat
.request
import MoatBridgeRequest
49 from bridgedb
.qrcodes
import generateQR
50 from bridgedb
.schedule
import Unscheduled
51 from bridgedb
.schedule
import ScheduledInterval
52 from bridgedb
.util
import replaceControlChars
54 # We use our metrics singletons to keep track of BridgeDB metrics such as
55 # "number of failed HTTPS bridge requests."
56 moatMetrix
= metrics
.MoatMetrics()
57 internalMetrix
= metrics
.InternalMetrics()
60 #: The current version of the moat JSON API that we speak
61 MOAT_API_VERSION
= '0.1.0'
63 #: The root path to resources for the moat server
64 SERVER_PUBLIC_ROOT
= None
66 #: An ordered list of the preferred transports which moat should
67 #: distribute, in order from most preferable to least preferable.
68 TRANSPORT_PREFERENCE_LIST
= None
70 #: All of the pluggable transports BridgeDB currently supports.
71 SUPPORTED_TRANSPORTS
= None
75 """Get the server's public FQDN plus the root directory for the web server.
80 if not root
.startswith('/') and not fqdn
.endswith('/'):
81 return '/'.join([fqdn
, root
])
83 return ''.join([fqdn
, root
])
86 """Set the global :data:`SERVER_PUBLIC_ROOT` variable.
88 :param str root: The path to the root directory for the web server.
90 logging
.info("Setting Moat server public root to %r" % root
)
92 global SERVER_PUBLIC_ROOT
93 SERVER_PUBLIC_ROOT
= root
96 """Get the setting for the HTTP server's public FQDN from the global
97 :data:`SERVER_PUBLIC_FQDN variable.
101 return SERVER_PUBLIC_ROOT
103 def setPreferredTransports(preferences
):
104 """Set the global ``TRANSPORT_PREFERENCE_LIST``."""
105 global TRANSPORT_PREFERENCE_LIST
106 TRANSPORT_PREFERENCE_LIST
= preferences
108 def getPreferredTransports():
109 """Get the global ``TRANSPORT_PREFERENCE_LIST``.
112 :returns: A list of preferences for which pluggable transports to distribute
115 return TRANSPORT_PREFERENCE_LIST
117 def setSupportedTransports(transports
):
118 """Set the global ``SUPPORTED_TRANSPORTS``.
120 :param dist transports: The ``SUPPORTED_TRANSPORTS`` dict from a
123 supported
= [k
for (k
, v
) in transports
.items() if v
]
125 if not "vanilla" in supported
:
126 supported
.append("vanilla")
128 global SUPPORTED_TRANSPORTS
129 SUPPORTED_TRANSPORTS
= supported
131 def getSupportedTransports():
132 """Get the global ``SUPPORTED_TRANSPORTS``.
135 :returns: A list all pluggable transports we support.
137 return SUPPORTED_TRANSPORTS
140 class JsonAPIResource(resource
.Resource
):
141 """A resource which conforms to the `JSON API spec <http://jsonapi.org/>`__.
144 def __init__(self
, useForwardedHeader
=True, skipLoopback
=False):
145 """Create a JSON API resource, containing either error(s) or data.
147 :param bool useForwardedHeader: If ``True``, obtain the client's IP
148 address from the ``X-Forwarded-For`` HTTP header.
149 :param bool skipLoopback: Skip loopback addresses when parsing the
150 X-Forwarded-For header.
152 resource
.Resource
.__init
__(self
)
153 self
.useForwardedHeader
= useForwardedHeader
154 self
.skipLoopback
= skipLoopback
156 def getClientIP(self
, request
):
157 """Get the client's IP address from the ``'X-Forwarded-For:'``
158 header, or from the :api:`request <twisted.web.server.Request>`.
160 :type request: :api:`twisted.web.http.Request`
161 :param request: A ``Request`` for a
162 :api:`twisted.web.resource.Resource`.
163 :rtype: ``None`` or :any:`str`
164 :returns: The client's IP address, if it was obtainable.
166 return getClientIP(request
, self
.useForwardedHeader
, self
.skipLoopback
)
168 def formatDataForResponse(self
, data
, request
):
169 """Format a dictionary of ``data`` into JSON and add necessary response
172 This method will set the appropriate response headers:
173 * `Content-Type: application/vnd.api+json`
174 * `Server: moat/VERSION`
177 :param data: Some data to respond with. This will be formatted as JSON.
178 :type request: :api:`twisted.web.http.Request`
179 :param request: A ``Request`` for a :api:`twisted.web.resource.Resource`.
180 :returns: The encoded data.
182 request
.responseHeaders
.addRawHeader("Content-Type", "application/vnd.api+json")
183 request
.responseHeaders
.addRawHeader("Server", "moat/%s" % MOAT_API_VERSION
)
186 rendered
= json
.dumps(data
).encode("utf-8")
193 class JsonAPIErrorResource(JsonAPIResource
):
194 """A JSON API resource which explains that some error has occured."""
198 def __init__(self
, id=0, type="", code
=200, status
="OK", detail
=""):
199 """Create a :api:`twisted.web.resource.Resource` for a JSON API errors
202 resource
.Resource
.__init
__(self
)
209 def render_GET(self
, request
):
210 # status codes and messages are at the JSON API layer, not HTTP layer:
215 'version': MOAT_API_VERSION
,
217 'status': self
.status
,
218 'detail': self
.detail
,
221 return self
.formatDataForResponse(data
, request
)
223 render_POST
= render_GET
226 resource403
= JsonAPIErrorResource(code
=403, status
="Forbidden")
227 resource404
= JsonAPIErrorResource(code
=404, status
="Not Found")
228 resource406
= JsonAPIErrorResource(code
=406, status
="Not Acceptable")
229 resource415
= JsonAPIErrorResource(code
=415, status
="Unsupported Media Type")
230 resource419
= JsonAPIErrorResource(code
=419, status
="No You're A Teapot")
231 resource501
= JsonAPIErrorResource(code
=501, status
="Not Implemented")
234 class CustomErrorHandlingResource(resource
.Resource
):
235 """A :api:`twisted.web.resource.Resource` which wraps the
236 :api:`twisted.web.resource.Resource.getChild` method in order to use
237 custom error handling pages.
239 def getChild(self
, path
, request
):
240 logging
.debug("[501] %s %s" % (request
.method
, request
.uri
))
242 response
= resource501
243 response
.detail
= "moat version %s does not implement %s %s" % \
244 (MOAT_API_VERSION
, request
.method
.decode('utf-8'), request
.uri
.decode('utf-8'))
248 class JsonAPIDataResource(JsonAPIResource
):
249 """A resource which returns some JSON API data."""
251 def __init__(self
, useForwardedHeader
=True, skipLoopback
=False):
252 JsonAPIResource
.__init
__(self
, useForwardedHeader
, skipLoopback
)
254 def checkRequestHeaders(self
, request
):
255 """The JSON API specification requires servers to respond with certain HTTP
256 status codes and message if the client's request headers are inappropriate in
257 any of the following ways:
259 * Servers MUST respond with a 415 Unsupported Media Type status code if
260 a request specifies the header Content-Type: application/vnd.api+json
261 with any media type parameters.
263 * Servers MUST respond with a 406 Not Acceptable status code if a
264 request’s Accept header contains the JSON API media type and all
265 instances of that media type are modified with media type parameters.
267 supports_json_api
= False
268 accept_json_api_header
= False
269 accept_header_is_ok
= False
271 if request
.requestHeaders
.hasHeader("Content-Type"):
272 headers
= request
.requestHeaders
.getRawHeaders("Content-Type")
273 # The "pragma: no cover"s are because, no matter what I do, I cannot
274 # for the life of me trick twisted's test infrastructure to not send
275 # some variant of these headers. ¯\_(ツ)_/¯
276 for contentType
in headers
: # pragma: no cover
277 # The request must have the Content-Type set to 'application/vnd.api+json':
278 if contentType
== 'application/vnd.api+json':
279 supports_json_api
= True
280 # The request must not specify a Content-Type with media parameters:
281 if ';' in contentType
:
282 supports_json_api
= False
284 if not supports_json_api
:
287 # If the request has an Accept header which contains
288 # 'application/vnd.api+json' then at least one instance of that type
289 # must have no parameters:
290 if request
.requestHeaders
.hasHeader("Accept"): # pragma: no cover
291 headers
= request
.requestHeaders
.getRawHeaders("Accept")
292 for accept
in headers
:
293 if accept
.startswith('application/vnd.api+json'):
294 accept_json_api_header
= True
295 if ';' not in accept
:
296 accept_header_is_ok
= True
298 if accept_json_api_header
and not accept_header_is_ok
: # pragma: no cover
302 class CaptchaResource(JsonAPIDataResource
):
305 def __init__(self
, hmacKey
=None, publicKey
=None, secretKey
=None,
306 useForwardedHeader
=True, skipLoopback
=False):
307 JsonAPIDataResource
.__init
__(self
, useForwardedHeader
, skipLoopback
)
308 self
.hmacKey
= hmacKey
309 self
.publicKey
= publicKey
310 self
.secretKey
= secretKey
313 class CaptchaFetchResource(CaptchaResource
):
314 """A resource to retrieve a CAPTCHA challenge."""
318 def __init__(self
, hmacKey
=None, publicKey
=None, secretKey
=None,
319 captchaDir
="captchas", useForwardedHeader
=True, skipLoopback
=False):
322 :param bytes hmacKey: The master HMAC key, used for validating CAPTCHA
323 challenge strings in :meth:`captcha.GimpCaptcha.check`. The file
324 where this key is stored can be set via the
325 ``GIMP_CAPTCHA_HMAC_KEYFILE`` option in the config file.
326 are stored. See the ``GIMP_CAPTCHA_DIR`` config setting.
327 :param str secretkey: A PKCS#1 OAEP-padded, private RSA key, used for
328 verifying the client's solution to the CAPTCHA. See
329 :func:`bridgedb.crypto.getRSAKey` and the
330 ``GIMP_CAPTCHA_RSA_KEYFILE`` config setting.
331 :param str publickey: A PKCS#1 OAEP-padded, public RSA key, used for
332 creating the ``captcha_challenge_field`` string to give to a
334 :param str captchaDir: The directory where the cached CAPTCHA images
335 :param bool useForwardedHeader: If ``True``, obtain the client's IP
336 address from the ``X-Forwarded-For`` HTTP header.
337 :param bool skipLoopback: Skip loopback addresses when parsing the
338 X-Forwarded-For header.
340 CaptchaResource
.__init
__(self
, hmacKey
, publicKey
, secretKey
,
342 self
.captchaDir
= captchaDir
343 self
.supportedTransports
= getSupportedTransports()
345 def getCaptchaImage(self
, request
):
346 """Get a random CAPTCHA image from our **captchaDir**.
348 Creates a :class:`~bridgedb.captcha.GimpCaptcha`, and calls its
349 :meth:`~bridgedb.captcha.GimpCaptcha.get` method to return a random
350 CAPTCHA and challenge string.
352 :type request: :api:`twisted.web.http.Request`
353 :param request: A client's initial request for some other resource
354 which is protected by this one (i.e. protected by a CAPTCHA).
355 :returns: A 2-tuple of ``(image, challenge)``, where::
356 - ``image`` is a string holding a binary, JPEG-encoded image.
357 - ``challenge`` is a unique string associated with the request.
359 # Create a new HMAC key, specific to requests from this client:
360 clientIP
= self
.getClientIP(request
)
361 clientHMACKey
= crypto
.getHMAC(self
.hmacKey
, clientIP
)
362 capt
= captcha
.GimpCaptcha(self
.publicKey
, self
.secretKey
,
363 clientHMACKey
, self
.captchaDir
)
366 except captcha
.GimpCaptchaError
as error
:
368 except Exception as impossible
:
369 logging
.error("Unhandled error while retrieving Gimp captcha!")
370 logging
.error(impossible
)
372 return (capt
.image
, capt
.challenge
.decode('utf-8') if isinstance(capt
.challenge
, bytes
) else capt
.challenge
)
374 def getPreferredTransports(self
, supportedTransports
):
375 """Choose which transport a client should request, based on their list
376 of ``supportedTransports``.
378 :param list supportedTransports: A list of transports the client
379 reported that they support (as returned from
380 :meth:`~bridgedb.distributors.moat.server.CaptchaFetchResource.extractSupportedTransports`).
382 :returns: A string specifying the chosen transport, provided there is an
383 overlap between which transports BridgeDB and the client support.
384 Otherwise, if there is no overlap, returns a list of all the
385 transports which BridgeDB *does* support.
387 preferenceOrder
= getPreferredTransports()
390 for pt
in preferenceOrder
:
391 if pt
in supportedTransports
:
394 # If we couldn't pick the best one that we both support, return the
395 # whole list of what we're able to distribute:
397 preferred
= getSupportedTransports()
401 def extractSupportedTransports(self
, request
):
402 """Extract the transports a client supports from their POST request.
404 :param str request: A JSON blob containing the following
406 * "version": The moat protocol version.
407 * "type": "client-transports".
408 * "supported": ['TRANSPORT', … ]
410 * TRANSPORT is a string identifying a transport, e.g. "obfs4".
412 :returns: The list of transports the client supports.
417 encoded_data
= request
.content
.read()
418 data
= json
.loads(encoded_data
)["data"][0]
420 if data
["type"] != "client-transports":
422 "Bad JSON API object type: expected %s got %s" %
423 ('client-transports', data
["type"]))
424 elif data
["version"] != MOAT_API_VERSION
:
426 "Client requested protocol version %s, but we're using %s" %
427 (data
["version"], MOAT_API_VERSION
))
428 elif not data
["supported"]:
430 "Client didn't provide any supported transports")
432 supported
= data
["supported"]
433 except KeyError as err
:
434 logging
.debug(("Error processing client POST request: Client JSON "
435 "API data missing '%s' field") % (err
))
436 except ValueError as err
:
437 logging
.warn("Error processing client POST request: %s" % err
)
438 except Exception as impossible
:
439 logging
.error("Unhandled error while extracting moat client transports!")
440 logging
.error(impossible
)
444 def render_POST(self
, request
):
445 """Retrieve a captcha from the moat API server and serve it to the client.
447 :type request: :api:`twisted.web.http.Request`
448 :param request: A ``Request`` object for a CAPTCHA.
450 :returns: A JSON blob containing the following fields:
451 * "version": The moat protocol version.
452 * "image": A base64-encoded CAPTCHA JPEG image.
453 * "challenge": A base64-encoded, encrypted challenge. The client
454 will need to hold on to the and pass it back later, along with
455 their challenge response.
456 * "error": An ASCII error message.
457 Any of the above JSON fields may be "null".
459 error
= self
.checkRequestHeaders(request
)
461 if error
: # pragma: no cover
462 return error
.render(request
)
464 supported
= self
.extractSupportedTransports(request
)
465 preferred
= self
.getPreferredTransports(supported
)
466 image
, challenge
= self
.getCaptchaImage(request
)
471 'type': 'moat-challenge',
472 'version': MOAT_API_VERSION
,
473 'transport': preferred
,
475 'challenge': challenge
, # The challenge is already base64-encoded
480 data
["data"][0]["image"] = base64
.b64encode(image
).decode('utf-8')
481 except Exception as impossible
:
482 logging
.error("Could not construct or encode captcha!")
483 logging
.error(impossible
)
485 return self
.formatDataForResponse(data
, request
)
488 class CaptchaCheckResource(CaptchaResource
):
489 """A resource to verify a CAPTCHA solution and distribute bridges."""
493 def __init__(self
, distributor
, schedule
, N
=1,
494 hmacKey
=None, publicKey
=None, secretKey
=None,
495 useForwardedHeader
=True, skipLoopback
=False):
496 """Create a new resource for checking CAPTCHA solutions and returning
499 :type distributor: :class:`MoatDistributor`
500 :param distributor: The mechanism to retrieve bridges for this
502 :type schedule: :class:`~bridgedb.schedule.ScheduledInterval`
503 :param schedule: The time period used to tweak the bridge selection
505 :param int N: The number of bridges to hand out per query.
506 :param bool useForwardedHeader: Whether or not we should use the the
507 X-Forwarded-For header instead of the source IP address.
508 :param bool skipLoopback: Skip loopback addresses when parsing the
509 X-Forwarded-For header.
511 CaptchaResource
.__init
__(self
, hmacKey
, publicKey
, secretKey
,
513 self
.distributor
= distributor
514 self
.schedule
= schedule
515 self
.nBridgesToGive
= N
516 self
.useForwardedHeader
= useForwardedHeader
518 def createBridgeRequest(self
, ip
, data
):
519 """Create an appropriate :class:`MoatBridgeRequest` from the ``data``
520 of a client's request.
522 :param str ip: The client's IP address.
523 :param dict data: The decoded JSON API data from the client's request.
524 :rtype: :class:`MoatBridgeRequest`
525 :returns: An object which specifies the filters for retreiving
526 the type of bridges that the client requested.
528 logging
.debug("Creating moat bridge request object for %s." % ip
)
530 bridgeRequest
= MoatBridgeRequest()
533 bridgeRequest
.client
= IPAddress(ip
)
534 bridgeRequest
.isValid(True)
535 bridgeRequest
.withIPversion()
536 bridgeRequest
.withPluggableTransportType(data
)
537 bridgeRequest
.withoutBlockInCountry(data
)
538 bridgeRequest
.generateFilters()
542 def getBridges(self
, bridgeRequest
):
543 """Get bridges for a client's HTTP request.
545 :type bridgeRequest: :class:`MoatBridgeRequest`
546 :param bridgeRequest: A valid bridge request object with pre-generated
547 filters (as returned by :meth:`createBridgeRequest`).
549 :return: A list of :class:`~bridgedb.bridges.Bridge`s.
552 interval
= self
.schedule
.intervalStart(time
.time())
554 logging
.debug("Replying to JSON API request from %s." % bridgeRequest
.client
)
556 if bridgeRequest
.isValid():
557 bridges
= self
.distributor
.getBridges(bridgeRequest
, interval
)
561 def getBridgeLines(self
, bridgeRequest
, bridges
):
563 :type bridgeRequest: :class:`MoatBridgeRequest`
564 :param bridgeRequest: A valid bridge request object with pre-generated
565 filters (as returned by :meth:`createBridgeRequest`).
566 :param list bridges: A list of :class:`~bridgedb.bridges.Bridge`
569 :return: A list of bridge lines.
571 return [replaceControlChars(bridge
.getBridgeLine(bridgeRequest
))
572 for bridge
in bridges
]
574 def extractClientSolution(self
, data
):
575 """Extract the client's CAPTCHA solution from a POST request.
577 This is used after receiving a POST request from a client (which
578 should contain their solution to the CAPTCHA), to extract the solution
579 and challenge strings.
581 :param dict data: The decoded JSON API data from the client's request.
582 :returns: A redirect for a request for a new CAPTCHA if there was a
583 problem. Otherwise, returns a 2-tuple of strings, the first is the
584 client's CAPTCHA solution from the text input area, and the second
585 is the challenge string.
589 challenge
, solution
= None, None
592 if data
["type"] != "moat-solution":
594 "Bad JSON API object type: expected %s got %s" %
595 ("moat-solution", data
["type"]))
596 elif data
["id"] != "2":
598 "Bad JSON API data id: expected 2 got %s" %
600 elif data
["version"] != MOAT_API_VERSION
:
602 "Client requested protocol version %s, but we're using %s" %
603 (data
["version"], MOAT_API_VERSION
))
604 elif data
["transport"] not in getSupportedTransports():
606 "Transport '%s' is not currently supported" %
609 qrcode
= True if data
["qrcode"] == "true" else False
610 transport
= type('')(data
["transport"])
611 challenge
= type('')(data
["challenge"])
612 solution
= type('')(data
["solution"])
613 except KeyError as err
:
614 logging
.warn(("Error processing client POST request: "
615 "Client JSON API data missing '%s' field.") % err
)
616 except ValueError as err
:
617 logging
.warn("Error processing client POST request: %s" % err
)
618 except Exception as impossible
:
619 logging
.error(impossible
)
621 return (qrcode
, transport
, challenge
, solution
)
623 def checkSolution(self
, challenge
, solution
, clientIP
):
624 """Process a solved CAPTCHA via
625 :meth:`bridgedb.captcha.GimpCaptcha.check`.
627 :param str challenge: A base64-encoded, encrypted challenge.
628 :param str solution: The client's solution to the captcha
629 :param str clientIP: The client's IP address.
631 :returns: True, if the CAPTCHA solution was valid; False otherwise.
634 clientHMACKey
= crypto
.getHMAC(self
.hmacKey
, clientIP
)
637 valid
= captcha
.GimpCaptcha
.check(challenge
, solution
,
638 self
.secretKey
, clientHMACKey
)
639 except Exception as impossible
:
640 logging
.error(impossible
)
643 logging
.debug("%sorrect captcha from %r: %r." %
644 ("C" if valid
else "Inc", clientIP
, solution
))
648 def failureResponse(self
, id, request
, bridgeRequest
=None):
649 """Respond with status code "419 No You're A Teapot" if the captcha
650 verification failed, or status code "404 Not Found" if there
651 were none of the type of bridges requested.
653 :param int id: The JSON API "id" field of the
654 ``JsonAPIErrorResource`` which should be returned.
655 :type request: :api:`twisted.web.http.Request`
656 :param request: The current request we're handling.
657 :type bridgeRequest: :class:`MoatBridgeRequest`
658 :param bridgeRequest: A valid bridge request object with pre-generated
659 filters (as returned by :meth:`createBridgeRequest`).
662 error_response
= resource419
663 error_response
.id = "4"
664 error_response
.detail
= "The CAPTCHA solution was incorrect."
666 error_response
= resource419
667 error_response
.id = "5"
668 error_response
.detail
= "The CAPTCHA challenge timed out."
670 error_response
= resource404
671 error_response
.id = "6"
672 error_response
.detail
= ("No bridges available to fulfill "
673 "request: %s.") % bridgeRequest
675 error_response
.type = 'moat-bridges'
677 return error_response
.render(request
)
679 def render_POST(self
, request
):
680 """Process a client's CAPTCHA solution.
682 If the client's CAPTCHA solution is valid (according to
683 :meth:`checkSolution`), process and serve their original
684 request. Otherwise, redirect them back to a new CAPTCHA page.
686 :type request: :api:`twisted.web.http.Request`
687 :param request: A ``Request`` object, including POST arguments which
688 should include two key/value pairs: one key being
689 ``'captcha_challenge_field'``, and the other,
690 ``'captcha_response_field'``. These POST arguments should be
691 obtained from :meth:`render_GET`.
693 :returns: A rendered HTML page containing a ReCaptcha challenge image
694 for the client to solve.
697 error
= self
.checkRequestHeaders(request
)
699 if error
: # pragma: no cover
700 logging
.debug("Error while checking moat request headers.")
701 moatMetrix
.recordInvalidMoatRequest(request
)
702 return error
.render(request
)
707 "type": 'moat-bridges',
708 "version": MOAT_API_VERSION
,
715 pos
= request
.content
.tell()
716 encoded_client_data
= request
.content
.read()
717 # We rewind the stream to its previous position to allow the
718 # metrix module to read the request's content too.
719 request
.content
.seek(pos
)
720 client_data
= json
.loads(encoded_client_data
)["data"][0]
721 clientIP
= self
.getClientIP(request
)
723 (include_qrcode
, transport
,
724 challenge
, solution
) = self
.extractClientSolution(client_data
)
726 valid
= self
.checkSolution(challenge
, solution
, clientIP
)
727 except captcha
.CaptchaExpired
:
728 logging
.debug("The challenge had timed out")
729 moatMetrix
.recordInvalidMoatRequest(request
)
730 return self
.failureResponse(5, request
)
731 except Exception as impossible
:
732 logging
.warn("Unhandled exception while processing a POST /fetch request!")
733 logging
.error(impossible
)
734 moatMetrix
.recordInvalidMoatRequest(request
)
735 return self
.failureResponse(4, request
)
739 bridgeRequest
= self
.createBridgeRequest(clientIP
, client_data
)
740 bridges
= self
.getBridges(bridgeRequest
)
741 bridgeLines
= self
.getBridgeLines(bridgeRequest
, bridges
)
742 moatMetrix
.recordValidMoatRequest(request
)
744 # If we can only return less than the configured
745 # MOAT_BRIDGES_PER_ANSWER then log a warning.
746 if len(bridgeLines
) < self
.nBridgesToGive
:
747 logging
.warn(("Not enough bridges of the type specified to "
748 "fulfill the following request: %s") % bridgeRequest
)
750 internalMetrix
.recordEmptyMoatResponse()
752 internalMetrix
.recordHandoutsPerBridge(bridgeRequest
, bridges
)
754 if antibot
.isRequestFromBot(request
):
755 ttype
= transport
or "vanilla"
756 bridgeLines
= antibot
.getDecoyBridge(ttype
,
757 bridgeRequest
.ipVersion
)
759 # If we have no bridges at all to give to the client, then
760 # return a JSON API 404 error.
762 return self
.failureResponse(6, request
)
765 qrjpeg
= generateQR(bridgeLines
)
767 qrcode
= 'data:image/jpeg;base64,%s' % base64
.b64encode(qrjpeg
)
769 data
["data"][0]["qrcode"] = qrcode
770 data
["data"][0]["bridges"] = bridgeLines
772 return self
.formatDataForResponse(data
, request
)
774 moatMetrix
.recordInvalidMoatRequest(request
)
775 return self
.failureResponse(4, request
)
778 def addMoatServer(config
, distributor
):
779 """Set up a web server for moat bridge distribution.
781 :type config: :class:`bridgedb.persistent.Conf`
782 :param config: A configuration object from
783 :mod:`bridgedb.main`. Currently, we use these options::
788 MOAT_DIST_VIA_MEEK_ONLY
791 MOAT_SERVER_PUBLIC_ROOT
796 MOAT_BRIDGES_PER_ANSWER
797 MOAT_TRANSPORT_PREFERENCE_LIST
798 MOAT_USE_IP_FROM_FORWARDED_HEADER
799 MOAT_SKIP_LOOPBACK_ADDRESSES
801 MOAT_GIMP_CAPTCHA_HMAC_KEYFILE
802 MOAT_GIMP_CAPTCHA_RSA_KEYFILE
803 :type distributor: :class:`bridgedb.distributors.moat.distributor.MoatDistributor`
804 :param distributor: A bridge distributor.
805 :raises SystemExit: if the servers cannot be started.
806 :rtype: :api:`twisted.web.server.Site`
807 :returns: A webserver.
810 fwdHeaders
= config
.MOAT_USE_IP_FROM_FORWARDED_HEADER
811 numBridges
= config
.MOAT_BRIDGES_PER_ANSWER
812 skipLoopback
= config
.MOAT_SKIP_LOOPBACK_ADDRESSES
814 logging
.info("Starting moat servers...")
816 setFQDN(config
.SERVER_PUBLIC_FQDN
)
817 setRoot(config
.MOAT_SERVER_PUBLIC_ROOT
)
818 setSupportedTransports(config
.SUPPORTED_TRANSPORTS
)
819 setPreferredTransports(config
.MOAT_TRANSPORT_PREFERENCE_LIST
)
821 # Get the master HMAC secret key for CAPTCHA challenges, and then
822 # create a new HMAC key from it for use on the server.
823 captchaKey
= crypto
.getKey(config
.MOAT_GIMP_CAPTCHA_HMAC_KEYFILE
)
824 hmacKey
= crypto
.getHMAC(captchaKey
, "Moat-Captcha-Key")
825 # Load or create our encryption keys:
826 secretKey
, publicKey
= crypto
.getRSAKey(config
.MOAT_GIMP_CAPTCHA_RSA_KEYFILE
)
827 sched
= Unscheduled()
829 if config
.MOAT_ROTATION_PERIOD
:
830 count
, period
= config
.MOAT_ROTATION_PERIOD
.split()
831 sched
= ScheduledInterval(count
, period
)
833 sitePublicDir
= getRoot()
835 meek
= CustomErrorHandlingResource()
836 moat
= CustomErrorHandlingResource()
837 fetch
= CaptchaFetchResource(hmacKey
, publicKey
, secretKey
,
838 config
.GIMP_CAPTCHA_DIR
,
839 fwdHeaders
, skipLoopback
)
840 check
= CaptchaCheckResource(distributor
, sched
, numBridges
,
841 hmacKey
, publicKey
, secretKey
,
842 fwdHeaders
, skipLoopback
)
844 moat
.putChild(b
"fetch", fetch
)
845 moat
.putChild(b
"check", check
)
846 meek
.putChild(b
"moat", moat
)
848 root
= CustomErrorHandlingResource()
849 root
.putChild(b
"meek", meek
)
850 root
.putChild(b
"moat", moat
)
853 site
.displayTracebacks
= False
855 if config
.MOAT_HTTP_PORT
: # pragma: no cover
856 ip
= config
.MOAT_HTTP_IP
or ""
857 port
= config
.MOAT_HTTP_PORT
or 80
859 reactor
.listenTCP(port
, site
, interface
=ip
)
860 except CannotListenError
as error
:
861 raise SystemExit(error
)
862 logging
.info("Started Moat HTTP server on %s:%d" % (str(ip
), int(port
)))
864 if config
.MOAT_HTTPS_PORT
: # pragma: no cover
865 ip
= config
.MOAT_HTTPS_IP
or ""
866 port
= config
.MOAT_HTTPS_PORT
or 443
868 from twisted
.internet
.ssl
import DefaultOpenSSLContextFactory
869 factory
= DefaultOpenSSLContextFactory(config
.MOAT_TLS_KEY_FILE
,
870 config
.MOAT_TLS_CERT_FILE
)
871 reactor
.listenSSL(port
, site
, factory
, interface
=ip
)
872 except CannotListenError
as error
:
873 raise SystemExit(error
)
874 logging
.info("Started Moat TLS server on %s:%d" % (str(ip
), int(port
)))