Files
adlist_mikrotik/adlist/services.py
T
2026-10-09 10:53:10 +02:00

175 lines
5.3 KiB
Python

"""Redis-backed counters, recent links and request lifecycle hooks."""
import redis
import socket
import time
import json
from datetime import datetime
from functools import wraps
from urllib.parse import quote
from flask import request, Response, g
import config
def build_redis():
if config.REDIS_URL:
return redis.Redis.from_url(config.REDIS_URL)
return redis.Redis(
host=config.REDIS_HOST, port=config.REDIS_PORT, db=config.REDIS_DB
)
redis_client = build_redis()
def get_client_ip():
xff = request.headers.get("X-Forwarded-For", "").split(",")
if xff and xff[0].strip():
return xff[0].strip()
return request.remote_addr
def track_request_data():
g.start_time = time.perf_counter()
redis_client.incr(
f"stats:user_agents:{quote(request.headers.get('User-Agent', 'Unknown'), safe='')}"
)
redis_client.incr(f"stats:client_ips:{get_client_ip()}")
redis_client.incr(f"stats:methods:{request.method}")
def finalize_response(response):
elapsed = time.perf_counter() - getattr(g, "start_time", time.perf_counter())
redis_client.incrbyfloat("stats:processing_time_total", elapsed)
redis_client.incr("stats:processing_time_count")
try:
current_min = float(redis_client.get("stats:processing_time_min") or elapsed)
if elapsed < current_min:
redis_client.set("stats:processing_time_min", elapsed)
except Exception:
redis_client.set("stats:processing_time_min", elapsed)
try:
current_max = float(redis_client.get("stats:processing_time_max") or elapsed)
if elapsed > current_max:
redis_client.set("stats:processing_time_max", elapsed)
except Exception:
redis_client.set("stats:processing_time_max", elapsed)
path = request.path or "/"
if response.status_code >= 400:
response.headers["Cache-Control"] = "no-store"
return response
if path.startswith("/static/"):
response.headers.pop("Content-Disposition", None)
# Only versioned assets are immutable. Unversioned files must not stay stale after upgrades.
if request.args.get("v"):
response.headers["Cache-Control"] = "public, max-age=31536000, immutable"
else:
response.headers["Cache-Control"] = "public, max-age=3600"
return response
if path in {"/", "/stats", "/stats.json"}:
# Only validated /convert outputs are public cache objects.
response.headers["Cache-Control"] = "private, no-store"
return response
return response
def datetimeformat_filter(value, format="%Y-%m-%d %H:%M"):
try:
dt = datetime.fromisoformat(value)
return dt.strftime(format)
except (ValueError, AttributeError):
return value
def basic_auth_required(realm: str, user: str, password: str):
def decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
if not config.STATS_BASIC_AUTH_ENABLED:
return f(*args, **kwargs)
auth = request.authorization
if (
auth
and auth.type == "basic"
and auth.username == user
and auth.password == password
):
return f(*args, **kwargs)
resp = Response(status=401)
resp.headers["WWW-Authenticate"] = f'Basic realm="{realm}"'
return resp
return wrapper
return decorator
def track_url_request(url):
redis_client.incr(f"stats:url_requests:{quote(url, safe='')}")
def add_recent_link(url, target_ip):
ts = datetime.now().isoformat()
new_item = f"{ts}|{url}|{target_ip}"
key = "recent_links"
current = redis_client.lrange(key, 0, -1)
filtered = []
for raw in current:
try:
s = raw.decode()
parts = s.split("|")
if len(parts) >= 3 and parts[1] == url and parts[2] == target_ip:
continue
except Exception:
pass
filtered.append(raw)
with redis_client.pipeline() as pipe:
pipe.delete(key)
pipe.lpush(key, new_item)
if filtered:
pipe.rpush(key, *filtered[:99])
pipe.ltrim(key, 0, 99)
pipe.execute()
redis_client.incr("stats:recent_links_added")
def get_recent_links():
links = redis_client.lrange("recent_links", 0, 99)
out = []
for link in links:
parts = link.decode().split("|")
if len(parts) >= 3:
out.append((parts[0], parts[1], parts[2]))
elif len(parts) == 2:
out.append((parts[0], parts[1], "127.0.0.1"))
return out
def get_hostname(ip):
key = f"reverse_dns:{ip}"
cached = redis_client.get(key)
if cached:
return cached.decode()
try:
hostname = socket.gethostbyaddr(ip)[0]
except Exception:
hostname = ip
redis_client.setex(key, 3600, hostname)
return hostname
def add_recent_convert():
ip = get_client_ip()
hostname = get_hostname(ip)
ua = request.headers.get("User-Agent", "Unknown")
time_str = datetime.now().astimezone().isoformat()
url = request.full_path
data = {
"url": url,
"ip": ip,
"hostname": hostname,
"time": time_str,
"user_agent": ua,
}
redis_client.lpush("recent_converts", json.dumps(data))
redis_client.ltrim("recent_converts", 0, 99)