Files
notifications-api/notifications_utils/request_helper.py
2025-02-04 07:39:42 -08:00

139 lines
4.9 KiB
Python

from flask import abort, current_app, request
from flask.wrappers import Request
TRACE_ID_HEADER = "X-B3-TraceId"
SPAN_ID_HEADER = "X-B3-SpanId"
PARENT_SPAN_ID_HEADER = "X-B3-ParentSpanId"
class NotifyRequest(Request):
"""
A custom Request class, implementing extraction of zipkin headers used to trace request through cloudfoundry
as described here: https://docs.cloudfoundry.org/concepts/http-routing.html#zipkin-headers
"""
@property
def request_id(self):
return self.trace_id
@property
def trace_id(self):
"""
The "trace id" (in zipkin terms) assigned to this request, if present (None otherwise)
"""
if not hasattr(self, "_trace_id"):
self._trace_id = self._get_header_value(TRACE_ID_HEADER)
return self._trace_id
@property
def span_id(self):
"""
The "span id" (in zipkin terms) set in this request's header, if present (None otherwise)
"""
if not hasattr(self, "_span_id"):
# note how we don't generate an id of our own. not being supplied a span id implies that we are running in
# an environment with no span-id-aware request router, and thus would have no intermediary to prevent the
# propagation of our span id all the way through all our onwards requests much like trace id. and the point
# of span id is to assign identifiers to each individual request.
self._span_id = self._get_header_value(SPAN_ID_HEADER)
return self._span_id
@property
def parent_span_id(self):
"""
The "parent span id" (in zipkin terms) set in this request's header, if present (None otherwise)
"""
if not hasattr(self, "_parent_span_id"):
self._parent_span_id = self._get_header_value(PARENT_SPAN_ID_HEADER)
return self._parent_span_id
def _get_header_value(self, header_name):
"""
Returns value of the given header
"""
if header_name in self.headers and self.headers[header_name]:
return self.headers[header_name]
return None
class ResponseHeaderMiddleware(object):
def __init__(self, app):
self._app = app
def __call__(self, environ, start_response):
try:
req = NotifyRequest(environ)
def rewrite_response_headers(status, headers, exc_info=None):
lower_existing_header_names = frozenset(
name.lower() for name, value in headers
)
if TRACE_ID_HEADER.lower() not in lower_existing_header_names:
headers.append((TRACE_ID_HEADER, str(req.trace_id)))
if SPAN_ID_HEADER.lower() not in lower_existing_header_names:
headers.append((SPAN_ID_HEADER, str(req.span_id)))
headers = [
(key, value)
for key, value in headers
if key.lower() not in ["server", "last-modified"]
]
return start_response(status, headers, exc_info)
return self._app(environ, rewrite_response_headers)
except BaseException as be: # noqa
if "AuthError" in str(be): # notify-api-1135
current_app.logger.exception("AuthError")
elif "AttributeError" in str(be): # notify-api-1394
current_app.logger.exception("AttributeError")
elif "MethodNotAllowed" in str(be): # notify-admin-1392
current_app.logger.exception("MethodNotAllowed")
else:
raise be
def init_app(app):
app.request_class = NotifyRequest
app.wsgi_app = ResponseHeaderMiddleware(app.wsgi_app)
def check_proxy_header_before_request():
keys = [
current_app.config.get("ROUTE_SECRET_KEY_1"),
current_app.config.get("ROUTE_SECRET_KEY_2"),
]
result, msg = _check_proxy_header_secret(request, keys)
if not result:
if current_app.config.get("CHECK_PROXY_HEADER", False):
current_app.logger.warning(msg)
abort(403)
# We need to return None to continue processing the request
# http://flask.pocoo.org/docs/0.12/api/#flask.Flask.before_request
return None
def _check_proxy_header_secret(request, secrets, header="X-Custom-Forwarder"):
if header not in request.headers:
return False, "Header missing"
header_secret = request.headers.get(header)
if not header_secret:
return False, "Header exists but is empty"
# if there isn't any non-empty secret configured we fail closed
if not any(secrets):
return False, "Secrets are not configured"
for i, secret in enumerate(secrets):
if header_secret == secret:
return True, "Key used: {}".format(
i + 1
) # add 1 to make it human-compatible
return False, "Header didn't match any keys"