diff --git a/xcap/http_utils.py b/xcap/http_utils.py index c201ab5..0ca7d9d 100644 --- a/xcap/http_utils.py +++ b/xcap/http_utils.py @@ -1,158 +1,172 @@ import datetime -from typing import List, Optional, Union +from typing import List, Optional +from fastapi import Request + + +def get_client_ip(request: Request) -> Optional[str]: + forwarded_for = request.headers.get('X-Forwarded-For') + + if forwarded_for: + client_ip = forwarded_for.split(',')[0].strip() + elif request.client: + client_ip = request.client.host + else: + client_ip = None + + return client_ip def tokenize(header, foldCase=True): """Tokenize a string according to normal HTTP header parsing rules. In particular: - Whitespace is irrelevant and eaten next to special separator tokens. Its existence (but not amount) is important between character strings. - Quoted string support including embedded backslashes. - Case is insignificant (and thus lowercased), except in quoted strings. (unless foldCase=False) - Multiple headers are concatenated with ',' NOTE: not all headers can be parsed with this function. Takes a raw header value (list of strings), and Returns a generator of strings. """ tokens = set("()<>@,;:\\\"/[]?={} \t") # Special separator characters ctls = set(chr(i) for i in range(0, 32)) # Control characters string = ",".join(header) start = 0 cur = 0 quoted = False qpair = False inSpaces = -1 qstring = None for x in string: if quoted: if qpair: qpair = False qstring = qstring + string[start:cur-1] + x start = cur + 1 elif x == '\\': qpair = True elif x == '"': quoted = False yield qstring + string[start:cur] qstring = None start = cur + 1 elif x in tokens: if start != cur: if foldCase: yield string[start:cur].lower() else: yield string[start:cur] start = cur + 1 if x == '"': quoted = True qstring = "" inSpaces = False elif x in " \t": if inSpaces is False: inSpaces = True else: inSpaces = -1 yield x # Yield the special character directly as a string elif x in ctls: raise ValueError(f"Invalid control character: {ord(x)} in header") else: if inSpaces is True: yield ' ' # Yield space as a plain string inSpaces = False inSpaces = False cur = cur + 1 if qpair: raise ValueError("Missing character after '\\'") if quoted: raise ValueError("Missing end quote") if start != cur: if foldCase: yield string[start:cur].lower() else: yield string[start:cur] def quoteString(s): return '"%s"' % s.replace('\\', '\\\\').replace('"', '\\"') # Helper function to parse comma-separated header values def parse_list_header(header_value: Optional[str]) -> List[Optional[str]]: if header_value: parsed_etags = [] for token in header_value.split(','): stripped_token = token.strip('"') # Remove any surrounding quotes if stripped_token == '*': parsed_etags.append('*') # Append '*' if it's a star else: parsed_etags.append(ETag.parse(tokenize([stripped_token]))) # Parse the ETag if it's not a star return parsed_etags return [] # Helper function to parse date-time headers like If-Modified-Since def parse_datetime(value: str) -> Optional[datetime.datetime]: try: return datetime.datetime.strptime(value, "%a, %d %b %Y %H:%M:%S GMT") except ValueError: return None class ETag(object): def __init__(self, tag, weak=False): self.tag = str(tag) self.weak = weak def match(self, other, strongCompare): # Sec 13.3. # The strong comparison function: in order to be considered equal, both # validators MUST be identical in every way, and both MUST NOT be weak. # # The weak comparison function: in order to be considered equal, both # validators MUST be identical in every way, but either or both of # them MAY be tagged as "weak" without affecting the result. if not isinstance(other, ETag) or other.tag != self.tag: return False if strongCompare and (other.weak or self.weak): return False return True def __eq__(self, other): return isinstance(other, ETag) and other.tag == self.tag and other.weak == self.weak def __ne__(self, other): return not self.__eq__(other) def __repr__(self): return "Etag(%r, weak=%r)" % (self.tag, self.weak) def parse(tokens): tokens = tuple(tokens) if len(tokens) == 1 and isinstance(tokens[0], str): return ETag(tokens[0]) if(len(tokens) == 3 and tokens[0] == "w" and tokens[1] == '/'): return ETag(tokens[2], weak=True) raise ValueError("Invalid ETag.") parse = staticmethod(parse) def generate(self): if self.weak: return 'W/'+quoteString(self.tag) else: return quoteString(self.tag)