You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
186 lines
7.7 KiB
186 lines
7.7 KiB
from rest_framework.generics import GenericAPIView
|
|
from rest_framework.response import Response
|
|
import logging
|
|
import re
|
|
from pathlib import Path
|
|
import geoip2.database
|
|
import geoip2.errors
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# GeoLite2 database path
|
|
CITY_DB_PATH = Path("utils/country_city_db/GeoLite2-City.mmdb")
|
|
|
|
|
|
def detect_browser_from_user_agent(user_agent):
|
|
"""
|
|
Detect browser name from User-Agent string.
|
|
|
|
Args:
|
|
user_agent (str): The User-Agent header value
|
|
|
|
Returns:
|
|
str or None: Browser name if detected, None if not a browser or detection fails
|
|
"""
|
|
if not user_agent:
|
|
return None
|
|
|
|
try:
|
|
user_agent = user_agent.lower()
|
|
|
|
# Check for Flutter/Dart app (return None for non-browser requests)
|
|
if any(keyword in user_agent for keyword in ['dart:io', 'flutter', 'dart/']):
|
|
return None
|
|
|
|
# Check for mobile apps that might not be browsers
|
|
if any(keyword in user_agent for keyword in ['habibapp', 'mobile app']):
|
|
return None
|
|
|
|
# Browser detection patterns (order matters - more specific first)
|
|
browser_patterns = [
|
|
(r'edg/', 'Edge'), # Microsoft Edge (Chromium-based)
|
|
(r'edge/', 'Edge'), # Microsoft Edge (Legacy)
|
|
(r'opr/', 'Opera'), # Opera
|
|
(r'opera/', 'Opera'), # Opera
|
|
(r'chrome/', 'Chrome'), # Google Chrome
|
|
(r'chromium/', 'Chromium'), # Chromium
|
|
(r'firefox/', 'Firefox'), # Mozilla Firefox
|
|
(r'fxios/', 'Firefox'), # Firefox for iOS
|
|
(r'safari/', 'Safari'), # Safari (check after Chrome/Edge as they also contain Safari)
|
|
(r'version/.*safari', 'Safari'), # Safari with version
|
|
]
|
|
|
|
# Check each pattern
|
|
for pattern, browser_name in browser_patterns:
|
|
if re.search(pattern, user_agent):
|
|
# Additional check for Safari to avoid false positives
|
|
if browser_name == 'Safari':
|
|
# Make sure it's not Chrome, Edge, or other browsers that include Safari in UA
|
|
if not any(other in user_agent for other in ['chrome', 'edg', 'opr', 'opera']):
|
|
return browser_name
|
|
else:
|
|
return browser_name
|
|
|
|
# If no specific browser detected but contains Mozilla, it might be an unknown browser
|
|
if 'mozilla' in user_agent and any(keyword in user_agent for keyword in ['gecko', 'webkit']):
|
|
return 'Unknown Browser'
|
|
|
|
return None
|
|
|
|
except Exception as e:
|
|
# Log the error but don't let it break the API
|
|
logger.warning(f"Error detecting browser from user agent: {e}")
|
|
return None
|
|
|
|
|
|
class RegionInfoView(GenericAPIView):
|
|
def get(self, request, *args, **kwargs):
|
|
# Get browser information safely
|
|
browser = None
|
|
try:
|
|
user_agent = request.META.get('HTTP_USER_AGENT', '')
|
|
browser = detect_browser_from_user_agent(user_agent)
|
|
except Exception as e:
|
|
# Log the error but continue with the API response
|
|
logger.warning(f"Error detecting browser in RegionInfoView: {e}")
|
|
browser = None
|
|
|
|
# Get IP address
|
|
ip = self.get_client_ip(request)
|
|
|
|
# Get geolocation data from GeoIP2 database
|
|
geo_data = self.get_location_from_ip(ip)
|
|
|
|
region_info = {
|
|
'ip': request.META.get('HTTP_CF_CONNECTING_IP') or ip,
|
|
'country': request.META.get('HTTP_CF_IPCOUNTRY'),
|
|
'region': request.META.get('HTTP_CF_REGION'),
|
|
'region_code': request.META.get('HTTP_CF_REGION_CODE'),
|
|
'city': request.META.get('HTTP_CF_CITY'),
|
|
'timezone': request.META.get('HTTP_CF_TIMEZONE'),
|
|
'browser': browser,
|
|
}
|
|
|
|
# Add geolocation data if available
|
|
if geo_data:
|
|
region_info.update({
|
|
'country_code': geo_data.get('country_code'),
|
|
'latitude': geo_data.get('latitude'),
|
|
'longitude': geo_data.get('longitude'),
|
|
'accuracy_radius': geo_data.get('accuracy_radius'),
|
|
'time_zone': geo_data.get('time_zone'),
|
|
'postal_code': geo_data.get('postal_code'),
|
|
})
|
|
|
|
return Response(region_info)
|
|
|
|
def get_client_ip(self, request):
|
|
"""Extract client IP from request"""
|
|
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
|
if x_forwarded_for:
|
|
ip = x_forwarded_for.split(',')[0].strip()
|
|
else:
|
|
ip = request.META.get('REMOTE_ADDR')
|
|
return ip
|
|
|
|
def get_location_from_ip(self, ip):
|
|
"""Get location data from IP using GeoIP2 database"""
|
|
try:
|
|
# Skip local/private IPs
|
|
if ip in ['127.0.0.1', 'localhost'] or ip.startswith('192.168.') or ip.startswith('10.'):
|
|
logger.warning(f"Skipping local/private IP: {ip}")
|
|
return {
|
|
'ip': ip,
|
|
'city': None,
|
|
'country': None,
|
|
'country_code': None,
|
|
'latitude': None,
|
|
'longitude': None,
|
|
'accuracy_radius': None,
|
|
'time_zone': None,
|
|
'postal_code': None,
|
|
}
|
|
|
|
if not CITY_DB_PATH.exists():
|
|
logger.error(f"GeoIP2 database not found at {CITY_DB_PATH}")
|
|
return None
|
|
|
|
with geoip2.database.Reader(CITY_DB_PATH) as reader:
|
|
response = reader.city(ip)
|
|
|
|
# Extract city name with validation
|
|
city_name = None
|
|
if response.city and response.city.name:
|
|
# Check if city name is actually a subdivision (region)
|
|
# This is a known issue in GeoIP2 where subdivision names appear as city names
|
|
subdivision_names = [s.name for s in response.subdivisions] if response.subdivisions else []
|
|
|
|
if response.city.name not in subdivision_names:
|
|
# City name is valid - not a subdivision
|
|
city_name = response.city.name
|
|
else:
|
|
# City name matches a subdivision - this is a region, not a city
|
|
logger.warning(f"IP {ip}: City name '{response.city.name}' matches subdivision - treating as region")
|
|
city_name = None # Don't return region as city
|
|
|
|
location_data = {
|
|
'ip': ip,
|
|
'city': city_name,
|
|
'country': response.country.name if response.country else None,
|
|
'country_code': response.country.iso_code if response.country else None,
|
|
'latitude': response.location.latitude if response.location else None,
|
|
'longitude': response.location.longitude if response.location else None,
|
|
'accuracy_radius': response.location.accuracy_radius if response.location else None,
|
|
'time_zone': response.location.time_zone if response.location else None,
|
|
'postal_code': response.postal.code if response.postal else None,
|
|
}
|
|
|
|
logger.info(f"Successfully found location for IP {ip}: {location_data.get('city')}, {location_data.get('country')}")
|
|
return location_data
|
|
|
|
except geoip2.errors.AddressNotFoundError:
|
|
logger.warning(f"IP address {ip} not found in GeoIP2 database")
|
|
return None
|
|
except Exception as e:
|
|
logger.error(f"Error getting location from IP {ip}: {str(e)}")
|
|
return None
|