diff --git a/tildes/tildes/__init__.py b/tildes/tildes/__init__.py index 6023928..b23ac20 100644 --- a/tildes/tildes/__init__.py +++ b/tildes/tildes/__init__.py @@ -3,17 +3,13 @@ """Configure and initialize the Pyramid app.""" -from time import time -from typing import Any, Callable, Dict, Optional, Tuple +from typing import Any, Dict, Optional, Tuple from marshmallow.exceptions import ValidationError from paste.deploy.config import PrefixMiddleware -from prometheus_client import Histogram from pyramid.config import Configurator from pyramid.httpexceptions import HTTPTooManyRequests -from pyramid.registry import Registry from pyramid.request import Request -from pyramid.response import Response from redis import Redis import sentry_sdk from sentry_sdk.integrations.pyramid import PyramidIntegration @@ -43,9 +39,9 @@ def main(global_config: Dict[str, str], **settings: str) -> PrefixMiddleware: config.scan("tildes.views") - config.add_tween("tildes.http_method_tween_factory") - config.add_tween("tildes.metrics_tween_factory") - config.add_tween("tildes.theme_cookie_tween_factory") + config.add_tween("tildes.tweens.http_method_tween_factory") + config.add_tween("tildes.tweens.metrics_tween_factory") + config.add_tween("tildes.tweens.theme_cookie_tween_factory") config.add_static_view("images", "/images") @@ -86,90 +82,6 @@ def main(global_config: Dict[str, str], **settings: str) -> PrefixMiddleware: return prefixed_app -def http_method_tween_factory(handler: Callable, registry: Registry) -> Callable: - # pylint: disable=unused-argument - """Return a tween function that can override the request's HTTP method.""" - - def method_override_tween(request: Request) -> Request: - """Override HTTP method with one specified in header.""" - valid_overrides_by_method = {"POST": ["DELETE", "PATCH", "PUT"]} - - original_method = request.method.upper() - valid_overrides = valid_overrides_by_method.get(original_method, []) - override = request.headers.get("X-HTTP-Method-Override", "").upper() - - if override in valid_overrides: - request.method = override - - return handler(request) - - return method_override_tween - - -def metrics_tween_factory(handler: Callable, registry: Registry) -> Callable: - # pylint: disable=unused-argument - """Return a tween function that gathers metrics for Prometheus.""" - - request_histogram = Histogram( - "tildes_pyramid_requests_seconds", - "Request processing times", - labelnames=["route", "status_code", "method", "logged_in"], - ) - - def metrics_tween(request: Request) -> Response: - """Gather metrics for each request.""" - start_time = time() - response = handler(request) - duration = time() - start_time - - # ignore requests for invalid addresses - if not request.matched_route: - return response - - request_histogram.labels( - route=request.matched_route.name, - status_code=response.status_code, - method=request.method, - logged_in=str(bool(request.user)).lower(), - ).observe(duration) - - return response - - return metrics_tween - - -def theme_cookie_tween_factory(handler: Callable, registry: Registry) -> Callable: - # pylint: disable=unused-argument - """Return a tween function that sets the theme cookie.""" - - def theme_cookie_tween(request: Request) -> Response: - """Set the theme cookie if needed (currently always, see comment below).""" - response = handler(request) - - current_theme = request.cookies.get("theme", "") - if not current_theme and request.user: - current_theme = request.user.theme_default - - # Currently, we want to always set the theme cookie. This is because we - # recently started setting the domain on this cookie (to be able to apply the - # user's theme to the Blog/Docs sites), and all older cookies won't have a - # domain set. This will basically let us convert the old no-domain cookies to - # new ones. After a decent amount of time (maybe sometime in April 2019), we - # can change this to only set the cookie when it's not already present and the - # user has a default theme set (so their default theme will work for Blog/Docs). - if current_theme: - response.set_cookie( - "theme", - current_theme, - max_age=315360000, - secure=True, - domain="." + request.domain, - ) - return response - - return theme_cookie_tween - - def get_redis_connection(request: Request) -> Redis: """Return a connection to the Redis server.""" socket = request.registry.settings["redis.unix_socket_path"] diff --git a/tildes/tildes/tweens.py b/tildes/tildes/tweens.py new file mode 100644 index 0000000..f1b9bfd --- /dev/null +++ b/tildes/tildes/tweens.py @@ -0,0 +1,96 @@ +# Copyright (c) 2019 Tildes contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Contains Pyramid "tweens", used to insert additional logic into request-handling.""" + +from time import time +from typing import Callable + +from prometheus_client import Histogram +from pyramid.registry import Registry +from pyramid.request import Request +from pyramid.response import Response + + +def http_method_tween_factory(handler: Callable, registry: Registry) -> Callable: + # pylint: disable=unused-argument + """Return a tween function that can override the request's HTTP method.""" + + def method_override_tween(request: Request) -> Response: + """Override HTTP method with one specified in header.""" + valid_overrides_by_method = {"POST": ["DELETE", "PATCH", "PUT"]} + + original_method = request.method.upper() + valid_overrides = valid_overrides_by_method.get(original_method, []) + override = request.headers.get("X-HTTP-Method-Override", "").upper() + + if override in valid_overrides: + request.method = override + + return handler(request) + + return method_override_tween + + +def metrics_tween_factory(handler: Callable, registry: Registry) -> Callable: + # pylint: disable=unused-argument + """Return a tween function that gathers metrics for Prometheus.""" + + request_histogram = Histogram( + "tildes_pyramid_requests_seconds", + "Request processing times", + labelnames=["route", "status_code", "method", "logged_in"], + ) + + def metrics_tween(request: Request) -> Response: + """Gather metrics for each request.""" + start_time = time() + response = handler(request) + duration = time() - start_time + + # ignore requests for invalid addresses + if not request.matched_route: + return response + + request_histogram.labels( + route=request.matched_route.name, + status_code=response.status_code, + method=request.method, + logged_in=str(bool(request.user)).lower(), + ).observe(duration) + + return response + + return metrics_tween + + +def theme_cookie_tween_factory(handler: Callable, registry: Registry) -> Callable: + # pylint: disable=unused-argument + """Return a tween function that sets the theme cookie.""" + + def theme_cookie_tween(request: Request) -> Response: + """Set the theme cookie if needed (currently always, see comment below).""" + response = handler(request) + + current_theme = request.cookies.get("theme", "") + if not current_theme and request.user: + current_theme = request.user.theme_default + + # Currently, we want to always set the theme cookie. This is because we + # recently started setting the domain on this cookie (to be able to apply the + # user's theme to the Blog/Docs sites), and all older cookies won't have a + # domain set. This will basically let us convert the old no-domain cookies to + # new ones. After a decent amount of time (maybe sometime in April 2019), we + # can change this to only set the cookie when it's not already present and the + # user has a default theme set (so their default theme will work for Blog/Docs). + if current_theme: + response.set_cookie( + "theme", + current_theme, + max_age=315360000, + secure=True, + domain="." + request.domain, + ) + return response + + return theme_cookie_tween