Source code for top.web.task
import unicodedata
from dataclasses import dataclass
from typing import Optional, List, Tuple
from dataclasses_json import dataclass_json
from top.core.task import Task
[docs]@dataclass_json
@dataclass
class HTTPTask(Task):
"""A task for tracking HTTP requests."""
#: Protocol from incoming HTTP request
protocol: Optional[str] = None
#: Host from incoming HTTP request
host: Optional[str] = None
#: HTTP method like GET, POST, put
method: Optional[str] = None
#: Request path, like /api/my-func
path: Optional[str] = None
#: HTTP GET request params
params: Optional[dict] = None
#: The full request URI if available
uri: Optional[str] = None
#: IP address that connected to the web server.
#:
#: This is the direct IP address of the TCP/IP connection.
#: You probably want :py:meth:`get_original_ip` in most of the
#: cases.
#:
client_ip_address: Optional[str] = None
#: Request HTTP headers.
#:
#: Available as key value mapping.
#:
#: Note that in HTTP protocol a header can appear twice.
#:
#: Uppercase all key names.
#:
request_headers: Optional[List[Tuple[str, str]]] = None
#: When response has been generated, what code did we sent.
#: Only available when the request processing has finished.
status_code: Optional[int] = None
#: Server status message
status_message: Optional[str] = None
#: Response HTTP headers.
#:
#: Available as key value mapping.
#:
#: Note that in HTTP protocol a header can appear twice.
#: Uppercase all key names.
#:
response_headers: Optional[List[Tuple[str, str]]] = None
def __repr__(self):
if self.params:
params = " ".join([f"{key}={value}" for key, value in self.params.items()])
else:
params = """"""
if self.status_code:
return f"<HTTPTask {self.method} {self.path} {params} {self.status_code}>"
else:
return f"<HTTPTask {self.method} {self.path} {params}>"
[docs] def get_single_request_header(self, name: str) -> Optional[str]:
"""Get a value of a single HTTP header in a request.
:param name:
Case insensitive HTTP header name
:raise AssertionError:
If the same header appears twice
:return:
The header value
"""
if not self.request_headers:
return None
retval = None
for header, value in self.request_headers:
if header.upper() == name.upper():
assert not retval, "The header appears twice: {name}"
retval = value
return retval
[docs] def get_single_response_header(self, name: str) -> Optional[str]:
"""Get a value of a single HTTP header in a response.
:param name:
Case insensitive HTTP header name
:raise AssertionError:
If the same header appears twice
:return:
The header value
"""
if not self.response_headers:
return None
retval = None
for header, value in self.response_headers:
if header.upper() == name.upper():
assert not retval, "The header appears twice: {name}"
retval = value
return retval
[docs] def get_host(self) -> str:
"""HTTP request header shortcut method."""
return self.get_single_request_header("HOST")
[docs] def get_user_agent(self) -> str:
"""HTTP request header shortcut method."""
return self.get_single_request_header("USER-AGENT")
[docs] def get_accept_encoding(self) -> str:
"""HTTP request header shortcut method."""
return self.get_single_request_header("ACCEPT-ENCODING")
[docs] def get_content_length(self) -> Optional[int]:
"""Get the content length of the response.
If not set return None.
"""
val = self.get_single_response_header("Content-length")
if val:
return int(val)
return None
[docs] def get_ip_country(self) -> str:
"""Return the country code of the client IP address.
Currently only supports `cf-ipcountry <https://support.cloudflare.com/hc/en-us/articles/200168236-Configuring-IP-geolocation>`_
header.
:return:
ISO 3166-1 Alpha 2 country code of the client as uppercase.
Special codes like T1 are returned for Tor network, etc.
"""
return self.get_single_request_header("CF-IPCOUNTRY")
[docs] def get_flag(self) -> str:
"""Return the Unicode flag emoticon based on the country IP address.
`Based on emoji-country-flag <https://pypi.org/project/emoji-country-flag/>`_.
See :py:meth:`get_ip_country`
"""
# TODO: Waiting for issue
code = self.get_ip_country()
if code is not None:
try:
flag = "".join(
unicodedata.lookup(f"REGIONAL INDICATOR SYMBOL LETTER {char}")
for char in code
)
return flag
except KeyError:
# T1 = Tor country
return ""
else:
return ""
[docs] def get_original_ip(self) -> str:
"""Get the originating IP address of the requestor.
In the case the HTTP request was forwarded through services
like Cloudflare and reverse proxies like Nginx and Apache,
get the user IP address.
"""
forwarded_for = self.get_single_request_header("X-FORWARDED-FOR")
if forwarded_for:
return forwarded_for.split(",")[0].strip()
return self.client_ip_address