first commit
This commit is contained in:
242
app.py
Normal file
242
app.py
Normal file
@@ -0,0 +1,242 @@
|
||||
"""
|
||||
GeoIP Ban Generator - Web Application
|
||||
"""
|
||||
|
||||
from flask import Flask, render_template, request, Response, jsonify
|
||||
import hashlib
|
||||
import os
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
from functools import wraps
|
||||
from datetime import datetime
|
||||
|
||||
import config
|
||||
from api import api
|
||||
from geoip_handler import GeoIPHandler
|
||||
|
||||
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()
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
@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('/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"""
|
||||
|
||||
if request.path == '/' or request.path.startswith('/api/'):
|
||||
response.headers['Cache-Control'] = 'no-cache, no-store'
|
||||
|
||||
elif request.path.startswith('/static/'):
|
||||
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}'
|
||||
|
||||
elif request.path == '/api-docs':
|
||||
response.headers['Cache-Control'] = 'public, max-age=300'
|
||||
|
||||
return response
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(
|
||||
host=config.FLASK_HOST,
|
||||
port=config.FLASK_PORT,
|
||||
debug=config.FLASK_DEBUG
|
||||
)
|
||||
Reference in New Issue
Block a user