web.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. # This file is part of Radicale Server - Calendar Server
  2. # Copyright (C) 2017 Unrud <unrud@outlook.com>
  3. #
  4. # This library is free software: you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation, either version 3 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This library is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with Radicale. If not, see <http://www.gnu.org/licenses/>.
  16. import os
  17. import posixpath
  18. import time
  19. from http import client
  20. from importlib import import_module
  21. import pkg_resources
  22. from radicale import storage
  23. from radicale.log import logger
  24. NOT_FOUND = (
  25. client.NOT_FOUND, (("Content-Type", "text/plain"),),
  26. "The requested resource could not be found.")
  27. MIMETYPES = {
  28. ".css": "text/css",
  29. ".eot": "application/vnd.ms-fontobject",
  30. ".gif": "image/gif",
  31. ".html": "text/html",
  32. ".js": "application/javascript",
  33. ".manifest": "text/cache-manifest",
  34. ".png": "image/png",
  35. ".svg": "image/svg+xml",
  36. ".ttf": "application/font-sfnt",
  37. ".txt": "text/plain",
  38. ".woff": "application/font-woff",
  39. ".woff2": "font/woff2",
  40. ".xml": "text/xml"}
  41. FALLBACK_MIMETYPE = "application/octet-stream"
  42. INTERNAL_TYPES = ("none", "internal")
  43. def load(configuration):
  44. """Load the web module chosen in configuration."""
  45. web_type = configuration.get("web", "type")
  46. if web_type == "none":
  47. web_class = NoneWeb
  48. elif web_type == "internal":
  49. web_class = Web
  50. else:
  51. try:
  52. web_class = import_module(web_type).Web
  53. except Exception as e:
  54. raise RuntimeError("Failed to load web module %r: %s" %
  55. (web_type, e)) from e
  56. logger.info("Web type is %r", web_type)
  57. return web_class(configuration)
  58. class BaseWeb:
  59. def __init__(self, configuration):
  60. self.configuration = configuration
  61. def get(self, environ, base_prefix, path, user):
  62. """GET request.
  63. ``base_prefix`` is sanitized and never ends with "/".
  64. ``path`` is sanitized and always starts with "/.web"
  65. ``user`` is empty for anonymous users.
  66. """
  67. raise NotImplementedError
  68. class NoneWeb(BaseWeb):
  69. def get(self, environ, base_prefix, path, user):
  70. if path != "/.web":
  71. return NOT_FOUND
  72. return client.OK, {"Content-Type": "text/plain"}, "Radicale works!"
  73. class Web(BaseWeb):
  74. def __init__(self, configuration):
  75. super().__init__(configuration)
  76. self.folder = pkg_resources.resource_filename(__name__, "web")
  77. def get(self, environ, base_prefix, path, user):
  78. try:
  79. filesystem_path = storage.path_to_filesystem(
  80. self.folder, path[len("/.web"):])
  81. except ValueError as e:
  82. logger.debug("Web content with unsafe path %r requested: %s",
  83. path, e, exc_info=True)
  84. return NOT_FOUND
  85. if os.path.isdir(filesystem_path) and not path.endswith("/"):
  86. location = posixpath.basename(path) + "/"
  87. return (client.FOUND,
  88. {"Location": location, "Content-Type": "text/plain"},
  89. "Redirected to %s" % location)
  90. if os.path.isdir(filesystem_path):
  91. filesystem_path = os.path.join(filesystem_path, "index.html")
  92. if not os.path.isfile(filesystem_path):
  93. return NOT_FOUND
  94. content_type = MIMETYPES.get(
  95. os.path.splitext(filesystem_path)[1].lower(), FALLBACK_MIMETYPE)
  96. with open(filesystem_path, "rb") as f:
  97. answer = f.read()
  98. last_modified = time.strftime(
  99. "%a, %d %b %Y %H:%M:%S GMT",
  100. time.gmtime(os.fstat(f.fileno()).st_mtime))
  101. headers = {
  102. "Content-Type": content_type,
  103. "Last-Modified": last_modified}
  104. return client.OK, headers, answer