debug.py 20 KB

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