web_middlewares.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. import re
  2. from typing import TYPE_CHECKING, Awaitable, Callable, Tuple, Type, TypeVar
  3. from .web_exceptions import HTTPPermanentRedirect, _HTTPMove
  4. from .web_request import Request
  5. from .web_response import StreamResponse
  6. from .web_urldispatcher import SystemRoute
  7. __all__ = (
  8. "middleware",
  9. "normalize_path_middleware",
  10. )
  11. if TYPE_CHECKING: # pragma: no cover
  12. from .web_app import Application
  13. _Func = TypeVar("_Func")
  14. async def _check_request_resolves(request: Request, path: str) -> Tuple[bool, Request]:
  15. alt_request = request.clone(rel_url=path)
  16. match_info = await request.app.router.resolve(alt_request)
  17. alt_request._match_info = match_info # type: ignore
  18. if match_info.http_exception is None:
  19. return True, alt_request
  20. return False, request
  21. def middleware(f: _Func) -> _Func:
  22. f.__middleware_version__ = 1 # type: ignore
  23. return f
  24. _Handler = Callable[[Request], Awaitable[StreamResponse]]
  25. _Middleware = Callable[[Request, _Handler], Awaitable[StreamResponse]]
  26. def normalize_path_middleware(
  27. *,
  28. append_slash: bool = True,
  29. remove_slash: bool = False,
  30. merge_slashes: bool = True,
  31. redirect_class: Type[_HTTPMove] = HTTPPermanentRedirect
  32. ) -> _Middleware:
  33. """
  34. Middleware factory which produces a middleware that normalizes
  35. the path of a request. By normalizing it means:
  36. - Add or remove a trailing slash to the path.
  37. - Double slashes are replaced by one.
  38. The middleware returns as soon as it finds a path that resolves
  39. correctly. The order if both merge and append/remove are enabled is
  40. 1) merge slashes
  41. 2) append/remove slash
  42. 3) both merge slashes and append/remove slash.
  43. If the path resolves with at least one of those conditions, it will
  44. redirect to the new path.
  45. Only one of `append_slash` and `remove_slash` can be enabled. If both
  46. are `True` the factory will raise an assertion error
  47. If `append_slash` is `True` the middleware will append a slash when
  48. needed. If a resource is defined with trailing slash and the request
  49. comes without it, it will append it automatically.
  50. If `remove_slash` is `True`, `append_slash` must be `False`. When enabled
  51. the middleware will remove trailing slashes and redirect if the resource
  52. is defined
  53. If merge_slashes is True, merge multiple consecutive slashes in the
  54. path into one.
  55. """
  56. correct_configuration = not (append_slash and remove_slash)
  57. assert correct_configuration, "Cannot both remove and append slash"
  58. @middleware
  59. async def impl(request: Request, handler: _Handler) -> StreamResponse:
  60. if isinstance(request.match_info.route, SystemRoute):
  61. paths_to_check = []
  62. if "?" in request.raw_path:
  63. path, query = request.raw_path.split("?", 1)
  64. query = "?" + query
  65. else:
  66. query = ""
  67. path = request.raw_path
  68. if merge_slashes:
  69. paths_to_check.append(re.sub("//+", "/", path))
  70. if append_slash and not request.path.endswith("/"):
  71. paths_to_check.append(path + "/")
  72. if remove_slash and request.path.endswith("/"):
  73. paths_to_check.append(path[:-1])
  74. if merge_slashes and append_slash:
  75. paths_to_check.append(re.sub("//+", "/", path + "/"))
  76. if merge_slashes and remove_slash:
  77. merged_slashes = re.sub("//+", "/", path)
  78. paths_to_check.append(merged_slashes[:-1])
  79. for path in paths_to_check:
  80. path = re.sub("^//+", "/", path) # SECURITY: GHSA-v6wp-4m6f-gcjg
  81. resolves, request = await _check_request_resolves(request, path)
  82. if resolves:
  83. raise redirect_class(request.raw_path + query)
  84. return await handler(request)
  85. return impl
  86. def _fix_request_current_app(app: "Application") -> _Middleware:
  87. @middleware
  88. async def impl(request: Request, handler: _Handler) -> StreamResponse:
  89. with request.match_info.set_current_app(app):
  90. return await handler(request)
  91. return impl