Files
backend/routers/ip.py
T
2026-06-14 16:22:07 +03:00

193 lines
6.2 KiB
Python

import json
from typing import Optional, Dict, Any, Tuple
from bs4 import BeautifulSoup
from pydantic import BaseModel, Field
from fastapi import APIRouter, Request
from httpx import AsyncClient
# ==================== Pydantic модели ====================
class LocationModel(BaseModel):
"""Геоданные"""
city: Optional[str] = None
region: Optional[str] = None
country: Optional[str] = None
postal_code: Optional[str] = None
latitude: Optional[float] = None
longitude: Optional[float] = None
timezone: Optional[str] = None
class ASNModel(BaseModel):
"""ASN данные"""
asn: Optional[str] = None
name: Optional[str] = None
route: Optional[str] = None
class CompanyModel(BaseModel):
"""Данные компании"""
name: Optional[str] = None
abuse_contact: Optional[str] = None
class SecurityModel(BaseModel):
"""Данные безопасности"""
vpn: bool = False
proxy: bool = False
tor: bool = False
hosting: bool = False
class IPInfoModel(BaseModel):
"""Главная модель IP информации"""
ip: str
url: Optional[str] = None
location: LocationModel = Field(default_factory=LocationModel)
asn: ASNModel = Field(default_factory=ASNModel)
company: CompanyModel = Field(default_factory=CompanyModel)
security: SecurityModel = Field(default_factory=SecurityModel)
# ==================== Парсер ====================
class IPInfo:
"""Парсер IP информации из JSON-LD"""
def __init__(self, html: str):
self._data = self._parse(html)
def _parse(self, html: str) -> Optional[IPInfoModel]:
"""Парсит HTML и возвращает модель"""
soup = BeautifulSoup(html, 'html.parser')
script = soup.find('script', type='application/ld+json')
if not script or not script.string:
return None
try:
data: Dict = json.loads(script.string.strip())
except json.JSONDecodeError:
return None
if data.get('@type') != 'DataFeed':
return None
# Базовые поля
result = IPInfoModel(
ip=data.get('name', ''),
url=data.get('url')
)
# Парсим локацию
location = data.get('contentLocation', {})
address = location.get('address', {})
geo = location.get('geo', {})
result.location = LocationModel(
city=address.get('addressLocality'),
region=address.get('addressRegion'),
country=address.get('addressCountry'),
postal_code=address.get('postalCode'),
latitude=geo.get('latitude'),
longitude=geo.get('longitude'),
timezone=self._find_value(data, 'Timezone')
)
# Парсим ASN
result.asn = ASNModel(
asn=self._find_value(data, 'ASN'),
name=self._find_value(data, 'AS Name'),
route=self._find_value(data, 'AS Route')
)
# Парсим компанию
result.company = CompanyModel(
name=self._find_value(data, 'Company Name'),
abuse_contact=self._find_value(data, 'Abuse Contact')
)
# Парсим безопасность
result.security = SecurityModel(
vpn=self._find_bool(data, 'VPN'),
proxy=self._find_bool(data, 'Proxy'),
tor=self._find_bool(data, 'Tor'),
hosting=self._find_bool(data, 'Hosting')
)
return result
def _find_value(self, data: Dict, name: str) -> Optional[str]:
"""Поиск значения в variableMeasured"""
for item in data.get('variableMeasured', []):
if item.get('name') == name:
return item.get('value')
return None
def _find_bool(self, data: Dict, name: str) -> bool:
"""Поиск булевого значения"""
value = self._find_value(data, name)
return value == 'Yes'
def dict(self) -> Optional[Dict[str, Any]]:
"""Возвращает словарь с данными"""
if self._data:
return self._data.model_dump(exclude_none=True)
return None
@property
def is_valid(self) -> bool:
"""Проверяет, удалось ли распарсить данные"""
return self._data is not None
@property
def coordinates(self) -> Optional[Tuple[float, float]]:
"""Возвращает координаты как tuple"""
if self._data and self._data.location.latitude and self._data.location.longitude:
return (self._data.location.latitude, self._data.location.longitude)
return None
# ==================== Роутер ====================
router = APIRouter(prefix='/api/v1', tags=['ip'])
import logging
logging.basicConfig(level=logging.DEBUG)
@router.get('/ip')
async def get_ip(request: Request):
# 1. Проверяем заголовки
forwarded = request.headers.get('X-Forwarded-For')
print(f"[DEBUG] X-Forwarded-For: {forwarded}")
print(f"[DEBUG] All headers: {dict(request.headers)}")
# 2. Определяем IP
if forwarded:
client_ip = forwarded.split(',')[0].strip()
else:
client_ip = request.client.host
print(f"[DEBUG] Client IP: {client_ip}")
# 3. Проверяем запрос к ipinfo.io
async with AsyncClient() as client:
url = f'https://ipinfo.io/{client_ip}'
print(f"[DEBUG] Request URL: {url}")
response = await client.get(url)
print(f"[DEBUG] Response status: {response.status_code}")
print(f"[DEBUG] Response headers: {response.headers}")
print(f"[DEBUG] Response text (first 500 chars): {response.text[:500]}")
html = response.text
ipinfo = IPInfo(html)
print(f"[DEBUG] IPInfo is_valid: {ipinfo.is_valid}")
print(f"[DEBUG] IPInfo._data: {ipinfo._data}")
if ipinfo._data is None:
return {"error": "Parse failed", "ip": client_ip, "raw_html_preview": html[:200]}
return ipinfo.dict()