예제 #1
0
def test_response_cookie():
    response = Response()
    response.headers.cookie[str('foo')] = str('bar')
    def start_response(status, headers):
        assert headers[0][0] == str('Set-Cookie')
        assert headers[0][1].startswith(str('foo=bar'))
    response.to_wsgi({}, start_response, 'utf8')
예제 #2
0
def test_response_body_as_bytestring_results_in_an_iterable():
    response = Response(body=b"Greetings, program!")
    def start_response(status, headers):
        pass
    expected = [b"Greetings, program!"]
    actual = list(response.to_wsgi({}, start_response, 'utf8').body)
    assert actual == expected
예제 #3
0
def test_response_to_wsgi():
    response = Response(body=b"Greetings, program!")
    def start_response(status, headers):
        pass
    expected = [b"Greetings, program!"]
    actual = list(response.to_wsgi({}, start_response, 'utf8').body)
    assert actual == expected
예제 #4
0
def turn_socket_error_into_50X(website,
                               state,
                               exception,
                               _=lambda a: a,
                               response=None):
    # The mangopay module reraises exceptions and stores the original in `__cause__`.
    if isinstance(exception, mangopay.exceptions.APIError):
        exception = exception.__cause__
    # replace the exception with a Response
    if isinstance(exception, Timeout) or 'timeout' in str(exception).lower():
        response = response or Response()
        response.code = 504
    elif isinstance(exception, (OSError, ConnectionError)):
        response = response or Response()
        response.code = 502
    else:
        return
    # log the exception
    website.tell_sentry(exception, state, level='warning')
    # show a proper error message
    response.body = _(
        "Processing your request failed because our server was unable to communicate "
        "with a service located on another machine. This is a temporary issue, please "
        "try again later.")
    return {'response': response, 'exception': None}
예제 #5
0
def test_response_body_as_iterable_comes_through_untouched():
    response = Response(body=[b"Greetings, ", b"program!"])
    def start_response(status, headers):
        pass
    expected = [b"Greetings, ", b"program!"]
    actual = list(response.to_wsgi({}, start_response, 'utf8').body)
    assert actual == expected
예제 #6
0
def word(mapping, k, pattern=r'^\w+$', unicode=False):
    r = mapping[k]
    if not r:
        raise Response().error(400, "`%s` value %r is empty" % (k, r))
    if not re.match(pattern, r, re.UNICODE if unicode else re.ASCII):
        raise Response().error(400, "`%s` value %r contains forbidden characters" % (k, r))
    return r
예제 #7
0
def test_response_headers_are_str():
    response = Response()
    response.headers[b'Location'] = b'somewhere'
    def start_response(status, headers):
        assert isinstance(headers[0][0], str)
        assert isinstance(headers[0][1], str)
    response.to_wsgi({}, start_response, 'utf8')
예제 #8
0
def handle_negotiation_exception(exception):
    if isinstance(exception, NotFound):
        response = Response(404)
    elif isinstance(exception, NegotiationFailure):
        response = Response(406, exception.message)
    else:
        return
    return {'response': response, 'exception': None}
예제 #9
0
def test_response_cookie():
    response = Response()
    response.headers.cookie[str('foo')] = str('bar')

    def start_response(status, headers):
        assert headers[0][0] == str('Set-Cookie')
        assert headers[0][1].startswith(str('foo=bar'))

    response.to_wsgi({}, start_response, 'utf8')
예제 #10
0
def test_response_headers_are_str():
    response = Response()
    response.headers[b'Location'] = b'somewhere'

    def start_response(status, headers):
        assert isinstance(headers[0][0], str)
        assert isinstance(headers[0][1], str)

    response.to_wsgi({}, start_response, 'utf8')
예제 #11
0
def test_response_body_as_iterable_comes_through_untouched():
    response = Response(body=[b"Greetings, ", b"program!"])

    def start_response(status, headers):
        pass

    expected = [b"Greetings, ", b"program!"]
    actual = list(response.to_wsgi({}, start_response, 'utf8').body)
    assert actual == expected
예제 #12
0
def test_response_body_as_bytestring_results_in_an_iterable():
    response = Response(body=b"Greetings, program!")

    def start_response(status, headers):
        pass

    expected = [b"Greetings, program!"]
    actual = list(response.to_wsgi({}, start_response, 'utf8').body)
    assert actual == expected
예제 #13
0
def test_response_to_wsgi():
    response = Response(body=b"Greetings, program!")

    def start_response(status, headers):
        pass

    expected = [b"Greetings, program!"]
    actual = list(response.to_wsgi({}, start_response, 'utf8').body)
    assert actual == expected
예제 #14
0
def canonize(request, website):
    """Enforce a certain scheme and hostname.

    This is a Pando state chain function to ensure that requests are served on a
    certain root URL, even if multiple domains point to the application.
    """
    is_callback = request.path.raw.startswith('/callbacks/')
    is_healthcheck = request.headers.get(b'User-Agent',
                                         b'').startswith(b'ELB-HealthChecker')
    if is_callback or is_healthcheck:
        # Don't redirect callbacks
        if request.path.raw[-1] == '/' or is_healthcheck:
            l = request.line
            scheme, netloc, path, query, fragment = urlsplit(l.uri)
            assert path[-1] == '/'  # sanity check
            path = '/callbacks/health.txt' if is_healthcheck else path[:-1]
            new_uri = urlunsplit((scheme, netloc, path, query, fragment))
            request.line = Line(l.method.raw, new_uri, l.version.raw)
        return
    scheme = request.headers.get(b'X-Forwarded-Proto', b'http')
    try:
        request.hostname = host = request.headers[b'Host'].decode('idna')
    except UnicodeDecodeError:
        request.hostname = host = ''
    canonical_host = website.canonical_host
    canonical_scheme = website.canonical_scheme
    bad_scheme = scheme.decode('ascii', 'replace') != canonical_scheme
    bad_host = False
    if canonical_host:
        if host == canonical_host:
            pass
        elif host.endswith('.' + canonical_host):
            subdomain = host[:-len(canonical_host) - 1]
            if subdomain in website.locales:
                accept_langs = request.headers.get(b'Accept-Language', b'')
                accept_langs = subdomain.encode('idna') + b',' + accept_langs
                request.headers[b'Accept-Language'] = accept_langs
            else:
                bad_host = True
        else:
            bad_host = True
    if bad_scheme or bad_host:
        url = '%s://%s' % (canonical_scheme,
                           canonical_host if bad_host else host)
        if request.line.method in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
            # Redirect to a particular path for idempotent methods.
            url += request.line.uri.path.raw
            if request.line.uri.querystring:
                url += '?' + request.line.uri.querystring.raw
        else:
            # For non-idempotent methods, redirect to homepage.
            url += '/'
        response = Response()
        response.headers[b'Cache-Control'] = b'public, max-age=86400'
        response.redirect(url)
예제 #15
0
def canonize(request, website):
    """Enforce a certain scheme and hostname.

    This is a Pando state chain function to ensure that requests are served on a
    certain root URL, even if multiple domains point to the application.
    """
    if request.path.raw.startswith('/callbacks/'):
        # Don't redirect callbacks
        if request.path.raw[-1] == '/':
            # Remove trailing slash
            l = request.line
            scheme, netloc, path, query, fragment = urlsplit(l.uri)
            assert path[-1] == '/'  # sanity check
            path = path[:-1]
            new_uri = urlunsplit((scheme, netloc, path, query, fragment))
            request.line = Line(l.method.raw, new_uri, l.version.raw)
        return
    scheme = request.headers.get(b'X-Forwarded-Proto', b'http')
    try:
        request.hostname = host = request.headers[b'Host'].decode('idna')
    except UnicodeDecodeError:
        request.hostname = host = ''
    canonical_host = website.canonical_host
    canonical_scheme = website.canonical_scheme
    bad_scheme = scheme.decode('ascii', 'replace') != canonical_scheme
    bad_host = False
    if canonical_host:
        if host == canonical_host:
            pass
        elif host.endswith('.'+canonical_host):
            subdomain = host[:-len(canonical_host)-1]
            if subdomain in website.locales:
                accept_langs = request.headers.get(b'Accept-Language', b'')
                accept_langs = subdomain.encode('idna') + b',' + accept_langs
                request.headers[b'Accept-Language'] = accept_langs
            else:
                bad_host = True
        else:
            bad_host = True
    if bad_scheme or bad_host:
        url = '%s://%s' % (canonical_scheme, canonical_host if bad_host else host)
        if request.line.method in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
            # Redirect to a particular path for idempotent methods.
            url += request.line.uri.path.raw
            if request.line.uri.querystring:
                url += '?' + request.line.uri.querystring.raw
        else:
            # For non-idempotent methods, redirect to homepage.
            url += '/'
        response = Response()
        response.headers[b'Cache-Control'] = b'public, max-age=86400'
        response.redirect(url)
예제 #16
0
def get_int(d, k, default=None, minimum=None):
    r = d.get(k)
    if r is None:
        return default
    try:
        r = int(r)
    except (ValueError, TypeError):
        raise Response().error(400,
                               "`%s` value %r is not a valid integer" % (k, r))
    if minimum is not None and r < minimum:
        raise Response().error(
            400, "`%s` value %r is less than %i" % (k, r, minimum))
    return r
예제 #17
0
def test_response_headers_protect_against_crlf_injection():
    response = Response()

    def inject():
        response.headers[b'Location'] = b'foo\r\nbar'

    raises(CRLFInjection, inject)
예제 #18
0
def parse_int(o, **kw):
    try:
        return int(o)
    except (ValueError, TypeError):
        if 'default' in kw:
            return kw['default']
        raise Response().error(400, "%r is not a valid integer" % o)
예제 #19
0
 def get_query_id(self, querystring):
     token = querystring['access_token']
     i = token.rfind('.')
     data, data_hash = token[:i], token[i + 1:]
     if data_hash != hashlib.md5(data + '.' + self.api_secret).hexdigest():
         raise Response(400, 'Invalid hash in access_token')
     return querystring['query_id']
예제 #20
0
    def api_error_handler(self, response, is_user_session, domain):
        response_text = response.text  # for Sentry
        status = response.status_code
        if status == 404:
            raise Response(404, response_text)
        if status == 401 and is_user_session:
            # https://tools.ietf.org/html/rfc5849#section-3.2
            raise TokenExpiredError
        if status == 429 and is_user_session:
            limit, remaining, reset = self.get_ratelimit_headers(response)

            def msg(_, to_age):
                if remaining == 0 and reset:
                    return _(
                        "You've consumed your quota of requests, you can try again in {0}.",
                        to_age(reset))
                else:
                    return _(
                        "You're making requests too fast, please try again later."
                    )

            raise LazyResponse(status, msg)
        if status != 200:
            logger.error('{} responded with {}:\n{}'.format(
                domain, status, response_text))
            msg = lambda _: _("{0} returned an error, please try again later.",
                              domain)
            raise LazyResponse(502, msg)
예제 #21
0
 def x_user_info(self, extracted, info, default):
     if 'accounts' in info:
         accounts = info.get('accounts')
         if accounts:
             return accounts[0]
         raise Response(404)
     return default
예제 #22
0
def get_int(d, k, default=NO_DEFAULT, minimum=None):
    try:
        r = d[k]
    except (KeyError, Response):
        if default is NO_DEFAULT:
            raise
        return default
    try:
        r = int(r)
    except (ValueError, TypeError):
        raise Response().error(400,
                               "`%s` value %r is not a valid integer" % (k, r))
    if minimum is not None and r < minimum:
        raise Response().error(
            400, "`%s` value %r is less than %i" % (k, r, minimum))
    return r
예제 #23
0
def test_set_whence_raised_works():
    try:
        raise Response(200)
    except Response as r:
        assert r.whence_raised == (None, None)
        r.set_whence_raised()
        assert r.whence_raised[0] == 'tests' + os.sep + 'test_response.py'
        assert isinstance(r.whence_raised[1], int)
예제 #24
0
def turn_socket_error_into_50X(website, exception, _=lambda a: a, response=None):
    # The mangopay module reraises exceptions and stores the original in `__cause__`.
    exception = getattr(exception, '__cause__', exception)
    if isinstance(exception, Timeout) or 'timeout' in str(exception).lower():
        response = response or Response()
        response.code = 504
    elif isinstance(exception, (socket.error, ConnectionError)):
        response = response or Response()
        response.code = 502
    else:
        return
    response.body = _(
        "Processing your request failed because our server was unable to communicate "
        "with a service located on another machine. This is a temporary issue, please "
        "try again later."
    )
    return {'response': response, 'exception': None}
예제 #25
0
def get_int(d, k, default=None):
    r = d.get(k)
    if r is None:
        return default
    try:
        return int(r)
    except (ValueError, TypeError):
        raise Response().error(400,
                               "`%s` value %r is not a valid integer" % (k, r))
예제 #26
0
def get_choice(d, k, choices, default=NO_DEFAULT):
    try:
        r = d[k]
    except (KeyError, Response):
        if default is NO_DEFAULT:
            raise
        return default
    if r not in choices:
        raise Response().error(400, "`%s` value %r is invalid. Choices: %r" % (k, r, choices))
    return r
예제 #27
0
def get_color(d, k, default=NO_DEFAULT):
    try:
        r = d[k]
    except (KeyError, Response):
        if default is NO_DEFAULT:
            raise
        return default
    if not color_re.match(r):
        raise Response().error(
            400, "`%s` value %r is not a valid hexadecimal color" % (k, r))
    return r
예제 #28
0
def return_500_for_exception(website, exception, response=None):
    response = response or Response()
    response.code = 500
    if website.show_tracebacks:
        import traceback
        response.body = traceback.format_exc()
    else:
        response.body = (
            "Uh-oh, you've found a serious bug. Sorry for the inconvenience, "
            "we'll get it fixed ASAP.")
    return {'response': response, 'exception': None}
예제 #29
0
def parse_boolean(mapping, k, default=NO_DEFAULT):
    try:
        r = mapping[k].lower()
    except (KeyError, Response):
        if default is NO_DEFAULT:
            raise
        return default
    if r in TRUEISH:
        return True
    if r in FALSEISH:
        return False
    raise Response().error(400, "`%s` value %r is invalid" % (k, r))
예제 #30
0
def parse_list(mapping, k, cast, default=NO_DEFAULT, sep=','):
    try:
        r = mapping[k].split(sep)
    except (KeyError, Response):
        if default is NO_DEFAULT:
            raise
        return default
    try:
        r = [cast(v) for v in r]
    except (ValueError, TypeError):
        raise Response().error(400, "`%s` value %r is invalid" % (k, mapping[k]))
    return r
예제 #31
0
def turn_socket_error_into_50X(website,
                               state,
                               exception,
                               _=str.format,
                               response=None):
    """Catch network errors and replace them with a 502 or 504 response.

    Because network exceptions are often caught and wrapped by libraries, this
    function recursively looks at the standard `__cause__` and `__context__`
    attributes of exceptions in order to find the initial error.

    https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement
    https://stackoverflow.com/a/11235957/2729778
    """
    for i in range(100):
        if isinstance(exception,
                      Timeout) or 'timeout' in str(exception).lower():
            response = response or Response()
            response.code = 504
            break
        elif isinstance(exception, (OSError, ConnectionError)):
            response = response or Response()
            response.code = 502
            break
        elif getattr(exception, '__cause__', None):
            exception = exception.__cause__
        elif getattr(exception, '__context__', None):
            exception = exception.__context__
        else:
            return
    # log the exception
    website.tell_sentry(exception, state, level='warning')
    # show a proper error message
    response.body = _(
        "Processing your request failed because our server was unable to communicate "
        "with a service located on another machine. This is a temporary issue, please "
        "try again later.")
    return {'response': response, 'exception': None}
예제 #32
0
def compile_assets(website):
    cleanup = []
    for spt in find_files(website.www_root+'/assets/', '*.spt'):
        filepath = spt[:-4]  # /path/to/www/assets/foo.css
        if not os.path.exists(filepath):
            cleanup.append(filepath)
        dispatch_result = DispatchResult(DispatchStatus.okay, spt, {}, "Found.", {}, True)
        state = dict(dispatch_result=dispatch_result, response=Response())
        state['state'] = state
        content = resources.get(website.request_processor, spt).render(state).body
        if not isinstance(content, bytes):
            content = content.encode('utf8')
        tmpfd, tmpfpath = mkstemp(dir='.')
        os.write(tmpfd, content)
        os.close(tmpfd)
        os.rename(tmpfpath, filepath)
    if website.env.clean_assets:
        atexit.register(lambda: rm_f(*cleanup))
예제 #33
0
def remove_email_address_from_blacklist(address, user, request):
    """
    This function allows anyone to remove an email address from the blacklist,
    but with rate limits for non-admins in order to prevent abuse.
    """
    with website.db.get_cursor() as cursor:
        if not user.is_acting_as('admin'):
            source = user.id or request.source
            website.db.hit_rate_limit('email.unblacklist.source', source,
                                      TooManyAttempts)
        r = cursor.all(
            """
            UPDATE email_blacklist
               SET ignore_after = current_timestamp
                 , ignored_by = %(user_id)s
             WHERE lower(address) = lower(%(address)s)
               AND (ignore_after IS NULL OR ignore_after > current_timestamp)
         RETURNING *
        """, dict(address=address, user_id=user.id))
        if not r:
            return
        if not user.is_acting_as('admin'):
            if any(bl.reason == 'complaint' for bl in r):
                raise Response(
                    403,
                    ("Only admins are allowed to unblock an address which is "
                     "blacklisted because of a complaint."))
            website.db.hit_rate_limit('email.unblacklist.target', address,
                                      TooManyAttempts)
    # Mark the matching `email_blacklisted` notifications as read
    participant = website.db.Participant.from_email(address)
    if participant:
        notifications = website.db.all(
            """
            SELECT id, context
              FROM notifications
             WHERE participant = %s
               AND event = 'email_blacklisted'
               AND is_new
        """, (participant.id, ))
        for notif in notifications:
            context = deserialize(notif.context)
            if context['blacklisted_address'].lower() == address.lower():
                participant.mark_notification_as_read(notif.id)
예제 #34
0
def parse_date(mapping, k, default=NO_DEFAULT, sep='-'):
    try:
        r = mapping[k]
        if r:
            r = r.split(sep)
        elif default is not NO_DEFAULT:
            return default
    except (KeyError, Response):
        if default is NO_DEFAULT:
            raise
        return default
    try:
        year, month, day = map(int, r)
        # the above raises ValueError if the number of parts isn't 3
        # or if any part isn't an integer
        r = date(year, month, day)
    except (ValueError, TypeError):
        raise Response().error(400, "`%s` value %r is invalid" % (k, mapping[k]))
    return r
예제 #35
0
    def api_error_handler(self, response, is_user_session):
        status = response.status_code
        if status == 404:
            raise Response(404, response.text)
        if status == 429 and is_user_session:
            limit, remaining, reset = self.get_ratelimit_headers(response)

            def msg(_, to_age):
                if remaining == 0 and reset:
                    return _(
                        "You've consumed your quota of requests, you can try again in {0}.",
                        to_age(reset))
                else:
                    return _(
                        "You're making requests too fast, please try again later."
                    )

            raise LazyResponse(status, msg)
        if status != 200:
            logger.error('{} api responded with {}:\n{}'.format(
                self.name, status, response.text))
            msg = lambda _: _("{0} returned an error, please try again later.",
                              self.display_name)
            raise LazyResponse(502, msg)
예제 #36
0
 def __init__(self, *args, **kw):
     Response.__init__(self, self.code, '', **kw)
     self.lazy_body = self.msg
     self.args = args
예제 #37
0
 def __init__(self):
     Response.__init__(self, 503, '')
     self.html_template = 'templates/no-db.html'
예제 #38
0
 def __init__(self):
     Response.__init__(self, 403, '')
     self.html_template = 'templates/log-in-required.html'
예제 #39
0
 def __init__(self):
     Response.__init__(self, 403, '')
     self.html_template = 'templates/no-payins.html'
예제 #40
0
 def __init__(self, ambiguous_string, suggestions):
     Response.__init__(self, 400, '')
     self.ambiguous_string = ambiguous_string
     self.suggestions = suggestions
예제 #41
0
 def __init__(self, code, lazy_body, **kw):
     Response.__init__(self, code, '', **kw)
     self.lazy_body = lazy_body
예제 #42
0
 def __init__(self, id, class_name):
     Response.__init__(self, 400, "Invalid %s ID: %r" % (class_name, id))
예제 #43
0
def create_response_object(request, website):
    response = Response()
    response.request = request
    response.website = website
    return {'response': response}
예제 #44
0
def export_history(participant,
                   year,
                   mode,
                   key,
                   back_as='namedtuple',
                   require_key=False):
    db = participant.db
    base_url = website.canonical_url + '/~'
    params = dict(id=participant.id, year=year, base_url=base_url)
    out = {}
    if mode == 'aggregate':
        out['given'] = lambda: db.all("""
            SELECT (%(base_url)s || t.tippee::text) AS donee_url,
                   min(p.username) AS donee_username, basket_sum(t.amount) AS amount
              FROM transfers t
              JOIN participants p ON p.id = t.tippee
             WHERE t.tipper = %(id)s
               AND extract(year from t.timestamp) = %(year)s
               AND t.status = 'succeeded'
               AND t.context IN ('tip', 'take', 'tip-in-advance', 'take-in-advance')
               AND t.refund_ref IS NULL
               AND t.virtual IS NOT true
          GROUP BY t.tippee
        """,
                                      params,
                                      back_as=back_as)
        out['reimbursed'] = lambda: db.all("""
            SELECT (%(base_url)s || t.tippee::text) AS recipient_url,
                   min(p.username) AS recipient_username, basket_sum(t.amount) AS amount
              FROM transfers t
              JOIN participants p ON p.id = t.tippee
             WHERE t.tipper = %(id)s
               AND extract(year from t.timestamp) = %(year)s
               AND t.status = 'succeeded'
               AND t.context = 'expense'
          GROUP BY t.tippee
        """,
                                           params,
                                           back_as=back_as)
        out['taken'] = lambda: db.all("""
            SELECT (%(base_url)s || t.team::text) AS team_url,
                   min(p.username) AS team_username, basket_sum(t.amount) AS amount
              FROM transfers t
              JOIN participants p ON p.id = t.team
             WHERE t.tippee = %(id)s
               AND t.context IN ('take', 'take-in-advance')
               AND extract(year from t.timestamp) = %(year)s
               AND t.status = 'succeeded'
               AND t.virtual IS NOT true
          GROUP BY t.team
        """,
                                      params,
                                      back_as=back_as)
    else:
        out['exchanges'] = lambda: db.all("""
            SELECT timestamp, amount, fee, status, note
              FROM exchanges
             WHERE participant = %(id)s
               AND extract(year from timestamp) = %(year)s
          ORDER BY id ASC
        """,
                                          params,
                                          back_as=back_as)
        out['given'] = lambda: db.all("""
            SELECT timestamp, (%(base_url)s || t.tippee::text) AS donee_url,
                   p.username AS donee_username, t.amount, t.context
              FROM transfers t
              JOIN participants p ON p.id = t.tippee
             WHERE t.tipper = %(id)s
               AND extract(year from t.timestamp) = %(year)s
               AND t.status = 'succeeded'
               AND t.virtual IS NOT true
          ORDER BY t.id ASC
        """,
                                      params,
                                      back_as=back_as)
        out['taken'] = lambda: db.all("""
            SELECT timestamp, (%(base_url)s || t.team::text) AS team_url,
                   p.username AS team_username, t.amount
              FROM transfers t
              JOIN participants p ON p.id = t.team
             WHERE t.tippee = %(id)s
               AND t.context IN ('take', 'take-in-advance')
               AND extract(year from t.timestamp) = %(year)s
               AND t.status = 'succeeded'
               AND t.virtual IS NOT true
          ORDER BY t.id ASC
        """,
                                      params,
                                      back_as=back_as)
        out['received'] = lambda: db.all("""
            SELECT timestamp, amount, context
              FROM transfers
             WHERE tippee = %(id)s
               AND context NOT IN ('take', 'take-in-advance')
               AND extract(year from timestamp) = %(year)s
               AND status = 'succeeded'
               AND virtual IS NOT true
          ORDER BY id ASC
        """,
                                         params,
                                         back_as=back_as)

    if key:
        try:
            return out[key]()
        except KeyError:
            raise Response(400, "bad key `%s`" % key)
    elif require_key:
        raise Response(400, "missing `key` parameter")
    else:
        return {k: v() for k, v in out.items()}
예제 #45
0
 def __init__(self, *args, **kw):
     Response.__init__(self, 429, (
         "You have consumed your quota of admin actions. This isn't supposed "
         "to happen."
     ))