def test_headers(): h = Headers(raw=[(b"a", b"123"), (b"a", b"456"), (b"b", b"789")]) assert "a" in h assert "A" in h assert "b" in h assert "B" in h assert "c" not in h assert h["a"] == "123" assert h.get("a") == "123" assert h.get("nope", default=None) is None assert h.getlist("a") == ["123", "456"] assert h.keys() == ["a", "a", "b"] assert h.values() == ["123", "456", "789"] assert h.items() == [("a", "123"), ("a", "456"), ("b", "789")] assert list(h) == ["a", "a", "b"] assert dict(h) == {"a": "123", "b": "789"} assert repr( h) == "Headers(raw=[(b'a', b'123'), (b'a', b'456'), (b'b', b'789')])" assert h == Headers(raw=[(b"a", b"123"), (b"b", b"789"), (b"a", b"456")]) assert h != [(b"a", b"123"), (b"A", b"456"), (b"b", b"789")] h = Headers({"a": "123", "b": "789"}) assert h["A"] == "123" assert h["B"] == "789" assert h.raw == [(b"a", b"123"), (b"b", b"789")] assert repr(h) == "Headers({'a': '123', 'b': '789'})"
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: if self.allow_any or scope["type"] not in ( "http", "websocket", ): # pragma: no cover await self.app(scope, receive, send) return headers = Headers(scope=scope) host = headers.get("host", "").split(":")[0] is_valid_host = False found_www_redirect = False for pattern in self.allowed_hosts: if host == pattern or ( pattern.startswith("*") and host.endswith(pattern[1:]) ): is_valid_host = True break elif "www." + host == pattern: found_www_redirect = True if is_valid_host: await self.app(scope, receive, send) else: response: Response if found_www_redirect and self.www_redirect: url = URL(scope=scope) redirect_url = url.replace(netloc="www." + url.netloc) response = RedirectResponse(url=str(redirect_url)) else: response = PlainTextResponse("Invalid host header", status_code=400) await response(scope, receive, send)
async def __call__( self, scope, receive, send ) -> None: if scope["type"] != "http": # pragma: no cover handler = await self.app(scope, receive, send) await handler.__call__(receive, send) return method = scope["method"] headers = Headers(scope=scope) origin = headers.get("origin") if origin is None: handler = await self.app(scope, receive, send) await handler.__call__(receive, send) return if method == "OPTIONS" and "access-control-request-method" in headers: response = self.preflight_response(request_headers=headers) await response(scope, receive, send) return await self.simple_response( scope, receive, send, request_headers=headers )
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: if scope["type"] == "http": headers = Headers(scope=scope) if "gzip" in headers.get("Accept-Encoding", ""): responder = GZipResponder(self.app, self.minimum_size) await responder(scope, receive, send) return await self.app(scope, receive, send)
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: if scope["type"] != "http": await self.app(scope, receive, send) return headers = Headers(scope=scope) span_ctx = headers.get("x-noc-span-ctx", 0) span_id = headers.get("x-noc-span", 0) sample = 1 if span_ctx and span_id else 0 with Span( server=self.service_name, service="api", sample=sample, parent=span_id, context=span_ctx, in_label=scope["path"], ): await self.app(scope, receive, send)
def __call__(self, scope: Scope) -> ASGIInstance: if scope["type"] in ("http", "websocket") and not self.allow_any: headers = Headers(scope=scope) host = headers.get("host") if host not in self.allowed_hosts: return PlainTextResponse("Invalid host header", status_code=400) return self.app(scope)
def is_not_modified(self, stat_headers: typing.Dict[str, str]) -> bool: etag = stat_headers["etag"] last_modified = stat_headers["last-modified"] req_headers = Headers(scope=self.scope) if etag == req_headers.get("if-none-match"): return True if "if-modified-since" not in req_headers: return False last_req_time = req_headers["if-modified-since"] return parsedate(last_req_time) >= parsedate(last_modified) # type: ignore
def get_context(self, scope): request_method = scope.get("method") if request_method: trace_name = f"starlette_http_{request_method.lower()}" else: trace_name = "starlette_http" headers = Headers(scope=scope) return { "name": trace_name, "type": "http_server", "request.host": headers.get("host"), "request.method": request_method, "request.path": scope.get("path"), "request.content_length": int(headers.get("content-length", 0)), "request.user_agent": headers.get("user-agent"), "request.scheme": scope.get("scheme"), "request.query": scope.get("query_string").decode("ascii"), }
def get_jwt(self, header: Headers) -> str: if "Authorization" not in header: return token = header.get("Authorization") scheme, jwt = token.split() if scheme.lower() != "bearer": raise InvalidAuthorizationToken("header dose not start \ with 'Bearer'") return jwt
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: if scope["type"] == "http": headers = Headers(scope=scope) if "br" in headers.get("Accept-Encoding", ""): responder = BrotliResponder( self.app, self.quality, self.mode, self.lgwin, self.lgblock, self.minimum_size, ) await responder(scope, receive, send) return if self.gzip_fallback and "gzip" in headers.get( "Accept-Encoding", ""): responder = GZipResponder(self.app, self.minimum_size) await responder(scope, receive, send) return await self.app(scope, receive, send)
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: headers = Headers(scope=scope) self.should_decode_from_msgpack_to_json = ( "application/x-msgpack" in headers.get("content-type", "") ) # Take an initial guess, although we eventually may not # be able to do the conversion. self.should_encode_from_json_to_msgpack = ( "application/x-msgpack" in headers.getlist("accept") ) self.receive = receive self.send = send await self.app(scope, self.receive_with_msgpack, self.send_with_msgpack)
def matches(self, scope: Scope) -> typing.Tuple[Match, Scope]: headers = Headers(scope=scope) host = headers.get("host", "").split(":")[0] match = self.host_regex.match(host) if match: matched_params = match.groupdict() for key, value in matched_params.items(): matched_params[key] = self.param_convertors[key].convert(value) path_params = dict(scope.get("path_params", {})) path_params.update(matched_params) child_scope = {"path_params": path_params, "endpoint": self.app} return Match.FULL, child_scope return Match.NONE, {}
def __call__(self, scope: Scope) -> ASGIInstance: if scope["type"] == "http": method = scope["method"] headers = Headers(scope=scope) origin = headers.get("origin") if origin is not None: if method == "OPTIONS" and "access-control-request-method" in headers: return self.preflight_response(request_headers=headers) else: return functools.partial(self.simple_response, scope=scope, request_headers=headers) return self.app(scope)
def __call__(self, scope: Scope) -> ASGIInstance: if scope["type"] in ("http", "websocket") and not self.allow_any: headers = Headers(scope=scope) host = headers.get("host", "").split(":")[0] for pattern in self.allowed_hosts: if ( host == pattern or pattern.startswith("*") and host.endswith(pattern[1:]) ): break else: return PlainTextResponse("Invalid host header", status_code=400) return self.app(scope)
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: if scope["type"] == "http": headers = Headers(scope=scope) accepted = { item.strip() for item in headers.get("accept-encoding", "").split(",") if item } responder = CompressionResponder(self.app, self.minimum_size, accepted, self.compression_registry) await responder(scope, receive, send) return await self.app(scope, receive, send)
async def send_with_logging(self, message: Message) -> None: if message["type"] == "http.response.start": self._response_status_code = message.get("status") headers = Headers(raw=message["headers"]) self.should_log_response_body = "application/json" in headers.get( "content-type", "") await self.send(message) elif message["type"] == "http.response.body": if not self.should_log_response_body: await self.send(message) return body: bytes = message.get("body", b"") self._response_body.extend(body) await self.send(message)
def preflight_response(self, request_headers: Headers) -> ASGIInstance: requested_origin = request_headers["origin"] requested_method = request_headers["access-control-request-method"] requested_headers = request_headers.get( "access-control-request-headers") requested_cookie = "cookie" in request_headers headers = dict(self.preflight_headers) failures = [] if self.is_allowed_origin(origin=requested_origin): if not self.allow_all_origins: # If self.allow_all_origins is True, then the "Access-Control-Allow-Origin" # header is already set to "*". # If we only allow specific origins, then we have to mirror back # the Origin header in the response. headers["Access-Control-Allow-Origin"] = requested_origin else: failures.append("origin") if requested_method not in self.allow_methods: failures.append("method") # If we allow all headers, then we have to mirror back any requested # headers in the response. if self.allow_all_headers and requested_headers is not None: headers["Access-Control-Allow-Headers"] = requested_headers elif requested_headers is not None: for header in requested_headers.split(","): if header.strip() not in self.allow_headers: failures.append("headers") # We don't strictly need to use 400 responses here, since its up to # the browser to enforce the CORS policy, but its more informative # if we do. if failures: failure_text = "Disallowed CORS " + ", ".join(failures) return PlainTextResponse(failure_text, status_code=400, headers=headers) return PlainTextResponse("OK", status_code=200, headers=headers)
def __call__(self, scope: Scope) -> ASGIInstance: if scope["type"] in ("http", "websocket") and not self.allow_any: headers = Headers(scope=scope) host = headers.get("host", "").split(":")[0] found_www_redirect = False for pattern in self.allowed_hosts: if host == pattern or (pattern.startswith("*") and host.endswith(pattern[1:])): break elif "www." + host == pattern: found_www_redirect = True else: if found_www_redirect and self.www_redirect: url = URL(scope=scope) redirect_url = url.replace(netloc="www." + url.netloc) return RedirectResponse(url=str(redirect_url)) return PlainTextResponse("Invalid host header", status_code=400) return self.app(scope)
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: self.receive = receive self.send = send request = Request(scope) if self._should_ignore_request(request): await self.app(scope, self.receive, self.send) return self._method = request.method self._path = request.url.path headers = Headers(scope=scope) self.should_log_request_body = "application/json" in headers.get( "content-type", "") await self.app(scope, self.receive_with_logging, self.send_with_logging) self._safe_log_request_response()
def preflight_response(self, request_headers: Headers) -> Response: requested_origin = request_headers["origin"] requested_method = request_headers["access-control-request-method"] requested_headers = request_headers.get( "access-control-request-headers") headers = dict(self.preflight_headers) failures = [] if self.is_allowed_origin(origin=requested_origin): if self.preflight_explicit_allow_origin: # The "else" case is already accounted for in self.preflight_headers # and the value would be "*". headers["Access-Control-Allow-Origin"] = requested_origin else: failures.append("origin") if requested_method not in self.allow_methods: failures.append("method") # If we allow all headers, then we have to mirror back any requested # headers in the response. if self.allow_all_headers and requested_headers is not None: headers["Access-Control-Allow-Headers"] = requested_headers elif requested_headers is not None: for header in [h.lower() for h in requested_headers.split(",")]: if header.strip() not in self.allow_headers: failures.append("headers") break # We don't strictly need to use 400 responses here, since its up to # the browser to enforce the CORS policy, but its more informative # if we do. if failures: failure_text = "Disallowed CORS " + ", ".join(failures) return PlainTextResponse(failure_text, status_code=400, headers=headers) return PlainTextResponse("OK", status_code=200, headers=headers)
def _get_token(self, headers: Headers): authorization = headers.get("Authorization") if authorization and authorization.startswith("Bearer "): return authorization[7:]
def __call__(self, scope): headers = Headers(scope["headers"]) if headers.get("host") != self.hostname: return PlainTextResponse("Invalid host header", status_code=400) return self.app(scope)
def __call__(self, scope: Scope) -> ASGIInstance: if scope["type"] == "http": headers = Headers(scope=scope) if "gzip" in headers.get("Accept-Encoding", ""): return GZipResponder(self.app, scope, self.minimum_size) return self.app(scope)
def _get_content_range(headers_200: Headers, s: slice) -> str: s = _normalize(headers_200, s) total = headers_200.get("content-length", "*") return f"bytes {s.start}-{s.stop - 1}/{total}"
async def __call__(self, scope, receive, send): headers = Headers(scope=scope) scope["user"] = "******" if headers.get( "Authorization") == "Bearer 123" else None await self.app(scope, receive, send)
def __call__(self, scope): headers = Headers(scope=scope) scope["user"] = "******" if headers.get( "Authorization") == "Bearer 123" else None return self.app(scope)
async def parse(self) -> FormData: # Parse the Content-Type header to get the multipart boundary. content_type, params = parse_options_header(self.headers["Content-Type"]) boundary = params.get(b"boundary") # Callbacks dictionary. callbacks = { "on_part_begin": self.on_part_begin, "on_part_data": self.on_part_data, "on_part_end": self.on_part_end, "on_header_field": self.on_header_field, "on_header_value": self.on_header_value, "on_header_end": self.on_header_end, "on_headers_finished": self.on_headers_finished, "on_end": self.on_end, } # Create the parser. parser = multipart.MultipartParser(boundary, callbacks) header_field = b"" header_value = b"" raw_headers = [] # type: typing.List[typing.Tuple[bytes, bytes]] field_name = "" data = b"" file = None # type: typing.Optional[UploadFile] items = ( [] ) # type: typing.List[typing.Tuple[str, typing.Union[str, UploadFile]]] # Feed the parser with data from the request. async for chunk in self.stream: parser.write(chunk) messages = list(self.messages) self.messages.clear() for message_type, message_bytes in messages: if message_type == MultiPartMessage.PART_BEGIN: raw_headers = [] data = b"" elif message_type == MultiPartMessage.HEADER_FIELD: header_field += message_bytes elif message_type == MultiPartMessage.HEADER_VALUE: header_value += message_bytes elif message_type == MultiPartMessage.HEADER_END: raw_headers.append((header_field.lower(), header_value)) header_field = b"" header_value = b"" elif message_type == MultiPartMessage.HEADERS_FINISHED: headers = Headers(raw=raw_headers) content_disposition = headers.get("Content-Disposition") content_type = headers.get("Content-Type", "") disposition, options = parse_options_header(content_disposition) field_name = options[b"name"].decode("latin-1") if b"filename" in options: filename = options[b"filename"].decode("latin-1") file = UploadFile(filename=filename, content_type=content_type) else: file = None elif message_type == MultiPartMessage.PART_DATA: if file is None: data += message_bytes else: await file.write(message_bytes) elif message_type == MultiPartMessage.PART_END: if file is None: items.append((field_name, data.decode("latin-1"))) else: await file.seek(0) items.append((field_name, file)) elif message_type == MultiPartMessage.END: pass parser.finalize() return FormData(items)
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: """Make the call.""" request_headers = Headers(scope=scope) range_header = request_headers.get("range", None) async with self.s3client as s3_client: try: kwargs = {"Bucket": self.bucket, "Key": self.key} if range_header is not None: kwargs["Range"] = range_header obj_info = await s3_client.get_object(**kwargs) last_modified = formatdate(datetime.timestamp( obj_info["LastModified"]), usegmt=True) self.headers.setdefault("content-length", str(obj_info["ContentLength"])) self.headers.setdefault("content-range", str(obj_info.get("ContentRange"))) self.headers.setdefault("last-modified", last_modified) self.headers.setdefault("etag", obj_info["ETag"]) except ClientError as err: self.status_code = 404 await send({ "type": "http.response.start", "status": self.status_code, "headers": self.raw_headers, }) await send({ "type": "http.response.body", "body": "File not found, details: " f"{json.dumps(err.response)}".encode("utf-8"), "more_body": False, }) except Exception as err: raise RuntimeError( f"File at path {self.path} does not exist, details: {err}" ) from err else: await send({ "type": "http.response.start", "status": self.status_code, "headers": self.raw_headers, }) if self.send_header_only: await send({ "type": "http.response.body", "body": b"", "more_body": False }) else: # Tentatively ignoring type checking failure to work around the # wrong type definitions for aiofiles that come with typeshed. See # https://github.com/python/typeshed/pull/4650 total_size = obj_info["ContentLength"] sent_size = 0 chunks = obj_info["Body"].iter_chunks( chunk_size=self.chunk_size) async for chunk in chunks: sent_size += len(chunk) await send({ "type": "http.response.body", "body": chunk, "more_body": sent_size < total_size, }) if self.background is not None: await self.background()
class CallbackClient: """ A class to abstract the callback logic. """ def __init__(self, function: Callable[..., Any], callback: Union[Dict[str, Any], bool]) -> None: self.__function = function self.__attribute_finders = self.__prepare_and_validate_finders( callback) self.__headers = Headers() self.__params: Dict[str, str] = {} self.__invalid_callback_object = "" @property def url(self) -> str: """Returns the callback URL if it was included, or None.""" location = self.__attribute_finders.get("callback_url_header") return self.__headers.get(location, "") if location is not None else "" @property def http_method(self) -> str: """Returns the callback HTTP method if it was included, or POST.""" location = self.__attribute_finders.get("callback_method_header", "") # POST is the default HTTP method http_method = self.__headers[ location] if location in self.__headers else "POST" return http_method.upper() @property def custom_key(self) -> Optional[str]: """Returns the custom callback key container if it was included, or None.""" location = self.__attribute_finders.get("custom_callback_key_header", "") if location in self.__headers: return self.__headers.get(location) return None def handle_callback(self, headers: Headers, params: Dict[str, Any]) -> JSONResponse: """ Validates that the callback data from the request is correct and delegates the main function call. Returns a JSON response. """ if self.__invalid_callback_object: # Callback object was defective, server should have stopped return JSONResponse({"message": self.__invalid_callback_object}, status_code=500) try: # Set headers and params self.__headers = headers self.__params = params # Validate callback data self.__validate_callback_url() self.__validate_callback_http_method() # Delegate function asyncio.ensure_future(self.__callback_call()) return JSONResponse({}, status_code=202) except InvalidCallbackHeadersError as error: return JSONResponse({"message": str(error)}, status_code=422) @staticmethod def __prepare_and_validate_finders( callback: Union[Dict[str, Any], bool]) -> Dict[str, str]: """ Collects and returns the callback data finders and validates that they are correct on decoration-time. """ try: validate_callback_data(callback) return get_header_finders(callback) except InvalidCallbackObjectError as error: log(str(error), level="critical") raise InvalidCallbackObjectError(error) from error def __validate_callback_url(self) -> None: """ Validates that the callback URL within the request header is valid. """ if not self.url: raise InvalidCallbackHeadersError("Invalid callback URL") def __validate_callback_http_method(self) -> None: """ Validates that the callback HTTP method within the request header is valid. """ if self.http_method.lower() not in HTTP_METHODS: raise InvalidCallbackHeadersError("Invalid callback HTTP method") async def __callback_call(self) -> None: """ Executes the function and makes the request to the callback endpoint. """ try: response = await generic_call(self.__function, self.__params) if self.custom_key is not None: response = {self.custom_key: response} async with httpx.AsyncClient() as client: await client.request( self.http_method, self.url, json=response, ) except Exception as error: message = "Error while executing the delegated method: " message += str(error) log(message, level="warn")
def prepare_fhir_scopes( scope: Scope, headers: Headers, params: QueryParams, errors: typing.List[typing.Any], ): # Generate Request ID scope["FHIR_REQUEST_ID"] = uuid.uuid4() # 1. Prepare Accept & FHIR Version # -------------------------------- accept = headers.get("accept", None) if accept: parts = accept.split(";") accept = parts[0].strip() if accept in ("application/json", "text/json"): accept = "application/fhir+json" if accept in ALLOWED_ACCEPTS: scope["FHIR_RESPONSE_ACCEPT"] = accept else: errors.append({ "loc": "Header.Accept", "msg": f"Accept mime '{accept}' is not supported.", "original": headers.get("accept"), }) if len(parts) > 1: version_str = None try: name, version_str = parts[1].strip().split("=") if name == "fhirVersion": version = MIME_FHIR_VERSION_MAP[version_str] scope["FHIR_VERSION"] = version scope["FHIR_VERSION_ORIGINAL"] = version_str else: errors.append({ "loc": "Header.Accept", "msg": "Invalid format of FHIR Version is provided in mime", "original": headers.get("accept"), }) except KeyError: errors.append({ "loc": "Header.Accept", "msg": f"Unsupported FHIR Version '{version_str}' is provided in mime", "original": headers.get("accept"), }) except ValueError: errors.append({ "loc": "Header.Accept", "msg": "Invalid format of FHIR Version is provided in mime", "original": headers.get("accept"), }) else: scope["FHIR_RESPONSE_ACCEPT"] = "application/fhir+json" if (scope.get("FHIR_VERSION_ORIGINAL", None) is None and scope.get("FHIR_VERSION", None) is None): scope["FHIR_VERSION"] = MIME_FHIR_VERSION_MAP[DEFAULT_FHIR_VERSION] # 2. Check Query String # --------------------- format_mime = params.get("_format", None) if format_mime is not None: if format_mime in ALLOWED_ACCEPTS: scope["FHIR_RESPONSE_FORMAT"] = format_mime else: errors.append({ "loc": "QueryString._format", "msg": f"Format mime '{format_mime}' is not supported.", "original": format_mime, }) pretty_response = params.get("_pretty", None) if pretty_response is not None: if pretty_response in ("true", "false"): scope["FHIR_RESPONSE_PRETTY"] = pretty_response == "true" else: errors.append({ "loc": "QueryString._pretty", "msg": f"Invalid ``_pretty`` value '{pretty_response}' is provided.", "original": pretty_response, }) # 3. Prepare Conditional Headers # ------------------------------ if headers.get("If-None-Exist"): scope["FHIR_CONDITION_NONE_EXIST"] = [ tuple( map(lambda x: x.strip(), headers.get("If-None-Exist").split("="))) ] if headers.get("If-Modified-Since"): try: scope["FHIR_CONDITION_MODIFIED_SINCE"] = parsedate_to_datetime( headers.get("If-Modified-Since")) except ValueError: errors.append({ "loc": "Header.If-Modified-Since", "msg": "Invalid formatted datetime value is provided.", "original": headers.get("If-Modified-Since"), }) if headers.get("If-None-Match"): try: scope["FHIR_CONDITION_NONE_MATCH"] = literal_eval( headers.get("If-None-Match").replace("W/", "")) except (SyntaxError, ValueError): errors.append({ "loc": "Header.If-None-Match", "msg": "Invalid formatted ETag value is provided.", "original": headers.get("If-None-Match"), }) if headers.get("If-Match"): try: scope["FHIR_CONDITION_MATCH"] = literal_eval( headers.get("If-Match").replace("W/", "")) except (SyntaxError, ValueError): errors.append({ "loc": "Header.If-Match", "msg": "Invalid formatted ETag value is provided.", "original": headers.get("If-Match"), })