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 20KB

5 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  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 HttpResponse, HttpResponseNotFound
  8. from django.template import Context, Engine, TemplateDoesNotExist
  9. from django.template.defaultfilters import pprint
  10. from django.urls import Resolver404, resolve
  11. from django.utils import timezone
  12. from django.utils.datastructures import MultiValueDict
  13. from django.utils.encoding import force_text
  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_text(
  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, IOError):
  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. frames = []
  349. # No exceptions were supplied to ExceptionReporter
  350. if not exceptions:
  351. return frames
  352. # In case there's just one exception, take the traceback from self.tb
  353. exc_value = exceptions.pop()
  354. tb = self.tb if not exceptions else exc_value.__traceback__
  355. while tb is not None:
  356. # Support for __traceback_hide__ which is used by a few libraries
  357. # to hide internal frames.
  358. if tb.tb_frame.f_locals.get('__traceback_hide__'):
  359. tb = tb.tb_next
  360. continue
  361. filename = tb.tb_frame.f_code.co_filename
  362. function = tb.tb_frame.f_code.co_name
  363. lineno = tb.tb_lineno - 1
  364. loader = tb.tb_frame.f_globals.get('__loader__')
  365. module_name = tb.tb_frame.f_globals.get('__name__') or ''
  366. pre_context_lineno, pre_context, context_line, post_context = self._get_lines_from_file(
  367. filename, lineno, 7, loader, module_name,
  368. )
  369. if pre_context_lineno is None:
  370. pre_context_lineno = lineno
  371. pre_context = []
  372. context_line = '<source code not available>'
  373. post_context = []
  374. frames.append({
  375. 'exc_cause': explicit_or_implicit_cause(exc_value),
  376. 'exc_cause_explicit': getattr(exc_value, '__cause__', True),
  377. 'tb': tb,
  378. 'type': 'django' if module_name.startswith('django.') else 'user',
  379. 'filename': filename,
  380. 'function': function,
  381. 'lineno': lineno + 1,
  382. 'vars': self.filter.get_traceback_frame_variables(self.request, tb.tb_frame),
  383. 'id': id(tb),
  384. 'pre_context': pre_context,
  385. 'context_line': context_line,
  386. 'post_context': post_context,
  387. 'pre_context_lineno': pre_context_lineno + 1,
  388. })
  389. # If the traceback for current exception is consumed, try the
  390. # other exception.
  391. if not tb.tb_next and exceptions:
  392. exc_value = exceptions.pop()
  393. tb = exc_value.__traceback__
  394. else:
  395. tb = tb.tb_next
  396. return frames
  397. def technical_404_response(request, exception):
  398. """Create a technical 404 error response. `exception` is the Http404."""
  399. try:
  400. error_url = exception.args[0]['path']
  401. except (IndexError, TypeError, KeyError):
  402. error_url = request.path_info[1:] # Trim leading slash
  403. try:
  404. tried = exception.args[0]['tried']
  405. except (IndexError, TypeError, KeyError):
  406. tried = []
  407. else:
  408. if (not tried or ( # empty URLconf
  409. request.path == '/' and
  410. len(tried) == 1 and # default URLconf
  411. len(tried[0]) == 1 and
  412. getattr(tried[0][0], 'app_name', '') == getattr(tried[0][0], 'namespace', '') == 'admin'
  413. )):
  414. return default_urlconf(request)
  415. urlconf = getattr(request, 'urlconf', settings.ROOT_URLCONF)
  416. if isinstance(urlconf, types.ModuleType):
  417. urlconf = urlconf.__name__
  418. caller = ''
  419. try:
  420. resolver_match = resolve(request.path)
  421. except Resolver404:
  422. pass
  423. else:
  424. obj = resolver_match.func
  425. if hasattr(obj, '__name__'):
  426. caller = obj.__name__
  427. elif hasattr(obj, '__class__') and hasattr(obj.__class__, '__name__'):
  428. caller = obj.__class__.__name__
  429. if hasattr(obj, '__module__'):
  430. module = obj.__module__
  431. caller = '%s.%s' % (module, caller)
  432. with Path(CURRENT_DIR, 'templates', 'technical_404.html').open() as fh:
  433. t = DEBUG_ENGINE.from_string(fh.read())
  434. c = Context({
  435. 'urlconf': urlconf,
  436. 'root_urlconf': settings.ROOT_URLCONF,
  437. 'request_path': error_url,
  438. 'urlpatterns': tried,
  439. 'reason': str(exception),
  440. 'request': request,
  441. 'settings': get_safe_settings(),
  442. 'raising_view_name': caller,
  443. })
  444. return HttpResponseNotFound(t.render(c), content_type='text/html')
  445. def default_urlconf(request):
  446. """Create an empty URLconf 404 error response."""
  447. with Path(CURRENT_DIR, 'templates', 'default_urlconf.html').open() as fh:
  448. t = DEBUG_ENGINE.from_string(fh.read())
  449. c = Context({
  450. 'version': get_docs_version(),
  451. })
  452. return HttpResponse(t.render(c), content_type='text/html')