dane w headerach i inne funkcje

This commit is contained in:
Mateusz Gruszczyński
2025-10-09 16:40:56 +02:00
parent cb109b63ae
commit eb137c87b0
7 changed files with 161 additions and 76 deletions

View File

@@ -1,4 +1,4 @@
from fastapi import APIRouter, Request, Depends, HTTPException, status
from fastapi import APIRouter, Request, Depends, HTTPException, status, Response
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from .deps import get_geo
from .config import settings
@@ -11,37 +11,47 @@ router = APIRouter()
security = HTTPBasic()
VENDOR_SINGLE_IP_HEADERS = [
"cf-connecting-ip", # Cloudflare
"true-client-ip", # Akamai/F5
"cf-connecting-ip", # Cloudflare
"true-client-ip", # Akamai/F5
"x-cluster-client-ip", # niektóre load balancery
"x-real-ip", # klasyk (nginx/traefik)
"x-real-ip", # klasyk (nginx/traefik)
]
def _check_admin(creds: HTTPBasicCredentials):
user = settings.admin_user
pwd = settings.admin_pass
if not user or not pwd:
raise HTTPException(status_code=403, detail='admin credentials not configured')
raise HTTPException(status_code=403, detail="admin credentials not configured")
# constant-time compare
if not (secrets.compare_digest(creds.username, user) and secrets.compare_digest(creds.password, pwd)):
raise HTTPException(status_code=401, detail='invalid credentials', headers={"WWW-Authenticate":"Basic"})
if not (
secrets.compare_digest(creds.username, user)
and secrets.compare_digest(creds.password, pwd)
):
raise HTTPException(
status_code=401,
detail="invalid credentials",
headers={"WWW-Authenticate": "Basic"},
)
return True
def _normalize_ip_str(ip_raw: str) -> str | None:
"""Usuń port, whitespace i ewentualne cudzysłowy"""
if not ip_raw:
return None
ip_raw = ip_raw.strip().strip('"').strip("'")
# usuń port, np. 1.2.3.4:5678
if ':' in ip_raw and ip_raw.count(':') == 1:
if ":" in ip_raw and ip_raw.count(":") == 1:
# prawdopodobnie IPv4:port
ip_raw = ip_raw.split(':')[0]
ip_raw = ip_raw.split(":")[0]
# Pozostaw kwestie IPv6 z %zone
return ip_raw
def _is_ip_trusted(ip_str: str) -> bool:
try:
ip = ipaddress.ip_address(ip_str.split('%')[0])
ip = ipaddress.ip_address(ip_str.split("%")[0])
except Exception:
return False
for net in settings.trusted_proxies:
@@ -52,17 +62,32 @@ def _is_ip_trusted(ip_str: str) -> bool:
continue
return False
def _extract_from_forwarded(header_value: str) -> list[str]:
# Forwarded: for=192.0.2.43, for="[2001:db8:cafe::17]";proto=http;by=...
ips = []
parts = re.split(r',\s*(?=[fF]or=)', header_value)
parts = re.split(r",\s*(?=[fF]or=)", header_value)
for part in parts:
m = re.search(r'for=(?P<val>"[^"]+"|[^;,\s]+)', part)
if m:
val = m.group('val').strip('"').strip("'")
val = m.group("val").strip('"').strip("'")
ips.append(val)
return ips
def geo_headers(data: dict) -> dict:
h = {}
country = data.get("country", {}).get("name") if data.get("country") else None
city = data.get("city")
ip_val = data.get("ip")
if ip_val and country:
h["X-IP-ADDRESS"] = ip_val
h["X-COUNTRY"] = country
if city:
h["X-CITY"] = city
return h
def get_client_ip(request: Request) -> str:
"""
Zwraca IP klienta biorąc pod uwagę:
@@ -127,48 +152,44 @@ def get_client_ip(request: Request) -> str:
try:
host = request.client.host
if host:
return host.split('%')[0] if '%' in host else host
return host.split("%")[0] if "%" in host else host
except Exception:
pass
return "0.0.0.0"
@router.get('/ip')
@router.get("/ip")
async def my_ip(request: Request, geo=Depends(get_geo)):
ip = get_client_ip(request)
# handle IPv6 mapped IPv4 like ::ffff:1.2.3.4
try:
ip = ip.split('%')[0]
except Exception:
pass
return geo.lookup(ip)
data = geo.lookup(ip)
return Response(
content=data.__str__(), media_type="application/json", headers=geo_headers(data)
)
@router.get('/ip/{ip_address}')
@router.get("/ip/{ip_address}")
async def ip_lookup(ip_address: str, geo=Depends(get_geo)):
# validate IP
try:
# allow zone index for IPv6 and strip it for validation
if '%' in ip_address:
addr = ip_address.split('%')[0]
else:
addr = ip_address
ipaddress.ip_address(addr)
except Exception:
raise HTTPException(status_code=400, detail='invalid IP address')
return geo.lookup(ip_address)
data = geo.lookup(ip_address)
return Response(
content=data.__str__(), media_type="application/json", headers=geo_headers(data)
)
@router.post('/reload')
@router.post("/reload")
async def reload(creds: HTTPBasicCredentials = Depends(security)):
_check_admin(creds)
provider = reload_provider()
return {'reloaded': True, 'provider': type(provider).__name__}
return {"reloaded": True, "provider": type(provider).__name__}
@router.get('/health')
@router.get("/health")
async def health():
return {'status':'ok'}
return {"status": "ok"}
#from fastapi import Request
#@router.get("/_debug/headers")
#async def debug_headers(request: Request):
# return {"headers": dict(request.headers)}
# from fastapi import Request
# @router.get("/_debug/headers")
# async def debug_headers(request: Request):
# return {"headers": dict(request.headers)}