redesign
This commit is contained in:
1 parent
ad620c976a
commit
0973129c17
26 files changed
+2255
-2245
No files matched your search
@@ -0,0 +1,174 @@
|
||||
"""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)
|
||||
Reference in new issue
Block a user