Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/sentry_sdk/integrations/wsgi.py: 17%

163 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1import sys 

2 

3from sentry_sdk._functools import partial 

4from sentry_sdk.hub import Hub, _should_send_default_pii 

5from sentry_sdk.utils import ( 

6 ContextVar, 

7 capture_internal_exceptions, 

8 event_from_exception, 

9) 

10from sentry_sdk._compat import PY2, reraise, iteritems 

11from sentry_sdk.tracing import Transaction 

12from sentry_sdk.sessions import auto_session_tracking 

13from sentry_sdk.integrations._wsgi_common import _filter_headers 

14 

15from sentry_sdk._types import MYPY 

16 

17if MYPY: 17 ↛ 18line 17 didn't jump to line 18, because the condition on line 17 was never true

18 from typing import Callable 

19 from typing import Dict 

20 from typing import Iterator 

21 from typing import Any 

22 from typing import Tuple 

23 from typing import Optional 

24 from typing import TypeVar 

25 from typing import Protocol 

26 

27 from sentry_sdk.utils import ExcInfo 

28 from sentry_sdk._types import EventProcessor 

29 

30 WsgiResponseIter = TypeVar("WsgiResponseIter") 

31 WsgiResponseHeaders = TypeVar("WsgiResponseHeaders") 

32 WsgiExcInfo = TypeVar("WsgiExcInfo") 

33 

34 class StartResponse(Protocol): 

35 def __call__(self, status, response_headers, exc_info=None): 

36 # type: (str, WsgiResponseHeaders, Optional[WsgiExcInfo]) -> WsgiResponseIter 

37 pass 

38 

39 

40_wsgi_middleware_applied = ContextVar("sentry_wsgi_middleware_applied") 

41 

42 

43if PY2: 43 ↛ 45line 43 didn't jump to line 45, because the condition on line 43 was never true

44 

45 def wsgi_decoding_dance(s, charset="utf-8", errors="replace"): 

46 # type: (str, str, str) -> str 

47 return s.decode(charset, errors) 

48 

49else: 

50 

51 def wsgi_decoding_dance(s, charset="utf-8", errors="replace"): 

52 # type: (str, str, str) -> str 

53 return s.encode("latin1").decode(charset, errors) 

54 

55 

56def get_host(environ, use_x_forwarded_for=False): 

57 # type: (Dict[str, str], bool) -> str 

58 """Return the host for the given WSGI environment. Yanked from Werkzeug.""" 

59 if use_x_forwarded_for and "HTTP_X_FORWARDED_HOST" in environ: 

60 rv = environ["HTTP_X_FORWARDED_HOST"] 

61 if environ["wsgi.url_scheme"] == "http" and rv.endswith(":80"): 

62 rv = rv[:-3] 

63 elif environ["wsgi.url_scheme"] == "https" and rv.endswith(":443"): 

64 rv = rv[:-4] 

65 elif environ.get("HTTP_HOST"): 

66 rv = environ["HTTP_HOST"] 

67 if environ["wsgi.url_scheme"] == "http" and rv.endswith(":80"): 

68 rv = rv[:-3] 

69 elif environ["wsgi.url_scheme"] == "https" and rv.endswith(":443"): 

70 rv = rv[:-4] 

71 elif environ.get("SERVER_NAME"): 

72 rv = environ["SERVER_NAME"] 

73 if (environ["wsgi.url_scheme"], environ["SERVER_PORT"]) not in ( 

74 ("https", "443"), 

75 ("http", "80"), 

76 ): 

77 rv += ":" + environ["SERVER_PORT"] 

78 else: 

79 # In spite of the WSGI spec, SERVER_NAME might not be present. 

80 rv = "unknown" 

81 

82 return rv 

83 

84 

85def get_request_url(environ, use_x_forwarded_for=False): 

86 # type: (Dict[str, str], bool) -> str 

87 """Return the absolute URL without query string for the given WSGI 

88 environment.""" 

89 return "%s://%s/%s" % ( 

90 environ.get("wsgi.url_scheme"), 

91 get_host(environ, use_x_forwarded_for), 

92 wsgi_decoding_dance(environ.get("PATH_INFO") or "").lstrip("/"), 

93 ) 

94 

95 

96class SentryWsgiMiddleware(object): 

97 __slots__ = ("app", "use_x_forwarded_for") 

98 

99 def __init__(self, app, use_x_forwarded_for=False): 

100 # type: (Callable[[Dict[str, str], Callable[..., Any]], Any], bool) -> None 

101 self.app = app 

102 self.use_x_forwarded_for = use_x_forwarded_for 

103 

104 def __call__(self, environ, start_response): 

105 # type: (Dict[str, str], Callable[..., Any]) -> _ScopedResponse 

106 if _wsgi_middleware_applied.get(False): 

107 return self.app(environ, start_response) 

108 

109 _wsgi_middleware_applied.set(True) 

110 try: 

111 hub = Hub(Hub.current) 

112 with auto_session_tracking(hub, session_mode="request"): 

113 with hub: 

114 with capture_internal_exceptions(): 

115 with hub.configure_scope() as scope: 

116 scope.clear_breadcrumbs() 

117 scope._name = "wsgi" 

118 scope.add_event_processor( 

119 _make_wsgi_event_processor( 

120 environ, self.use_x_forwarded_for 

121 ) 

122 ) 

123 

124 transaction = Transaction.continue_from_environ( 

125 environ, op="http.server", name="generic WSGI request" 

126 ) 

127 

128 with hub.start_transaction( 

129 transaction, custom_sampling_context={"wsgi_environ": environ} 

130 ): 

131 try: 

132 rv = self.app( 

133 environ, 

134 partial( 

135 _sentry_start_response, start_response, transaction 

136 ), 

137 ) 

138 except BaseException: 

139 reraise(*_capture_exception(hub)) 

140 finally: 

141 _wsgi_middleware_applied.set(False) 

142 

143 return _ScopedResponse(hub, rv) 

144 

145 

146def _sentry_start_response( 

147 old_start_response, # type: StartResponse 

148 transaction, # type: Transaction 

149 status, # type: str 

150 response_headers, # type: WsgiResponseHeaders 

151 exc_info=None, # type: Optional[WsgiExcInfo] 

152): 

153 # type: (...) -> WsgiResponseIter 

154 with capture_internal_exceptions(): 

155 status_int = int(status.split(" ", 1)[0]) 

156 transaction.set_http_status(status_int) 

157 

158 if exc_info is None: 

159 # The Django Rest Framework WSGI test client, and likely other 

160 # (incorrect) implementations, cannot deal with the exc_info argument 

161 # if one is present. Avoid providing a third argument if not necessary. 

162 return old_start_response(status, response_headers) 

163 else: 

164 return old_start_response(status, response_headers, exc_info) 

165 

166 

167def _get_environ(environ): 

168 # type: (Dict[str, str]) -> Iterator[Tuple[str, str]] 

169 """ 

170 Returns our explicitly included environment variables we want to 

171 capture (server name, port and remote addr if pii is enabled). 

172 """ 

173 keys = ["SERVER_NAME", "SERVER_PORT"] 

174 if _should_send_default_pii(): 

175 # make debugging of proxy setup easier. Proxy headers are 

176 # in headers. 

177 keys += ["REMOTE_ADDR"] 

178 

179 for key in keys: 

180 if key in environ: 

181 yield key, environ[key] 

182 

183 

184# `get_headers` comes from `werkzeug.datastructures.EnvironHeaders` 

185# 

186# We need this function because Django does not give us a "pure" http header 

187# dict. So we might as well use it for all WSGI integrations. 

188def _get_headers(environ): 

189 # type: (Dict[str, str]) -> Iterator[Tuple[str, str]] 

190 """ 

191 Returns only proper HTTP headers. 

192 

193 """ 

194 for key, value in iteritems(environ): 

195 key = str(key) 

196 if key.startswith("HTTP_") and key not in ( 

197 "HTTP_CONTENT_TYPE", 

198 "HTTP_CONTENT_LENGTH", 

199 ): 

200 yield key[5:].replace("_", "-").title(), value 

201 elif key in ("CONTENT_TYPE", "CONTENT_LENGTH"): 

202 yield key.replace("_", "-").title(), value 

203 

204 

205def get_client_ip(environ): 

206 # type: (Dict[str, str]) -> Optional[Any] 

207 """ 

208 Infer the user IP address from various headers. This cannot be used in 

209 security sensitive situations since the value may be forged from a client, 

210 but it's good enough for the event payload. 

211 """ 

212 try: 

213 return environ["HTTP_X_FORWARDED_FOR"].split(",")[0].strip() 

214 except (KeyError, IndexError): 

215 pass 

216 

217 try: 

218 return environ["HTTP_X_REAL_IP"] 

219 except KeyError: 

220 pass 

221 

222 return environ.get("REMOTE_ADDR") 

223 

224 

225def _capture_exception(hub): 

226 # type: (Hub) -> ExcInfo 

227 exc_info = sys.exc_info() 

228 

229 # Check client here as it might have been unset while streaming response 

230 if hub.client is not None: 

231 e = exc_info[1] 

232 

233 # SystemExit(0) is the only uncaught exception that is expected behavior 

234 should_skip_capture = isinstance(e, SystemExit) and e.code in (0, None) 

235 if not should_skip_capture: 

236 event, hint = event_from_exception( 

237 exc_info, 

238 client_options=hub.client.options, 

239 mechanism={"type": "wsgi", "handled": False}, 

240 ) 

241 hub.capture_event(event, hint=hint) 

242 

243 return exc_info 

244 

245 

246class _ScopedResponse(object): 

247 __slots__ = ("_response", "_hub") 

248 

249 def __init__(self, hub, response): 

250 # type: (Hub, Iterator[bytes]) -> None 

251 self._hub = hub 

252 self._response = response 

253 

254 def __iter__(self): 

255 # type: () -> Iterator[bytes] 

256 iterator = iter(self._response) 

257 

258 while True: 

259 with self._hub: 

260 try: 

261 chunk = next(iterator) 

262 except StopIteration: 

263 break 

264 except BaseException: 

265 reraise(*_capture_exception(self._hub)) 

266 

267 yield chunk 

268 

269 def close(self): 

270 # type: () -> None 

271 with self._hub: 

272 try: 

273 self._response.close() # type: ignore 

274 except AttributeError: 

275 pass 

276 except BaseException: 

277 reraise(*_capture_exception(self._hub)) 

278 

279 

280def _make_wsgi_event_processor(environ, use_x_forwarded_for): 

281 # type: (Dict[str, str], bool) -> EventProcessor 

282 # It's a bit unfortunate that we have to extract and parse the request data 

283 # from the environ so eagerly, but there are a few good reasons for this. 

284 # 

285 # We might be in a situation where the scope/hub never gets torn down 

286 # properly. In that case we will have an unnecessary strong reference to 

287 # all objects in the environ (some of which may take a lot of memory) when 

288 # we're really just interested in a few of them. 

289 # 

290 # Keeping the environment around for longer than the request lifecycle is 

291 # also not necessarily something uWSGI can deal with: 

292 # https://github.com/unbit/uwsgi/issues/1950 

293 

294 client_ip = get_client_ip(environ) 

295 request_url = get_request_url(environ, use_x_forwarded_for) 

296 query_string = environ.get("QUERY_STRING") 

297 method = environ.get("REQUEST_METHOD") 

298 env = dict(_get_environ(environ)) 

299 headers = _filter_headers(dict(_get_headers(environ))) 

300 

301 def event_processor(event, hint): 

302 # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any] 

303 with capture_internal_exceptions(): 

304 # if the code below fails halfway through we at least have some data 

305 request_info = event.setdefault("request", {}) 

306 

307 if _should_send_default_pii(): 

308 user_info = event.setdefault("user", {}) 

309 if client_ip: 

310 user_info.setdefault("ip_address", client_ip) 

311 

312 request_info["url"] = request_url 

313 request_info["query_string"] = query_string 

314 request_info["method"] = method 

315 request_info["env"] = env 

316 request_info["headers"] = headers 

317 

318 return event 

319 

320 return event_processor