#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ IP2Region Patch Override Tool A lightweight Python tool for applying ip2region database patches without modifying the original xdb database files. This creates a JSON cache that acts as an override layer, checking patches before querying the main database. Unlike the maker tools (Java/Golang/C++) which modify xdb files directly, this tool provides a non-destructive patch application method. Based on patch format from: https://github.com/lionsoul2014/ip2region/tree/master/data/fix Usage: python patch_override.py [--patches-dir DIR] [--output FILE] Author: Community Contribution License: Apache 2.0 (same as ip2region project) """ import json import ipaddress import sys import argparse from pathlib import Path from typing import Dict, List, Optional from datetime import datetime # Configure stdout for UTF-8 on Windows if sys.platform == 'win32': try: if hasattr(sys.stdout, 'reconfigure'): sys.stdout.reconfigure(encoding='utf-8', errors='replace') else: import io sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') except: pass def ip_to_int(ip: str) -> int: """Convert IP address to integer.""" try: return int(ipaddress.IPv4Address(ip)) except: return 0 def parse_patch_file(patch_path: Path) -> List[Dict]: """ Parse patch file format: start_ip|end_ip|Country|Province|City|ISP Returns list of patch entries with IP ranges converted to integers. """ patches = [] try: # Try UTF-8 first, fallback to GBK/GB2312 for Windows compatibility encodings = ['utf-8', 'gbk', 'gb2312', 'utf-8-sig'] content = None encoding_used = None # Read as binary first to avoid any encoding issues with open(patch_path, 'rb') as f: raw_bytes = f.read() for enc in encodings: try: content = raw_bytes.decode(enc) encoding_used = enc # Verify Chinese characters decode correctly if '中国' in content[:500] or '北京' in content[:500]: break except UnicodeDecodeError: continue if content is None: print(f"Warning: Could not decode {patch_path} with any encoding") return patches # Process lines for line_num, line in enumerate(content.splitlines(), 1): line = line.strip() if not line or line.startswith('#'): continue # Format: start_ip|end_ip|Country|Province|City|ISP parts = line.split('|') if len(parts) >= 6: start_ip = parts[0].strip() end_ip = parts[1].strip() country = parts[2].strip() province = parts[3].strip() city = parts[4].strip() isp = parts[5].strip() # Convert IPs to integers for range checking try: start_int = ip_to_int(start_ip) end_int = ip_to_int(end_ip) if start_int > 0 and end_int > 0: patches.append({ 'start_ip': start_ip, 'end_ip': end_ip, 'start_int': start_int, 'end_int': end_int, 'country': country, 'province': province, 'city': city, 'isp': isp, 'source': patch_path.name, 'line': line_num }) except Exception as e: print(f"Warning: Invalid IP range in {patch_path.name} line {line_num}: {e}") continue if encoding_used and encoding_used != 'utf-8': print(f" Note: File read with {encoding_used} encoding") except Exception as e: print(f"Error parsing {patch_path}: {e}") import traceback traceback.print_exc() return patches def build_patch_cache(patches_dir: Path, output_file: Path) -> Dict: """ Build a cache of all patches for fast lookup. Args: patches_dir: Directory containing .fix patch files output_file: Path to save the JSON cache file Returns: Dictionary with patch cache data """ if not patches_dir.exists(): print(f"Patch directory not found: {patches_dir}") return {} # Find all patch files patch_files = list(patches_dir.glob("*.fix")) if not patch_files: print(f"No patch files found in {patches_dir}") return {} print(f"Found {len(patch_files)} patch file(s):") for pf in patch_files: print(f" - {pf.name}") # Parse all patches all_patches = [] for patch_file in patch_files: patches = parse_patch_file(patch_file) print(f" Parsed {len(patches)} entries from {patch_file.name}") all_patches.extend(patches) print(f"\nTotal patches: {len(all_patches)}") if not all_patches: return {} # Build cache structure: list of patches sorted by start_int for binary search patches_sorted = sorted(all_patches, key=lambda x: x['start_int']) # Save to cache file with proper UTF-8 encoding cache_data = { 'patches': patches_sorted, 'total_patches': len(patches_sorted), 'last_updated': datetime.now().isoformat(), 'patch_files': [pf.name for pf in patch_files] } # Ensure output directory exists output_file.parent.mkdir(parents=True, exist_ok=True) # Save with UTF-8 encoding with open(output_file, 'w', encoding='utf-8', newline='\n') as f: json.dump(cache_data, f, ensure_ascii=False, indent=2) print(f"\nPatch cache saved to: {output_file}") print(f"Total patches cached: {len(patches_sorted)}") return cache_data def load_patch_cache(cache_file: Path) -> Dict: """Load patch cache from file.""" if not cache_file.exists(): return {} try: # Try UTF-8 first, with fallback encodings encodings = ['utf-8', 'utf-8-sig', 'gbk', 'gb2312'] cache_data = None for enc in encodings: try: with open(cache_file, 'r', encoding=enc) as f: cache_data = json.load(f) break except UnicodeDecodeError: continue if cache_data is None: print(f"Warning: Could not decode cache file with any encoding") return {} return cache_data except Exception as e: print(f"Error loading patch cache: {e}") return {} def find_patch_for_ip(ip: str, cache: Dict) -> Optional[Dict]: """ Find patch entry for a given IP address using binary search. Args: ip: IP address string cache: Patch cache dictionary Returns: Patch data dict if IP falls within any patch range, None otherwise """ patches = cache.get('patches', []) if not patches: return None try: ip_int = ip_to_int(ip) if ip_int == 0: return None # Binary search for matching range left, right = 0, len(patches) - 1 while left <= right: mid = (left + right) // 2 patch = patches[mid] start_int = patch.get('start_int', 0) end_int = patch.get('end_int', 0) if start_int <= ip_int <= end_int: # Found matching range - return location data return { 'province': patch.get('province', ''), 'city': patch.get('city', ''), 'country': patch.get('country', '中国'), 'isp': patch.get('isp', '') } elif ip_int < start_int: right = mid - 1 else: left = mid + 1 return None except Exception as e: print(f"Error finding patch for IP {ip}: {e}") import traceback traceback.print_exc() return None def main(): """Main function to build and test patch cache.""" parser = argparse.ArgumentParser( description='IP2Region Patch Override Tool - Create JSON cache from patch files', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: # Use default paths (patches/ and patches_cache.json) python patch_override.py # Specify custom directories python patch_override.py --patches-dir ./data/fix --output ./cache.json # Test with specific IPs python patch_override.py --test-ip 39.144.0.1 --test-ip 39.144.10.5 """ ) parser.add_argument( '--patches-dir', type=str, default='patches', help='Directory containing .fix patch files (default: patches)' ) parser.add_argument( '--output', type=str, default='patches_cache.json', help='Output JSON cache file path (default: patches_cache.json)' ) parser.add_argument( '--test-ip', action='append', dest='test_ips', help='Test IP addresses to verify patch lookup (can be specified multiple times)' ) args = parser.parse_args() patches_dir = Path(args.patches_dir) output_file = Path(args.output) print("=" * 60) print("IP2Region Patch Override Tool") print("=" * 60) # Build patch cache print(f"\n[1/2] Building patch cache...") print(f" Patches directory: {patches_dir}") print(f" Output file: {output_file}") cache = build_patch_cache(patches_dir, output_file) if not cache: print("\nNo patches found. Exiting.") return # Test patch lookup if IPs provided if args.test_ips: print(f"\n[2/2] Testing patch lookup...") for test_ip in args.test_ips: patch = find_patch_for_ip(test_ip, cache) if patch: province = patch.get('province', '') city = patch.get('city', '') print(f" {test_ip} -> {province}, {city} (from patch)") else: print(f" {test_ip} -> No patch found (will use main database)") else: # Default test IPs print(f"\n[2/2] Testing patch lookup...") test_ips = ['39.144.0.1', '39.144.10.5', '39.144.177.100'] for test_ip in test_ips: patch = find_patch_for_ip(test_ip, cache) if patch: province = patch.get('province', '') city = patch.get('city', '') print(f" {test_ip} -> {province}, {city} (from patch)") else: print(f" {test_ip} -> No patch found (will use main database)") print("\n" + "=" * 60) print("Patch cache creation complete!") print("\nNext steps:") print("1. Use the cache file in your IP geolocation service") print("2. Check patches before querying the main xdb database") print("3. Patches take priority over database results") print("=" * 60) if __name__ == "__main__": main()