Coverage for sm/views_avatars.py: 75%
36 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-24 19:06 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-24 19:06 +0000
1import logging
2import re
4import requests
5from django.http import HttpResponse, HttpResponseRedirect, HttpResponseBadRequest
6from django.core.cache import cache
8logger = logging.getLogger(__name__)
10# libravatar only serves md5 email hashes (32 lowercase hex chars).
11_HASH_RE = re.compile(r"^[0-9a-f]{32}$")
12_MAX_SIZE = 512
15def _validate_size(raw):
16 try:
17 size = int(raw)
18 except (TypeError, ValueError):
19 size = 80
20 return min(max(size, 1), _MAX_SIZE)
23def avatar_proxy(request, email_hash):
24 """
25 Proxies libravatar images to allow for aggressive browser caching.
27 The ``email_hash`` must be a valid 32-char md5 hex digest and ``s`` is
28 validated to a bounded integer so the endpoint can't be used to probe
29 arbitrary libravatar URLs.
30 """
31 if not _HASH_RE.match(email_hash.lower()):
32 return HttpResponseBadRequest("invalid email hash")
34 size = _validate_size(request.GET.get("s", 80))
35 cache_key = f"avatar_{email_hash}_{size}"
36 cached_avatar = cache.get(cache_key)
38 if cached_avatar:
39 response = HttpResponse(
40 cached_avatar["content"], content_type=cached_avatar["content_type"]
41 )
42 response["Cache-Control"] = "public, max-age=604800, immutable"
43 return response
45 url = f"https://cdn.libravatar.org/avatar/{email_hash}?s={size}&d=mm"
47 try:
48 res = requests.get(url, timeout=5)
49 if res.status_code == 200:
50 content_type = res.headers.get("Content-Type", "image/png")
51 cache.set(
52 cache_key, {"content": res.content, "content_type": content_type}, 86400
53 )
55 response = HttpResponse(res.content, content_type=content_type)
56 response["Cache-Control"] = "public, max-age=604800, immutable"
57 return response
58 except Exception:
59 logger.exception("Avatar proxy: failed to fetch %s", email_hash)
61 return HttpResponseRedirect(url)