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.
 
 
 
 
 
 

360 regels
9.5 KiB

  1. # -*- coding: utf-8 -*-
  2. # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
  3. # License: MIT. See LICENSE
  4. import logging
  5. import os
  6. from werkzeug.exceptions import HTTPException, NotFound
  7. from werkzeug.local import LocalManager
  8. from werkzeug.middleware.profiler import ProfilerMiddleware
  9. from werkzeug.middleware.shared_data import SharedDataMiddleware
  10. from werkzeug.wrappers import Request, Response
  11. import frappe
  12. import frappe.api
  13. import frappe.auth
  14. import frappe.handler
  15. import frappe.monitor
  16. import frappe.rate_limiter
  17. import frappe.recorder
  18. import frappe.utils.response
  19. from frappe import _
  20. from frappe.core.doctype.comment.comment import update_comments_in_parent_after_request
  21. from frappe.middlewares import StaticDataMiddleware
  22. from frappe.utils import get_site_name, sanitize_html
  23. from frappe.utils.error import make_error_snapshot
  24. from frappe.website.serve import get_response
  25. local_manager = LocalManager([frappe.local])
  26. _site = None
  27. _sites_path = os.environ.get("SITES_PATH", ".")
  28. SAFE_HTTP_METHODS = ("GET", "HEAD", "OPTIONS")
  29. UNSAFE_HTTP_METHODS = ("POST", "PUT", "DELETE", "PATCH")
  30. class RequestContext(object):
  31. def __init__(self, environ):
  32. self.request = Request(environ)
  33. def __enter__(self):
  34. init_request(self.request)
  35. def __exit__(self, type, value, traceback):
  36. frappe.destroy()
  37. @Request.application
  38. def application(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 frappe.SessionStopped as e:
  64. response = frappe.utils.response.handle_session_stopped()
  65. except Exception as e:
  66. response = handle_exception(e)
  67. else:
  68. rollback = after_request(rollback)
  69. finally:
  70. if request.method in ("POST", "PUT") and frappe.db and rollback:
  71. frappe.db.rollback()
  72. frappe.rate_limiter.update()
  73. frappe.monitor.stop(response)
  74. frappe.recorder.dump()
  75. log_request(request, response)
  76. process_response(response)
  77. frappe.destroy()
  78. return response
  79. def init_request(request):
  80. frappe.local.request = request
  81. frappe.local.is_ajax = frappe.get_request_header("X-Requested-With") == "XMLHttpRequest"
  82. site = _site or request.headers.get("X-Frappe-Site-Name") or get_site_name(request.host)
  83. frappe.init(site=site, sites_path=_sites_path)
  84. if not (frappe.local.conf and frappe.local.conf.db_name):
  85. # site does not exist
  86. raise NotFound
  87. if frappe.local.conf.get("maintenance_mode"):
  88. frappe.connect()
  89. raise frappe.SessionStopped("Session Stopped")
  90. else:
  91. frappe.connect(set_admin_as_user=False)
  92. request.max_content_length = frappe.local.conf.get("max_file_size") or 10 * 1024 * 1024
  93. make_form_dict(request)
  94. if request.method != "OPTIONS":
  95. frappe.local.http_request = frappe.auth.HTTPRequest()
  96. def log_request(request, response):
  97. if hasattr(frappe.local, "conf") and frappe.local.conf.enable_frappe_logger:
  98. frappe.logger("frappe.web", allow_site=frappe.local.site).info(
  99. {
  100. "site": get_site_name(request.host),
  101. "remote_addr": getattr(request, "remote_addr", "NOTFOUND"),
  102. "base_url": getattr(request, "base_url", "NOTFOUND"),
  103. "full_path": getattr(request, "full_path", "NOTFOUND"),
  104. "method": getattr(request, "method", "NOTFOUND"),
  105. "scheme": getattr(request, "scheme", "NOTFOUND"),
  106. "http_status_code": getattr(response, "status_code", "NOTFOUND"),
  107. }
  108. )
  109. def process_response(response):
  110. if not response:
  111. return
  112. # set cookies
  113. if hasattr(frappe.local, "cookie_manager"):
  114. frappe.local.cookie_manager.flush_cookies(response=response)
  115. # rate limiter headers
  116. if hasattr(frappe.local, "rate_limiter"):
  117. response.headers.extend(frappe.local.rate_limiter.headers())
  118. # CORS headers
  119. if hasattr(frappe.local, "conf") and frappe.conf.allow_cors:
  120. set_cors_headers(response)
  121. def set_cors_headers(response):
  122. origin = frappe.request.headers.get("Origin")
  123. allow_cors = frappe.conf.allow_cors
  124. if not (origin and allow_cors):
  125. return
  126. if allow_cors != "*":
  127. if not isinstance(allow_cors, list):
  128. allow_cors = [allow_cors]
  129. if origin not in allow_cors:
  130. return
  131. response.headers.extend(
  132. {
  133. "Access-Control-Allow-Origin": origin,
  134. "Access-Control-Allow-Credentials": "true",
  135. "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
  136. "Access-Control-Allow-Headers": (
  137. "Authorization,DNT,X-Mx-ReqToken,"
  138. "Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,"
  139. "Cache-Control,Content-Type"
  140. ),
  141. }
  142. )
  143. def make_form_dict(request):
  144. import json
  145. request_data = request.get_data(as_text=True)
  146. if "application/json" in (request.content_type or "") and request_data:
  147. args = json.loads(request_data)
  148. else:
  149. args = {}
  150. args.update(request.args or {})
  151. args.update(request.form or {})
  152. if not isinstance(args, dict):
  153. frappe.throw(_("Invalid request arguments"))
  154. frappe.local.form_dict = frappe._dict(args)
  155. if "_" in frappe.local.form_dict:
  156. # _ is passed by $.ajax so that the request is not cached by the browser. So, remove _ from form_dict
  157. frappe.local.form_dict.pop("_")
  158. def handle_exception(e):
  159. response = None
  160. http_status_code = getattr(e, "http_status_code", 500)
  161. return_as_message = False
  162. accept_header = frappe.get_request_header("Accept") or ""
  163. respond_as_json = (
  164. frappe.get_request_header("Accept")
  165. and (frappe.local.is_ajax or "application/json" in accept_header)
  166. or (frappe.local.request.path.startswith("/api/") and not accept_header.startswith("text"))
  167. )
  168. if frappe.conf.get("developer_mode"):
  169. # don't fail silently
  170. print(frappe.get_traceback())
  171. if respond_as_json:
  172. # handle ajax responses first
  173. # if the request is ajax, send back the trace or error message
  174. response = frappe.utils.response.report_error(http_status_code)
  175. elif (
  176. http_status_code == 500
  177. and (frappe.db and isinstance(e, frappe.db.InternalError))
  178. and (frappe.db and (frappe.db.is_deadlocked(e) or frappe.db.is_timedout(e)))
  179. ):
  180. http_status_code = 508
  181. elif http_status_code == 401:
  182. frappe.respond_as_web_page(
  183. _("Session Expired"),
  184. _("Your session has expired, please login again to continue."),
  185. http_status_code=http_status_code,
  186. indicator_color="red",
  187. )
  188. return_as_message = True
  189. elif http_status_code == 403:
  190. frappe.respond_as_web_page(
  191. _("Not Permitted"),
  192. _("You do not have enough permissions to complete the action"),
  193. http_status_code=http_status_code,
  194. indicator_color="red",
  195. )
  196. return_as_message = True
  197. elif http_status_code == 404:
  198. frappe.respond_as_web_page(
  199. _("Not Found"),
  200. _("The resource you are looking for is not available"),
  201. http_status_code=http_status_code,
  202. indicator_color="red",
  203. )
  204. return_as_message = True
  205. elif http_status_code == 429:
  206. response = frappe.rate_limiter.respond()
  207. else:
  208. traceback = "<pre>" + sanitize_html(frappe.get_traceback()) + "</pre>"
  209. # disable traceback in production if flag is set
  210. if frappe.local.flags.disable_traceback and not frappe.local.dev_server:
  211. traceback = ""
  212. frappe.respond_as_web_page(
  213. "Server Error", traceback, http_status_code=http_status_code, indicator_color="red", width=640
  214. )
  215. return_as_message = True
  216. if e.__class__ == frappe.AuthenticationError:
  217. if hasattr(frappe.local, "login_manager"):
  218. frappe.local.login_manager.clear_cookies()
  219. if http_status_code >= 500:
  220. make_error_snapshot(e)
  221. if return_as_message:
  222. response = get_response("message", http_status_code=http_status_code)
  223. return response
  224. def after_request(rollback):
  225. # if HTTP method would change server state, commit if necessary
  226. if frappe.db and (
  227. frappe.local.flags.commit or frappe.local.request.method in UNSAFE_HTTP_METHODS
  228. ):
  229. if frappe.db.transaction_writes:
  230. frappe.db.commit()
  231. rollback = False
  232. # update session
  233. if getattr(frappe.local, "session_obj", None):
  234. updated_in_db = frappe.local.session_obj.update()
  235. if updated_in_db:
  236. frappe.db.commit()
  237. rollback = False
  238. update_comments_in_parent_after_request()
  239. return rollback
  240. application = local_manager.make_middleware(application)
  241. def serve(
  242. port=8000, profile=False, no_reload=False, no_threading=False, site=None, sites_path="."
  243. ):
  244. global application, _site, _sites_path
  245. _site = site
  246. _sites_path = sites_path
  247. from werkzeug.serving import run_simple
  248. if profile or os.environ.get("USE_PROFILER"):
  249. application = ProfilerMiddleware(application, sort_by=("cumtime", "calls"))
  250. if not os.environ.get("NO_STATICS"):
  251. application = SharedDataMiddleware(
  252. application, {str("/assets"): str(os.path.join(sites_path, "assets"))}
  253. )
  254. application = StaticDataMiddleware(
  255. application, {str("/files"): str(os.path.abspath(sites_path))}
  256. )
  257. application.debug = True
  258. application.config = {"SERVER_NAME": "localhost:8000"}
  259. log = logging.getLogger("werkzeug")
  260. log.propagate = False
  261. in_test_env = os.environ.get("CI")
  262. if in_test_env:
  263. log.setLevel(logging.ERROR)
  264. run_simple(
  265. "0.0.0.0",
  266. int(port),
  267. application,
  268. use_reloader=False if in_test_env else not no_reload,
  269. use_debugger=not in_test_env,
  270. use_evalex=not in_test_env,
  271. threaded=not no_threading,
  272. )