Implement LDAP avatar serving
This adds a /avatar/<uid> endpoint which serves any jpegPhoto associated with a given user account. In true 'lol ldap' fashion, only `photo` and `jpegPhoto` fields are defined. The first one is for G3 photos (a fax format!). The latter is technically for JPEG. But we expect to abuse this and basically contain _any_ sensible photo format in there, as long as Python's PIL can parse it. The serving function always resamples images to a 256x256 PNG. This makes sure people don't leak EXIF and lets us depend on square avatars. This entire code assumes that it is safe to PIL.Image.open untrusted user data. My understanding is that it is, bar some DoS for very large images. We limit the potential for DoS by storing the images in LDAP, which I hope has some kind of field length limit... Oh, and this also adds a 'default avatar' functionality which serves simple generative mermaid art for any user who doesn't have an explicit avatar set. To prevent leaking the existence of users who don't have an avatar set, we serve such a generated avatar for all UIDs, including UIDs which don't exist.
This commit is contained in:
parent
3752e0c558
commit
a435e15698
5 changed files with 286 additions and 3 deletions
69
poetry.lock
generated
69
poetry.lock
generated
|
@ -187,6 +187,73 @@ files = [
|
|||
{file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "10.0.1"
|
||||
description = "Python Imaging Library (Fork)"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "Pillow-10.0.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:8f06be50669087250f319b706decf69ca71fdecd829091a37cc89398ca4dc17a"},
|
||||
{file = "Pillow-10.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:50bd5f1ebafe9362ad622072a1d2f5850ecfa44303531ff14353a4059113b12d"},
|
||||
{file = "Pillow-10.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6a90167bcca1216606223a05e2cf991bb25b14695c518bc65639463d7db722d"},
|
||||
{file = "Pillow-10.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f11c9102c56ffb9ca87134bd025a43d2aba3f1155f508eff88f694b33a9c6d19"},
|
||||
{file = "Pillow-10.0.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:186f7e04248103482ea6354af6d5bcedb62941ee08f7f788a1c7707bc720c66f"},
|
||||
{file = "Pillow-10.0.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0462b1496505a3462d0f35dc1c4d7b54069747d65d00ef48e736acda2c8cbdff"},
|
||||
{file = "Pillow-10.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d889b53ae2f030f756e61a7bff13684dcd77e9af8b10c6048fb2c559d6ed6eaf"},
|
||||
{file = "Pillow-10.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:552912dbca585b74d75279a7570dd29fa43b6d93594abb494ebb31ac19ace6bd"},
|
||||
{file = "Pillow-10.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:787bb0169d2385a798888e1122c980c6eff26bf941a8ea79747d35d8f9210ca0"},
|
||||
{file = "Pillow-10.0.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:fd2a5403a75b54661182b75ec6132437a181209b901446ee5724b589af8edef1"},
|
||||
{file = "Pillow-10.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2d7e91b4379f7a76b31c2dda84ab9e20c6220488e50f7822e59dac36b0cd92b1"},
|
||||
{file = "Pillow-10.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19e9adb3f22d4c416e7cd79b01375b17159d6990003633ff1d8377e21b7f1b21"},
|
||||
{file = "Pillow-10.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93139acd8109edcdeffd85e3af8ae7d88b258b3a1e13a038f542b79b6d255c54"},
|
||||
{file = "Pillow-10.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:92a23b0431941a33242b1f0ce6c88a952e09feeea9af4e8be48236a68ffe2205"},
|
||||
{file = "Pillow-10.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:cbe68deb8580462ca0d9eb56a81912f59eb4542e1ef8f987405e35a0179f4ea2"},
|
||||
{file = "Pillow-10.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:522ff4ac3aaf839242c6f4e5b406634bfea002469656ae8358644fc6c4856a3b"},
|
||||
{file = "Pillow-10.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:84efb46e8d881bb06b35d1d541aa87f574b58e87f781cbba8d200daa835b42e1"},
|
||||
{file = "Pillow-10.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:898f1d306298ff40dc1b9ca24824f0488f6f039bc0e25cfb549d3195ffa17088"},
|
||||
{file = "Pillow-10.0.1-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:bcf1207e2f2385a576832af02702de104be71301c2696d0012b1b93fe34aaa5b"},
|
||||
{file = "Pillow-10.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5d6c9049c6274c1bb565021367431ad04481ebb54872edecfcd6088d27edd6ed"},
|
||||
{file = "Pillow-10.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28444cb6ad49726127d6b340217f0627abc8732f1194fd5352dec5e6a0105635"},
|
||||
{file = "Pillow-10.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de596695a75496deb3b499c8c4f8e60376e0516e1a774e7bc046f0f48cd620ad"},
|
||||
{file = "Pillow-10.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:2872f2d7846cf39b3dbff64bc1104cc48c76145854256451d33c5faa55c04d1a"},
|
||||
{file = "Pillow-10.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4ce90f8a24e1c15465048959f1e94309dfef93af272633e8f37361b824532e91"},
|
||||
{file = "Pillow-10.0.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ee7810cf7c83fa227ba9125de6084e5e8b08c59038a7b2c9045ef4dde61663b4"},
|
||||
{file = "Pillow-10.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b1be1c872b9b5fcc229adeadbeb51422a9633abd847c0ff87dc4ef9bb184ae08"},
|
||||
{file = "Pillow-10.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:98533fd7fa764e5f85eebe56c8e4094db912ccbe6fbf3a58778d543cadd0db08"},
|
||||
{file = "Pillow-10.0.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:764d2c0daf9c4d40ad12fbc0abd5da3af7f8aa11daf87e4fa1b834000f4b6b0a"},
|
||||
{file = "Pillow-10.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fcb59711009b0168d6ee0bd8fb5eb259c4ab1717b2f538bbf36bacf207ef7a68"},
|
||||
{file = "Pillow-10.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:697a06bdcedd473b35e50a7e7506b1d8ceb832dc238a336bd6f4f5aa91a4b500"},
|
||||
{file = "Pillow-10.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f665d1e6474af9f9da5e86c2a3a2d2d6204e04d5af9c06b9d42afa6ebde3f21"},
|
||||
{file = "Pillow-10.0.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:2fa6dd2661838c66f1a5473f3b49ab610c98a128fc08afbe81b91a1f0bf8c51d"},
|
||||
{file = "Pillow-10.0.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:3a04359f308ebee571a3127fdb1bd01f88ba6f6fb6d087f8dd2e0d9bff43f2a7"},
|
||||
{file = "Pillow-10.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:723bd25051454cea9990203405fa6b74e043ea76d4968166dfd2569b0210886a"},
|
||||
{file = "Pillow-10.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:71671503e3015da1b50bd18951e2f9daf5b6ffe36d16f1eb2c45711a301521a7"},
|
||||
{file = "Pillow-10.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:44e7e4587392953e5e251190a964675f61e4dae88d1e6edbe9f36d6243547ff3"},
|
||||
{file = "Pillow-10.0.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:3855447d98cced8670aaa63683808df905e956f00348732448b5a6df67ee5849"},
|
||||
{file = "Pillow-10.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ed2d9c0704f2dc4fa980b99d565c0c9a543fe5101c25b3d60488b8ba80f0cce1"},
|
||||
{file = "Pillow-10.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5bb289bb835f9fe1a1e9300d011eef4d69661bb9b34d5e196e5e82c4cb09b37"},
|
||||
{file = "Pillow-10.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a0d3e54ab1df9df51b914b2233cf779a5a10dfd1ce339d0421748232cea9876"},
|
||||
{file = "Pillow-10.0.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:2cc6b86ece42a11f16f55fe8903595eff2b25e0358dec635d0a701ac9586588f"},
|
||||
{file = "Pillow-10.0.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:ca26ba5767888c84bf5a0c1a32f069e8204ce8c21d00a49c90dabeba00ce0145"},
|
||||
{file = "Pillow-10.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f0b4b06da13275bc02adfeb82643c4a6385bd08d26f03068c2796f60d125f6f2"},
|
||||
{file = "Pillow-10.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bc2e3069569ea9dbe88d6b8ea38f439a6aad8f6e7a6283a38edf61ddefb3a9bf"},
|
||||
{file = "Pillow-10.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:8b451d6ead6e3500b6ce5c7916a43d8d8d25ad74b9102a629baccc0808c54971"},
|
||||
{file = "Pillow-10.0.1-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:32bec7423cdf25c9038fef614a853c9d25c07590e1a870ed471f47fb80b244db"},
|
||||
{file = "Pillow-10.0.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7cf63d2c6928b51d35dfdbda6f2c1fddbe51a6bc4a9d4ee6ea0e11670dd981e"},
|
||||
{file = "Pillow-10.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f6d3d4c905e26354e8f9d82548475c46d8e0889538cb0657aa9c6f0872a37aa4"},
|
||||
{file = "Pillow-10.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:847e8d1017c741c735d3cd1883fa7b03ded4f825a6e5fcb9378fd813edee995f"},
|
||||
{file = "Pillow-10.0.1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:7f771e7219ff04b79e231d099c0a28ed83aa82af91fd5fa9fdb28f5b8d5addaf"},
|
||||
{file = "Pillow-10.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:459307cacdd4138edee3875bbe22a2492519e060660eaf378ba3b405d1c66317"},
|
||||
{file = "Pillow-10.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b059ac2c4c7a97daafa7dc850b43b2d3667def858a4f112d1aa082e5c3d6cf7d"},
|
||||
{file = "Pillow-10.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6caf3cd38449ec3cd8a68b375e0c6fe4b6fd04edb6c9766b55ef84a6e8ddf2d"},
|
||||
{file = "Pillow-10.0.1.tar.gz", hash = "sha256:d72967b06be9300fed5cfbc8b5bafceec48bf7cdc7dab66b1d2549035287191d"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"]
|
||||
tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
|
||||
|
||||
[[package]]
|
||||
name = "pyasn1"
|
||||
version = "0.5.0"
|
||||
|
@ -273,4 +340,4 @@ email = ["email-validator"]
|
|||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "~3.11"
|
||||
content-hash = "b026e5d2d31ebde4256e7793a62fbba895b049cfc270bf24ab1a0ed352f9d933"
|
||||
content-hash = "5fe83018dde9d22d0820c713b674849afb60d077ad33429fcfa330cafcba4ef0"
|
||||
|
|
|
@ -13,6 +13,7 @@ itsdangerous = "^2.0"
|
|||
Jinja2 = "^3.0"
|
||||
kerberos = "^1.3.0"
|
||||
MarkupSafe = "^2.0"
|
||||
pillow = "^10.0.1"
|
||||
python-ldap = "^3.2.0"
|
||||
uWSGI = "^2.0"
|
||||
Werkzeug = "^2.0"
|
||||
|
|
BIN
syrenka.png
Normal file
BIN
syrenka.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 48 KiB |
|
@ -45,8 +45,8 @@ def start():
|
|||
validation.sanitize_readable()
|
||||
|
||||
from webapp import views
|
||||
from webapp import auth, admin, vcard, passwd
|
||||
for module in (auth, admin, vcard, passwd):
|
||||
from webapp import auth, admin, avatar, vcard, passwd
|
||||
for module in (auth, admin, avatar, vcard, passwd):
|
||||
app.register_blueprint(module.bp)
|
||||
|
||||
app.connections = pools.LDAPConnectionPool(config.ldap_url, timeout=300.0)
|
||||
|
|
215
webapp/avatar.py
Normal file
215
webapp/avatar.py
Normal file
|
@ -0,0 +1,215 @@
|
|||
"""
|
||||
Module which serves users' avatars.
|
||||
|
||||
Based on a simple cache which keeps all avatars in memory.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
import colorsys
|
||||
import time
|
||||
import io
|
||||
import logging
|
||||
import random
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
import flask
|
||||
import ldap
|
||||
|
||||
from webapp import context, ldaputils
|
||||
|
||||
bp = flask.Blueprint('avatar', __name__)
|
||||
log = logging.getLogger('ldap-web.avatar')
|
||||
|
||||
|
||||
# Stolen from https://stackoverflow.com/questions/43512615/reshaping-rectangular-image-to-square
|
||||
def resize_image(image: Image, length: int) -> Image:
|
||||
"""
|
||||
Resize an image to a square. Can make an image bigger to make it fit or smaller if it doesn't fit. It also crops
|
||||
part of the image.
|
||||
|
||||
:param self:
|
||||
:param image: Image to resize.
|
||||
:param length: Width and height of the output image.
|
||||
:return: Return the resized image.
|
||||
"""
|
||||
|
||||
"""
|
||||
Resizing strategy :
|
||||
1) We resize the smallest side to the desired dimension (e.g. 1080)
|
||||
2) We crop the other side so as to make it fit with the same length as the smallest side (e.g. 1080)
|
||||
"""
|
||||
if image.size[0] < image.size[1]:
|
||||
# The image is in portrait mode. Height is bigger than width.
|
||||
|
||||
# This makes the width fit the LENGTH in pixels while conserving the ration.
|
||||
resized_image = image.resize((length, int(image.size[1] * (length / image.size[0]))))
|
||||
|
||||
# Amount of pixel to lose in total on the height of the image.
|
||||
required_loss = (resized_image.size[1] - length)
|
||||
|
||||
# Crop the height of the image so as to keep the center part.
|
||||
resized_image = resized_image.crop(
|
||||
box=(0, required_loss / 2, length, resized_image.size[1] - required_loss / 2))
|
||||
|
||||
# We now have a length*length pixels image.
|
||||
return resized_image
|
||||
else:
|
||||
# This image is in landscape mode or already squared. The width is bigger than the heihgt.
|
||||
|
||||
# This makes the height fit the LENGTH in pixels while conserving the ration.
|
||||
resized_image = image.resize((int(image.size[0] * (length / image.size[1])), length))
|
||||
|
||||
# Amount of pixel to lose in total on the width of the image.
|
||||
required_loss = resized_image.size[0] - length
|
||||
|
||||
# Crop the width of the image so as to keep 1080 pixels of the center part.
|
||||
resized_image = resized_image.crop(
|
||||
box=(required_loss / 2, 0, resized_image.size[0] - required_loss / 2, length))
|
||||
|
||||
# We now have a length*length pixels image.
|
||||
return resized_image
|
||||
|
||||
|
||||
syrenka = Image.open("syrenka.png")
|
||||
|
||||
|
||||
def default_avatar(uid: str) -> Image:
|
||||
"""
|
||||
Create little generative avatar for people who don't have a custom one
|
||||
configured.
|
||||
"""
|
||||
img = Image.new('RGBA', (256, 256), (255, 255, 255, 0))
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
# Deterministic rng for stable output.
|
||||
rng = random.Random(uid)
|
||||
|
||||
# Pick a nice random neon color.
|
||||
n_h, n_s, n_l = rng.random(), 0.5 + rng.random()/2.0, 0.4 + rng.random()/5.0
|
||||
|
||||
# Use muted version for background.
|
||||
r, g, b = [int(256*i) for i in colorsys.hls_to_rgb(n_h, n_l+0.3, n_s-0.1)]
|
||||
draw.rectangle([(0, 0), (256, 256)], fill=(r,g,b,255))
|
||||
|
||||
# Scale logo by randomized factor.
|
||||
factor = 0.7 + 0.1 * rng.random()
|
||||
w, h = int(syrenka.size[0] * factor), int(syrenka.size[1] * factor)
|
||||
overlay = syrenka.resize((w, h))
|
||||
|
||||
# Crop to headshot.
|
||||
overlay = overlay.crop(box=(0, 0, w, w))
|
||||
|
||||
# Give it a little nudge.
|
||||
overlay = overlay.rotate((rng.random() - 0.5) * 100)
|
||||
|
||||
# Colorize with full neon color.
|
||||
r, g, b = [int(256*i) for i in colorsys.hls_to_rgb(n_h,n_l,n_s)]
|
||||
pixels = overlay.load()
|
||||
for x in range(img.size[0]):
|
||||
for y in range(img.size[1]):
|
||||
alpha = pixels[x, y][3]
|
||||
pixels[x, y] = (r, g, b, alpha)
|
||||
|
||||
img.alpha_composite(overlay)
|
||||
|
||||
res = io.BytesIO()
|
||||
img.save(res, 'PNG')
|
||||
return res.getvalue()
|
||||
|
||||
|
||||
class AvatarCacheEntry:
|
||||
# UID of the avatar's user.
|
||||
uid: str
|
||||
# Deadline when this entry expires.
|
||||
deadline: float
|
||||
# Cached source bytes
|
||||
data: bytes
|
||||
# Cached converted bytes
|
||||
_converted: bytes
|
||||
|
||||
def __init__(self, uid: str, data: bytes):
|
||||
self.uid = uid
|
||||
self.deadline = time.time() + 3600
|
||||
self.data = data
|
||||
self._converted = b""
|
||||
|
||||
def serve(self):
|
||||
"""
|
||||
Serve sanitized image. Always re-encode to PNG 256x256.
|
||||
"""
|
||||
# Re-encode to PNG if we haven't yet.
|
||||
if self._converted == b"":
|
||||
try:
|
||||
img = Image.open(io.BytesIO(self.data))
|
||||
except Exception as e:
|
||||
log.warning("Could not parse avatar for {}: {}".format(self.uid, e))
|
||||
self.data = default_avatar(self.uid)
|
||||
img = Image.open(io.BytesIO(self.data))
|
||||
|
||||
res = io.BytesIO()
|
||||
img = resize_image(img, 256)
|
||||
img.save(res, 'PNG')
|
||||
self._converted = res.getvalue()
|
||||
|
||||
return flask.Response(self._converted, mimetype='image/png')
|
||||
|
||||
|
||||
class AvatarCache:
|
||||
# keyed by uid
|
||||
entries: dict[str, AvatarCacheEntry]
|
||||
|
||||
def __init__(self):
|
||||
self.entries = {}
|
||||
|
||||
def get(self, uid: str, bust: bool = False) -> AvatarCacheEntry:
|
||||
"""
|
||||
Get avatar, either from cache or from LDAP on cache miss. If 'bust' is
|
||||
set to True, the cache will not be consulted and the newest result from
|
||||
LDAP will always be served.
|
||||
"""
|
||||
now = time.time()
|
||||
# Try to get a cache entry to serve, if possible.
|
||||
if not bust and uid in self.entries:
|
||||
entry = self.entries[uid]
|
||||
if entry.deadline > now:
|
||||
return entry.serve()
|
||||
|
||||
# Otherwise, retrieve from LDAP.
|
||||
conn = context.get_admin_connection()
|
||||
try:
|
||||
dn = ldaputils.user_dn(uid)
|
||||
res = conn.search_s(dn, ldap.SCOPE_SUBTREE)
|
||||
except ldap.NO_SUCH_OBJECT:
|
||||
res = []
|
||||
|
||||
avatar = None
|
||||
if len(res) == 1:
|
||||
for attr, vs in res[0][1].items():
|
||||
if attr == 'jpegPhoto':
|
||||
for v in vs:
|
||||
try:
|
||||
avatar = base64.b64decode(v)
|
||||
except binascii.Error as e:
|
||||
log.warning("Could not b64decode avatar for {}".format(uid))
|
||||
avatar = None
|
||||
else:
|
||||
break
|
||||
break
|
||||
# If nothing was found in LDAP (either uid doesn't exist or uid doesn't
|
||||
# have an avatar attached), serve default avatar.
|
||||
if avatar is None:
|
||||
avatar = default_avatar(uid)
|
||||
|
||||
# Save avatar in cache.
|
||||
entry = AvatarCacheEntry(uid, avatar)
|
||||
self.entries[uid] = entry
|
||||
|
||||
# And serve the entry.
|
||||
return entry.serve()
|
||||
|
||||
cache = AvatarCache()
|
||||
|
||||
@bp.route('/avatar/<uid>', methods=['GET'])
|
||||
def avatar_serve(uid):
|
||||
return cache.get(uid)
|
Loading…
Add table
Reference in a new issue