""" GeoIP Ban Generator - Web Application """ from flask import Flask, render_template, request, Response, jsonify from geoip_handler import GeoIPHandler from pathlib import Path from functools import wraps from datetime import datetime from api import api import hashlib import os import sqlite3 import config app = Flask(__name__, static_folder=str(config.STATIC_DIR), template_folder=str(config.TEMPLATE_DIR)) app.config['SECRET_KEY'] = config.SECRET_KEY app.register_blueprint(api) handler = GeoIPHandler() CACHEABLE_PAGES = { "/api-docs", "/generator", } NO_CACHE_PREFIXES = ( "/api/", ) STATIC_PREFIX = "/static/" redis_cache = None if config.REDIS_ENABLED: try: from redis_cache import RedisCache redis_cache = RedisCache() redis_health = redis_cache.health_check() if redis_health['connected']: print(f"[REDIS] Connected successfully - {redis_health['memory_used_mb']}MB used", flush=True) else: print(f"[REDIS] Connection failed: {redis_health.get('error')}", flush=True) except Exception as e: print(f"[REDIS] Failed to initialize: {e}", flush=True) redis_cache = None def get_file_hash(filepath: Path) -> str: """Generate MD5 hash for cache busting""" try: if filepath.exists(): with open(filepath, 'rb') as f: return hashlib.md5(f.read()).hexdigest()[:8] except: pass return 'v1' def get_sqlite_status(): """Get SQLite cache database status""" db_path = config.GEOIP_DB_DIR / 'networks_cache.db' if not db_path.exists(): return { 'exists': False, 'status': 'missing' } try: file_size = db_path.stat().st_size modified_time = datetime.fromtimestamp(db_path.stat().st_mtime) conn = sqlite3.connect(str(db_path), timeout=5.0) cursor = conn.cursor() cursor.execute("SELECT COUNT(*) FROM cache_metadata") total_countries = cursor.fetchone()[0] cursor.execute("SELECT SUM(network_count) FROM cache_metadata") total_networks = cursor.fetchone()[0] or 0 conn.close() return { 'exists': True, 'status': 'ok', 'file_size_mb': round(file_size / 1024 / 1024, 2), 'total_countries': total_countries, 'total_networks': total_networks, 'modified': modified_time.isoformat() } except Exception as e: return { 'exists': True, 'status': 'error', 'error': str(e) } @app.context_processor def inject_globals(): """Inject global variables into templates""" return { 'app_name': config.APP_NAME, 'app_version': config.APP_VERSION, 'logo_url': config.LOGO_URL, 'logo_link': config.LOGO_LINK, 'footer_text': config.FOOTER_TEXT, 'footer_link': config.FOOTER_LINK, 'footer_link_text': config.FOOTER_LINK_TEXT, 'css_hash': get_file_hash(config.STATIC_DIR / 'css' / 'style.css'), 'js_hash': get_file_hash(config.STATIC_DIR / 'js' / 'app.js'), 'get_country_flag': config.get_country_flag, 'redis_enabled': config.REDIS_ENABLED, 'redis_connected': redis_cache.health_check()['connected'] if redis_cache else False, } from flask import jsonify, render_template, request def _wants_json(): if request.path.startswith("/api/"): return True accept = (request.headers.get("Accept") or "").lower() return "application/json" in accept def _render_4xx(code, title, message): payload = { "success": False, "error": title, "message": message, "path": request.path, "status": code, } if _wants_json(): return jsonify(payload), code return render_template( "error.html", status=code, title=title, message=message, path=request.path ), code @app.errorhandler(400) def bad_request(e): return _render_4xx(400, "Bad Request", "Request is invalid or missing required fields.") @app.errorhandler(401) def unauthorized(e): return _render_4xx(401, "Unauthorized", "Authentication is required for this resource.") @app.errorhandler(403) def forbidden(e): return _render_4xx(403, "Forbidden", "You don't have permission to access this resource.") @app.errorhandler(404) def not_found(e): return _render_4xx(404, "Not Found", "The requested endpoint/page does not exist.") @app.errorhandler(405) def method_not_allowed(e): return _render_4xx(405, "Method Not Allowed", "The HTTP method is not allowed for this endpoint.") @app.route('/') def index(): """Main page""" if config.MAXMIND_AUTO_UPDATE: handler.check_and_update() return render_template('index.html', countries=config.COMMON_COUNTRIES) @app.route('/api-docs') def api_docs(): """API documentation page""" return render_template('api.html') @app.route("/generator") def generator(): """Script gwnerator""" return render_template("generator.html") @app.route('/favicon.ico') def favicon(): return '', 204 @app.route('/health') def health(): """Health check endpoint for HAProxy/monitoring""" health_status = { 'status': 'healthy', 'timestamp': datetime.now().isoformat(), 'components': {} } health_status['components']['flask'] = { 'status': 'ok', 'version': config.APP_VERSION } maxmind_exists = handler.mmdb_file.exists() health_status['components']['maxmind_db'] = { 'status': 'ok' if maxmind_exists else 'missing', 'exists': maxmind_exists } if not maxmind_exists: health_status['status'] = 'degraded' sqlite_status = get_sqlite_status() health_status['components']['sqlite_cache'] = sqlite_status if not sqlite_status['exists'] or sqlite_status.get('status') != 'ok': health_status['status'] = 'degraded' if config.REDIS_ENABLED and redis_cache: redis_health = redis_cache.health_check() if redis_health.get('connected'): health_status['components']['redis'] = { 'status': 'ok', 'memory_mb': redis_health.get('memory_used_mb', 0), 'keys': redis_health.get('keys', 0) } else: health_status['components']['redis'] = { 'status': 'error', 'error': redis_health.get('error', 'Connection failed') } health_status['status'] = 'degraded' else: health_status['components']['redis'] = { 'status': 'disabled', 'enabled': False } status_code = 200 if health_status['status'] in ['healthy', 'degraded'] else 503 return jsonify(health_status), status_code @app.route('/api/stats/summary') def stats_summary(): """Combined stats endpoint for dashboard""" stats = { 'maxmind': { 'exists': handler.mmdb_file.exists(), 'needs_update': handler.needs_update() }, 'sqlite': get_sqlite_status(), 'redis': {'enabled': False} } if config.REDIS_ENABLED and redis_cache: redis_health = redis_cache.health_check() stats['redis'] = { 'enabled': True, 'connected': redis_health.get('connected', False), 'memory_mb': redis_health.get('memory_used_mb', 0), 'keys': redis_health.get('keys', 0) } return jsonify(stats) def cache_control(max_age: int = None): """Decorator for static file cache control""" def decorator(f): @wraps(f) def decorated_function(*args, **kwargs): response = f(*args, **kwargs) if isinstance(response, Response): if max_age: response.headers['Cache-Control'] = f'public, max-age={max_age}' else: response.headers['Cache-Control'] = 'no-cache, no-store' response.headers['Pragma'] = 'no-cache' response.headers['Expires'] = '0' return response return decorated_function return decorator @app.after_request def add_headers(response): """Add cache control headers based on request path""" path = request.path if path.startswith(STATIC_PREFIX): if "Content-Disposition" in response.headers: del response.headers["Content-Disposition"] if config.ENABLE_CACHE_BUSTING: response.headers["Cache-Control"] = ( f"public, max-age={config.CACHE_TTL_SECONDS}, immutable" ) else: response.headers["Cache-Control"] = f"public, max-age={config.CACHE_TTL_SECONDS}" return response if path in CACHEABLE_PAGES: response.headers["Cache-Control"] = "public, max-age=300" return response if path == "/" or any(path.startswith(p) for p in NO_CACHE_PREFIXES): response.headers["Cache-Control"] = "no-cache, no-store" return response return response if __name__ == '__main__': app.run( host=config.FLASK_HOST, port=config.FLASK_PORT, debug=config.FLASK_DEBUG )