Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
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.

debug.py 23KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613
  1. import functools
  2. import re
  3. import sys
  4. import types
  5. import warnings
  6. from pathlib import Path
  7. from django.conf import settings
  8. from django.http import Http404, HttpResponse, HttpResponseNotFound
  9. from django.template import Context, Engine, TemplateDoesNotExist
  10. from django.template.defaultfilters import pprint
  11. from django.urls import resolve
  12. from django.utils import timezone
  13. from django.utils.datastructures import MultiValueDict
  14. from django.utils.encoding import force_str
  15. from django.utils.module_loading import import_string
  16. from django.utils.regex_helper import _lazy_re_compile
  17. from django.utils.version import get_docs_version
  18. # Minimal Django templates engine to render the error templates
  19. # regardless of the project's TEMPLATES setting. Templates are
  20. # read directly from the filesystem so that the error handler
  21. # works even if the template loader is broken.
  22. DEBUG_ENGINE = Engine(
  23. debug=True,
  24. libraries={"i18n": "django.templatetags.i18n"},
  25. )
  26. def builtin_template_path(name):
  27. """
  28. Return a path to a builtin template.
  29. Avoid calling this function at the module level or in a class-definition
  30. because __file__ may not exist, e.g. in frozen environments.
  31. """
  32. return Path(__file__).parent / "templates" / name
  33. class ExceptionCycleWarning(UserWarning):
  34. pass
  35. class CallableSettingWrapper:
  36. """
  37. Object to wrap callable appearing in settings.
  38. * Not to call in the debug page (#21345).
  39. * Not to break the debug page if the callable forbidding to set attributes
  40. (#23070).
  41. """
  42. def __init__(self, callable_setting):
  43. self._wrapped = callable_setting
  44. def __repr__(self):
  45. return repr(self._wrapped)
  46. def technical_500_response(request, exc_type, exc_value, tb, status_code=500):
  47. """
  48. Create a technical server error response. The last three arguments are
  49. the values returned from sys.exc_info() and friends.
  50. """
  51. reporter = get_exception_reporter_class(request)(request, exc_type, exc_value, tb)
  52. if request.accepts("text/html"):
  53. html = reporter.get_traceback_html()
  54. return HttpResponse(html, status=status_code)
  55. else:
  56. text = reporter.get_traceback_text()
  57. return HttpResponse(
  58. text, status=status_code, content_type="text/plain; charset=utf-8"
  59. )
  60. @functools.lru_cache
  61. def get_default_exception_reporter_filter():
  62. # Instantiate the default filter for the first time and cache it.
  63. return import_string(settings.DEFAULT_EXCEPTION_REPORTER_FILTER)()
  64. def get_exception_reporter_filter(request):
  65. default_filter = get_default_exception_reporter_filter()
  66. return getattr(request, "exception_reporter_filter", default_filter)
  67. def get_exception_reporter_class(request):
  68. default_exception_reporter_class = import_string(
  69. settings.DEFAULT_EXCEPTION_REPORTER
  70. )
  71. return getattr(
  72. request, "exception_reporter_class", default_exception_reporter_class
  73. )
  74. def get_caller(request):
  75. resolver_match = request.resolver_match
  76. if resolver_match is None:
  77. try:
  78. resolver_match = resolve(request.path)
  79. except Http404:
  80. pass
  81. return "" if resolver_match is None else resolver_match._func_path
  82. class SafeExceptionReporterFilter:
  83. """
  84. Use annotations made by the sensitive_post_parameters and
  85. sensitive_variables decorators to filter out sensitive information.
  86. """
  87. cleansed_substitute = "********************"
  88. hidden_settings = _lazy_re_compile(
  89. "API|TOKEN|KEY|SECRET|PASS|SIGNATURE", flags=re.I
  90. )
  91. def cleanse_setting(self, key, value):
  92. """
  93. Cleanse an individual setting key/value of sensitive content. If the
  94. value is a dictionary, recursively cleanse the keys in that dictionary.
  95. """
  96. try:
  97. is_sensitive = self.hidden_settings.search(key)
  98. except TypeError:
  99. is_sensitive = False
  100. if is_sensitive:
  101. cleansed = self.cleansed_substitute
  102. elif isinstance(value, dict):
  103. cleansed = {k: self.cleanse_setting(k, v) for k, v in value.items()}
  104. elif isinstance(value, list):
  105. cleansed = [self.cleanse_setting("", v) for v in value]
  106. elif isinstance(value, tuple):
  107. cleansed = tuple([self.cleanse_setting("", v) for v in value])
  108. else:
  109. cleansed = value
  110. if callable(cleansed):
  111. cleansed = CallableSettingWrapper(cleansed)
  112. return cleansed
  113. def get_safe_settings(self):
  114. """
  115. Return a dictionary of the settings module with values of sensitive
  116. settings replaced with stars (*********).
  117. """
  118. settings_dict = {}
  119. for k in dir(settings):
  120. if k.isupper():
  121. settings_dict[k] = self.cleanse_setting(k, getattr(settings, k))
  122. return settings_dict
  123. def get_safe_request_meta(self, request):
  124. """
  125. Return a dictionary of request.META with sensitive values redacted.
  126. """
  127. if not hasattr(request, "META"):
  128. return {}
  129. return {k: self.cleanse_setting(k, v) for k, v in request.META.items()}
  130. def is_active(self, request):
  131. """
  132. This filter is to add safety in production environments (i.e. DEBUG
  133. is False). If DEBUG is True then your site is not safe anyway.
  134. This hook is provided as a convenience to easily activate or
  135. deactivate the filter on a per request basis.
  136. """
  137. return settings.DEBUG is False
  138. def get_cleansed_multivaluedict(self, request, multivaluedict):
  139. """
  140. Replace the keys in a MultiValueDict marked as sensitive with stars.
  141. This mitigates leaking sensitive POST parameters if something like
  142. request.POST['nonexistent_key'] throws an exception (#21098).
  143. """
  144. sensitive_post_parameters = getattr(request, "sensitive_post_parameters", [])
  145. if self.is_active(request) and sensitive_post_parameters:
  146. multivaluedict = multivaluedict.copy()
  147. for param in sensitive_post_parameters:
  148. if param in multivaluedict:
  149. multivaluedict[param] = self.cleansed_substitute
  150. return multivaluedict
  151. def get_post_parameters(self, request):
  152. """
  153. Replace the values of POST parameters marked as sensitive with
  154. stars (*********).
  155. """
  156. if request is None:
  157. return {}
  158. else:
  159. sensitive_post_parameters = getattr(
  160. request, "sensitive_post_parameters", []
  161. )
  162. if self.is_active(request) and sensitive_post_parameters:
  163. cleansed = request.POST.copy()
  164. if sensitive_post_parameters == "__ALL__":
  165. # Cleanse all parameters.
  166. for k in cleansed:
  167. cleansed[k] = self.cleansed_substitute
  168. return cleansed
  169. else:
  170. # Cleanse only the specified parameters.
  171. for param in sensitive_post_parameters:
  172. if param in cleansed:
  173. cleansed[param] = self.cleansed_substitute
  174. return cleansed
  175. else:
  176. return request.POST
  177. def cleanse_special_types(self, request, value):
  178. try:
  179. # If value is lazy or a complex object of another kind, this check
  180. # might raise an exception. isinstance checks that lazy
  181. # MultiValueDicts will have a return value.
  182. is_multivalue_dict = isinstance(value, MultiValueDict)
  183. except Exception as e:
  184. return "{!r} while evaluating {!r}".format(e, value)
  185. if is_multivalue_dict:
  186. # Cleanse MultiValueDicts (request.POST is the one we usually care about)
  187. value = self.get_cleansed_multivaluedict(request, value)
  188. return value
  189. def get_traceback_frame_variables(self, request, tb_frame):
  190. """
  191. Replace the values of variables marked as sensitive with
  192. stars (*********).
  193. """
  194. # Loop through the frame's callers to see if the sensitive_variables
  195. # decorator was used.
  196. current_frame = tb_frame.f_back
  197. sensitive_variables = None
  198. while current_frame is not None:
  199. if (
  200. current_frame.f_code.co_name == "sensitive_variables_wrapper"
  201. and "sensitive_variables_wrapper" in current_frame.f_locals
  202. ):
  203. # The sensitive_variables decorator was used, so we take note
  204. # of the sensitive variables' names.
  205. wrapper = current_frame.f_locals["sensitive_variables_wrapper"]
  206. sensitive_variables = getattr(wrapper, "sensitive_variables", None)
  207. break
  208. current_frame = current_frame.f_back
  209. cleansed = {}
  210. if self.is_active(request) and sensitive_variables:
  211. if sensitive_variables == "__ALL__":
  212. # Cleanse all variables
  213. for name in tb_frame.f_locals:
  214. cleansed[name] = self.cleansed_substitute
  215. else:
  216. # Cleanse specified variables
  217. for name, value in tb_frame.f_locals.items():
  218. if name in sensitive_variables:
  219. value = self.cleansed_substitute
  220. else:
  221. value = self.cleanse_special_types(request, value)
  222. cleansed[name] = value
  223. else:
  224. # Potentially cleanse the request and any MultiValueDicts if they
  225. # are one of the frame variables.
  226. for name, value in tb_frame.f_locals.items():
  227. cleansed[name] = self.cleanse_special_types(request, value)
  228. if (
  229. tb_frame.f_code.co_name == "sensitive_variables_wrapper"
  230. and "sensitive_variables_wrapper" in tb_frame.f_locals
  231. ):
  232. # For good measure, obfuscate the decorated function's arguments in
  233. # the sensitive_variables decorator's frame, in case the variables
  234. # associated with those arguments were meant to be obfuscated from
  235. # the decorated function's frame.
  236. cleansed["func_args"] = self.cleansed_substitute
  237. cleansed["func_kwargs"] = self.cleansed_substitute
  238. return cleansed.items()
  239. class ExceptionReporter:
  240. """Organize and coordinate reporting on exceptions."""
  241. @property
  242. def html_template_path(self):
  243. return builtin_template_path("technical_500.html")
  244. @property
  245. def text_template_path(self):
  246. return builtin_template_path("technical_500.txt")
  247. def __init__(self, request, exc_type, exc_value, tb, is_email=False):
  248. self.request = request
  249. self.filter = get_exception_reporter_filter(self.request)
  250. self.exc_type = exc_type
  251. self.exc_value = exc_value
  252. self.tb = tb
  253. self.is_email = is_email
  254. self.template_info = getattr(self.exc_value, "template_debug", None)
  255. self.template_does_not_exist = False
  256. self.postmortem = None
  257. def _get_raw_insecure_uri(self):
  258. """
  259. Return an absolute URI from variables available in this request. Skip
  260. allowed hosts protection, so may return insecure URI.
  261. """
  262. return "{scheme}://{host}{path}".format(
  263. scheme=self.request.scheme,
  264. host=self.request._get_raw_host(),
  265. path=self.request.get_full_path(),
  266. )
  267. def get_traceback_data(self):
  268. """Return a dictionary containing traceback information."""
  269. if self.exc_type and issubclass(self.exc_type, TemplateDoesNotExist):
  270. self.template_does_not_exist = True
  271. self.postmortem = self.exc_value.chain or [self.exc_value]
  272. frames = self.get_traceback_frames()
  273. for i, frame in enumerate(frames):
  274. if "vars" in frame:
  275. frame_vars = []
  276. for k, v in frame["vars"]:
  277. v = pprint(v)
  278. # Trim large blobs of data
  279. if len(v) > 4096:
  280. v = "%s… <trimmed %d bytes string>" % (v[0:4096], len(v))
  281. frame_vars.append((k, v))
  282. frame["vars"] = frame_vars
  283. frames[i] = frame
  284. unicode_hint = ""
  285. if self.exc_type and issubclass(self.exc_type, UnicodeError):
  286. start = getattr(self.exc_value, "start", None)
  287. end = getattr(self.exc_value, "end", None)
  288. if start is not None and end is not None:
  289. unicode_str = self.exc_value.args[1]
  290. unicode_hint = force_str(
  291. unicode_str[max(start - 5, 0) : min(end + 5, len(unicode_str))],
  292. "ascii",
  293. errors="replace",
  294. )
  295. from django import get_version
  296. if self.request is None:
  297. user_str = None
  298. else:
  299. try:
  300. user_str = str(self.request.user)
  301. except Exception:
  302. # request.user may raise OperationalError if the database is
  303. # unavailable, for example.
  304. user_str = "[unable to retrieve the current user]"
  305. c = {
  306. "is_email": self.is_email,
  307. "unicode_hint": unicode_hint,
  308. "frames": frames,
  309. "request": self.request,
  310. "request_meta": self.filter.get_safe_request_meta(self.request),
  311. "user_str": user_str,
  312. "filtered_POST_items": list(
  313. self.filter.get_post_parameters(self.request).items()
  314. ),
  315. "settings": self.filter.get_safe_settings(),
  316. "sys_executable": sys.executable,
  317. "sys_version_info": "%d.%d.%d" % sys.version_info[0:3],
  318. "server_time": timezone.now(),
  319. "django_version_info": get_version(),
  320. "sys_path": sys.path,
  321. "template_info": self.template_info,
  322. "template_does_not_exist": self.template_does_not_exist,
  323. "postmortem": self.postmortem,
  324. }
  325. if self.request is not None:
  326. c["request_GET_items"] = self.request.GET.items()
  327. c["request_FILES_items"] = self.request.FILES.items()
  328. c["request_COOKIES_items"] = self.request.COOKIES.items()
  329. c["request_insecure_uri"] = self._get_raw_insecure_uri()
  330. c["raising_view_name"] = get_caller(self.request)
  331. # Check whether exception info is available
  332. if self.exc_type:
  333. c["exception_type"] = self.exc_type.__name__
  334. if self.exc_value:
  335. c["exception_value"] = str(self.exc_value)
  336. if frames:
  337. c["lastframe"] = frames[-1]
  338. return c
  339. def get_traceback_html(self):
  340. """Return HTML version of debug 500 HTTP error page."""
  341. with self.html_template_path.open(encoding="utf-8") as fh:
  342. t = DEBUG_ENGINE.from_string(fh.read())
  343. c = Context(self.get_traceback_data(), use_l10n=False)
  344. return t.render(c)
  345. def get_traceback_text(self):
  346. """Return plain text version of debug 500 HTTP error page."""
  347. with self.text_template_path.open(encoding="utf-8") as fh:
  348. t = DEBUG_ENGINE.from_string(fh.read())
  349. c = Context(self.get_traceback_data(), autoescape=False, use_l10n=False)
  350. return t.render(c)
  351. def _get_source(self, filename, loader, module_name):
  352. source = None
  353. if hasattr(loader, "get_source"):
  354. try:
  355. source = loader.get_source(module_name)
  356. except ImportError:
  357. pass
  358. if source is not None:
  359. source = source.splitlines()
  360. if source is None:
  361. try:
  362. with open(filename, "rb") as fp:
  363. source = fp.read().splitlines()
  364. except OSError:
  365. pass
  366. return source
  367. def _get_lines_from_file(
  368. self, filename, lineno, context_lines, loader=None, module_name=None
  369. ):
  370. """
  371. Return context_lines before and after lineno from file.
  372. Return (pre_context_lineno, pre_context, context_line, post_context).
  373. """
  374. source = self._get_source(filename, loader, module_name)
  375. if source is None:
  376. return None, [], None, []
  377. # If we just read the source from a file, or if the loader did not
  378. # apply tokenize.detect_encoding to decode the source into a
  379. # string, then we should do that ourselves.
  380. if isinstance(source[0], bytes):
  381. encoding = "ascii"
  382. for line in source[:2]:
  383. # File coding may be specified. Match pattern from PEP-263
  384. # (https://www.python.org/dev/peps/pep-0263/)
  385. match = re.search(rb"coding[:=]\s*([-\w.]+)", line)
  386. if match:
  387. encoding = match[1].decode("ascii")
  388. break
  389. source = [str(sline, encoding, "replace") for sline in source]
  390. lower_bound = max(0, lineno - context_lines)
  391. upper_bound = lineno + context_lines
  392. try:
  393. pre_context = source[lower_bound:lineno]
  394. context_line = source[lineno]
  395. post_context = source[lineno + 1 : upper_bound]
  396. except IndexError:
  397. return None, [], None, []
  398. return lower_bound, pre_context, context_line, post_context
  399. def _get_explicit_or_implicit_cause(self, exc_value):
  400. explicit = getattr(exc_value, "__cause__", None)
  401. suppress_context = getattr(exc_value, "__suppress_context__", None)
  402. implicit = getattr(exc_value, "__context__", None)
  403. return explicit or (None if suppress_context else implicit)
  404. def get_traceback_frames(self):
  405. # Get the exception and all its causes
  406. exceptions = []
  407. exc_value = self.exc_value
  408. while exc_value:
  409. exceptions.append(exc_value)
  410. exc_value = self._get_explicit_or_implicit_cause(exc_value)
  411. if exc_value in exceptions:
  412. warnings.warn(
  413. "Cycle in the exception chain detected: exception '%s' "
  414. "encountered again." % exc_value,
  415. ExceptionCycleWarning,
  416. )
  417. # Avoid infinite loop if there's a cyclic reference (#29393).
  418. break
  419. frames = []
  420. # No exceptions were supplied to ExceptionReporter
  421. if not exceptions:
  422. return frames
  423. # In case there's just one exception, take the traceback from self.tb
  424. exc_value = exceptions.pop()
  425. tb = self.tb if not exceptions else exc_value.__traceback__
  426. while True:
  427. frames.extend(self.get_exception_traceback_frames(exc_value, tb))
  428. try:
  429. exc_value = exceptions.pop()
  430. except IndexError:
  431. break
  432. tb = exc_value.__traceback__
  433. return frames
  434. def get_exception_traceback_frames(self, exc_value, tb):
  435. exc_cause = self._get_explicit_or_implicit_cause(exc_value)
  436. exc_cause_explicit = getattr(exc_value, "__cause__", True)
  437. if tb is None:
  438. yield {
  439. "exc_cause": exc_cause,
  440. "exc_cause_explicit": exc_cause_explicit,
  441. "tb": None,
  442. "type": "user",
  443. }
  444. while tb is not None:
  445. # Support for __traceback_hide__ which is used by a few libraries
  446. # to hide internal frames.
  447. if tb.tb_frame.f_locals.get("__traceback_hide__"):
  448. tb = tb.tb_next
  449. continue
  450. filename = tb.tb_frame.f_code.co_filename
  451. function = tb.tb_frame.f_code.co_name
  452. lineno = tb.tb_lineno - 1
  453. loader = tb.tb_frame.f_globals.get("__loader__")
  454. module_name = tb.tb_frame.f_globals.get("__name__") or ""
  455. (
  456. pre_context_lineno,
  457. pre_context,
  458. context_line,
  459. post_context,
  460. ) = self._get_lines_from_file(
  461. filename,
  462. lineno,
  463. 7,
  464. loader,
  465. module_name,
  466. )
  467. if pre_context_lineno is None:
  468. pre_context_lineno = lineno
  469. pre_context = []
  470. context_line = "<source code not available>"
  471. post_context = []
  472. yield {
  473. "exc_cause": exc_cause,
  474. "exc_cause_explicit": exc_cause_explicit,
  475. "tb": tb,
  476. "type": "django" if module_name.startswith("django.") else "user",
  477. "filename": filename,
  478. "function": function,
  479. "lineno": lineno + 1,
  480. "vars": self.filter.get_traceback_frame_variables(
  481. self.request, tb.tb_frame
  482. ),
  483. "id": id(tb),
  484. "pre_context": pre_context,
  485. "context_line": context_line,
  486. "post_context": post_context,
  487. "pre_context_lineno": pre_context_lineno + 1,
  488. }
  489. tb = tb.tb_next
  490. def technical_404_response(request, exception):
  491. """Create a technical 404 error response. `exception` is the Http404."""
  492. try:
  493. error_url = exception.args[0]["path"]
  494. except (IndexError, TypeError, KeyError):
  495. error_url = request.path_info[1:] # Trim leading slash
  496. try:
  497. tried = exception.args[0]["tried"]
  498. except (IndexError, TypeError, KeyError):
  499. resolved = True
  500. tried = request.resolver_match.tried if request.resolver_match else None
  501. else:
  502. resolved = False
  503. if not tried or ( # empty URLconf
  504. request.path == "/"
  505. and len(tried) == 1
  506. and len(tried[0]) == 1 # default URLconf
  507. and getattr(tried[0][0], "app_name", "")
  508. == getattr(tried[0][0], "namespace", "")
  509. == "admin"
  510. ):
  511. return default_urlconf(request)
  512. urlconf = getattr(request, "urlconf", settings.ROOT_URLCONF)
  513. if isinstance(urlconf, types.ModuleType):
  514. urlconf = urlconf.__name__
  515. with builtin_template_path("technical_404.html").open(encoding="utf-8") as fh:
  516. t = DEBUG_ENGINE.from_string(fh.read())
  517. reporter_filter = get_default_exception_reporter_filter()
  518. c = Context(
  519. {
  520. "urlconf": urlconf,
  521. "root_urlconf": settings.ROOT_URLCONF,
  522. "request_path": error_url,
  523. "urlpatterns": tried,
  524. "resolved": resolved,
  525. "reason": str(exception),
  526. "request": request,
  527. "settings": reporter_filter.get_safe_settings(),
  528. "raising_view_name": get_caller(request),
  529. }
  530. )
  531. return HttpResponseNotFound(t.render(c))
  532. def default_urlconf(request):
  533. """Create an empty URLconf 404 error response."""
  534. with builtin_template_path("default_urlconf.html").open(encoding="utf-8") as fh:
  535. t = DEBUG_ENGINE.from_string(fh.read())
  536. c = Context(
  537. {
  538. "version": get_docs_version(),
  539. }
  540. )
  541. return HttpResponse(t.render(c))