debug.py 22 KB

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