You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

392 line
11 KiB

  1. # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
  2. # License: MIT. See LICENSE
  3. import logging
  4. import os
  5. from werkzeug.exceptions import HTTPException, NotFound
  6. from werkzeug.local import LocalManager
  7. from werkzeug.middleware.profiler import ProfilerMiddleware
  8. from werkzeug.middleware.shared_data import SharedDataMiddleware
  9. from werkzeug.wrappers import Request, Response
  10. import frappe
  11. import frappe.api
  12. import frappe.auth
  13. import frappe.handler
  14. import frappe.monitor
  15. import frappe.rate_limiter
  16. import frappe.recorder
  17. import frappe.utils.response
  18. from frappe import _
  19. from frappe.core.doctype.comment.comment import update_comments_in_parent_after_request
  20. from frappe.middlewares import StaticDataMiddleware
  21. from frappe.utils import get_site_name, sanitize_html
  22. from frappe.utils.error import make_error_snapshot
  23. from frappe.website.serve import get_response
  24. local_manager = LocalManager(frappe.local)
  25. _site = None
  26. _sites_path = os.environ.get("SITES_PATH", ".")
  27. SAFE_HTTP_METHODS = ("GET", "HEAD", "OPTIONS")
  28. UNSAFE_HTTP_METHODS = ("POST", "PUT", "DELETE", "PATCH")
  29. class RequestContext:
  30. def __init__(self, environ):
  31. self.request = Request(environ)
  32. def __enter__(self):
  33. init_request(self.request)
  34. def __exit__(self, type, value, traceback):
  35. frappe.destroy()
  36. @local_manager.middleware
  37. @Request.application
  38. def application(request: Request):
  39. response = None
  40. try:
  41. rollback = True
  42. init_request(request)
  43. frappe.recorder.record()
  44. frappe.monitor.start()
  45. frappe.rate_limiter.apply()
  46. frappe.api.validate_auth()
  47. if request.method == "OPTIONS":
  48. response = Response()
  49. elif frappe.form_dict.cmd:
  50. response = frappe.handler.handle()
  51. elif request.path.startswith("/api/"):
  52. response = frappe.api.handle()
  53. elif request.path.startswith("/backups"):
  54. response = frappe.utils.response.download_backup(request.path)
  55. elif request.path.startswith("/private/files/"):
  56. response = frappe.utils.response.download_private_file(request.path)
  57. elif request.method in ("GET", "HEAD", "POST"):
  58. response = get_response()
  59. else:
  60. raise NotFound
  61. except HTTPException as e:
  62. return e
  63. except Exception as e:
  64. response = handle_exception(e)
  65. else:
  66. rollback = after_request(rollback)
  67. finally:
  68. if request.method in ("POST", "PUT") and frappe.db and rollback:
  69. frappe.db.rollback()
  70. frappe.rate_limiter.update()
  71. frappe.monitor.stop(response)
  72. frappe.recorder.dump()
  73. log_request(request, response)
  74. process_response(response)
  75. frappe.destroy()
  76. return response
  77. def init_request(request):
  78. frappe.local.request = request
  79. frappe.local.is_ajax = frappe.get_request_header("X-Requested-With") == "XMLHttpRequest"
  80. site = _site or request.headers.get("X-Frappe-Site-Name") or get_site_name(request.host)
  81. frappe.init(site=site, sites_path=_sites_path)
  82. if not (frappe.local.conf and frappe.local.conf.db_name):
  83. # site does not exist
  84. raise NotFound
  85. if frappe.local.conf.maintenance_mode:
  86. frappe.connect()
  87. if frappe.local.conf.allow_reads_during_maintenance:
  88. setup_read_only_mode()
  89. else:
  90. raise frappe.SessionStopped("Session Stopped")
  91. else:
  92. frappe.connect(set_admin_as_user=False)
  93. request.max_content_length = frappe.local.conf.get("max_file_size") or 10 * 1024 * 1024
  94. make_form_dict(request)
  95. if request.method != "OPTIONS":
  96. frappe.local.http_request = frappe.auth.HTTPRequest()
  97. def setup_read_only_mode():
  98. """During maintenance_mode reads to DB can still be performed to reduce downtime. This
  99. function sets up read only mode
  100. - Setting global flag so other pages, desk and database can know that we are in read only mode.
  101. - Setup read only database access either by:
  102. - Connecting to read replica if one exists
  103. - Or setting up read only SQL transactions.
  104. """
  105. frappe.flags.read_only = True
  106. # If replica is available then just connect replica, else setup read only transaction.
  107. if frappe.conf.read_from_replica:
  108. frappe.connect_replica()
  109. else:
  110. frappe.db.begin(read_only=True)
  111. def log_request(request, response):
  112. if hasattr(frappe.local, "conf") and frappe.local.conf.enable_frappe_logger:
  113. frappe.logger("frappe.web", allow_site=frappe.local.site).info(
  114. {
  115. "site": get_site_name(request.host),
  116. "remote_addr": getattr(request, "remote_addr", "NOTFOUND"),
  117. "base_url": getattr(request, "base_url", "NOTFOUND"),
  118. "full_path": getattr(request, "full_path", "NOTFOUND"),
  119. "method": getattr(request, "method", "NOTFOUND"),
  120. "scheme": getattr(request, "scheme", "NOTFOUND"),
  121. "http_status_code": getattr(response, "status_code", "NOTFOUND"),
  122. }
  123. )
  124. def process_response(response):
  125. if not response:
  126. return
  127. # set cookies
  128. if hasattr(frappe.local, "cookie_manager"):
  129. frappe.local.cookie_manager.flush_cookies(response=response)
  130. # rate limiter headers
  131. if hasattr(frappe.local, "rate_limiter"):
  132. response.headers.extend(frappe.local.rate_limiter.headers())
  133. # CORS headers
  134. if hasattr(frappe.local, "conf"):
  135. set_cors_headers(response)
  136. def set_cors_headers(response):
  137. if not (
  138. (allowed_origins := frappe.conf.allow_cors)
  139. and (request := frappe.local.request)
  140. and (origin := request.headers.get("Origin"))
  141. ):
  142. return
  143. if allowed_origins != "*":
  144. if not isinstance(allowed_origins, list):
  145. allowed_origins = [allowed_origins]
  146. if origin not in allowed_origins:
  147. return
  148. cors_headers = {
  149. "Access-Control-Allow-Credentials": "true",
  150. "Access-Control-Allow-Origin": origin,
  151. "Vary": "Origin",
  152. }
  153. # only required for preflight requests
  154. if request.method == "OPTIONS":
  155. cors_headers["Access-Control-Allow-Methods"] = request.headers.get(
  156. "Access-Control-Request-Method"
  157. )
  158. if allowed_headers := request.headers.get("Access-Control-Request-Headers"):
  159. cors_headers["Access-Control-Allow-Headers"] = allowed_headers
  160. # allow browsers to cache preflight requests for upto a day
  161. if not frappe.conf.developer_mode:
  162. cors_headers["Access-Control-Max-Age"] = "86400"
  163. response.headers.extend(cors_headers)
  164. def make_form_dict(request):
  165. import json
  166. request_data = request.get_data(as_text=True)
  167. if "application/json" in (request.content_type or "") and request_data:
  168. args = json.loads(request_data)
  169. else:
  170. args = {}
  171. args.update(request.args or {})
  172. args.update(request.form or {})
  173. if not isinstance(args, dict):
  174. frappe.throw(_("Invalid request arguments"))
  175. frappe.local.form_dict = frappe._dict(args)
  176. if "_" in frappe.local.form_dict:
  177. # _ is passed by $.ajax so that the request is not cached by the browser. So, remove _ from form_dict
  178. frappe.local.form_dict.pop("_")
  179. def handle_exception(e):
  180. response = None
  181. http_status_code = getattr(e, "http_status_code", 500)
  182. return_as_message = False
  183. accept_header = frappe.get_request_header("Accept") or ""
  184. respond_as_json = (
  185. frappe.get_request_header("Accept")
  186. and (frappe.local.is_ajax or "application/json" in accept_header)
  187. or (frappe.local.request.path.startswith("/api/") and not accept_header.startswith("text"))
  188. )
  189. if not frappe.session.user:
  190. # If session creation fails then user won't be unset. This causes a lot of code that
  191. # assumes presence of this to fail. Session creation fails => guest or expired login
  192. # usually.
  193. frappe.session.user = "Guest"
  194. if respond_as_json:
  195. # handle ajax responses first
  196. # if the request is ajax, send back the trace or error message
  197. response = frappe.utils.response.report_error(http_status_code)
  198. elif isinstance(e, frappe.SessionStopped):
  199. response = frappe.utils.response.handle_session_stopped()
  200. elif (
  201. http_status_code == 500
  202. and (frappe.db and isinstance(e, frappe.db.InternalError))
  203. and (frappe.db and (frappe.db.is_deadlocked(e) or frappe.db.is_timedout(e)))
  204. ):
  205. http_status_code = 508
  206. elif http_status_code == 401:
  207. frappe.respond_as_web_page(
  208. _("Session Expired"),
  209. _("Your session has expired, please login again to continue."),
  210. http_status_code=http_status_code,
  211. indicator_color="red",
  212. )
  213. return_as_message = True
  214. elif http_status_code == 403:
  215. frappe.respond_as_web_page(
  216. _("Not Permitted"),
  217. _("You do not have enough permissions to complete the action"),
  218. http_status_code=http_status_code,
  219. indicator_color="red",
  220. )
  221. return_as_message = True
  222. elif http_status_code == 404:
  223. frappe.respond_as_web_page(
  224. _("Not Found"),
  225. _("The resource you are looking for is not available"),
  226. http_status_code=http_status_code,
  227. indicator_color="red",
  228. )
  229. return_as_message = True
  230. elif http_status_code == 429:
  231. response = frappe.rate_limiter.respond()
  232. else:
  233. traceback = "<pre>" + sanitize_html(frappe.get_traceback()) + "</pre>"
  234. # disable traceback in production if flag is set
  235. if frappe.local.flags.disable_traceback and not frappe.local.dev_server:
  236. traceback = ""
  237. frappe.respond_as_web_page(
  238. "Server Error", traceback, http_status_code=http_status_code, indicator_color="red", width=640
  239. )
  240. return_as_message = True
  241. if e.__class__ == frappe.AuthenticationError:
  242. if hasattr(frappe.local, "login_manager"):
  243. frappe.local.login_manager.clear_cookies()
  244. if http_status_code >= 500:
  245. make_error_snapshot(e)
  246. if return_as_message:
  247. response = get_response("message", http_status_code=http_status_code)
  248. if frappe.conf.get("developer_mode") and not respond_as_json:
  249. # don't fail silently for non-json response errors
  250. print(frappe.get_traceback())
  251. return response
  252. def after_request(rollback):
  253. # if HTTP method would change server state, commit if necessary
  254. if frappe.db and (
  255. frappe.local.flags.commit or frappe.local.request.method in UNSAFE_HTTP_METHODS
  256. ):
  257. if frappe.db.transaction_writes:
  258. frappe.db.commit()
  259. rollback = False
  260. # update session
  261. if getattr(frappe.local, "session_obj", None):
  262. updated_in_db = frappe.local.session_obj.update()
  263. if updated_in_db:
  264. frappe.db.commit()
  265. rollback = False
  266. update_comments_in_parent_after_request()
  267. return rollback
  268. def serve(
  269. port=8000, profile=False, no_reload=False, no_threading=False, site=None, sites_path="."
  270. ):
  271. global application, _site, _sites_path
  272. _site = site
  273. _sites_path = sites_path
  274. from werkzeug.serving import run_simple
  275. if profile or os.environ.get("USE_PROFILER"):
  276. application = ProfilerMiddleware(application, sort_by=("cumtime", "calls"))
  277. if not os.environ.get("NO_STATICS"):
  278. application = SharedDataMiddleware(
  279. application, {"/assets": str(os.path.join(sites_path, "assets"))}
  280. )
  281. application = StaticDataMiddleware(application, {"/files": str(os.path.abspath(sites_path))})
  282. application.debug = True
  283. application.config = {"SERVER_NAME": "localhost:8000"}
  284. log = logging.getLogger("werkzeug")
  285. log.propagate = False
  286. in_test_env = os.environ.get("CI")
  287. if in_test_env:
  288. log.setLevel(logging.ERROR)
  289. run_simple(
  290. "0.0.0.0",
  291. int(port),
  292. application,
  293. use_reloader=False if in_test_env else not no_reload,
  294. use_debugger=not in_test_env,
  295. use_evalex=not in_test_env,
  296. threaded=not no_threading,
  297. )