108 lines
3.1 KiB
Python
108 lines
3.1 KiB
Python
import io
|
|
import secrets
|
|
import uuid
|
|
import base64
|
|
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
|
|
from django.core.cache import caches
|
|
|
|
default_cache = caches['default']
|
|
|
|
CAPTCHA_CHARS = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
|
|
|
|
|
|
def _random_color(low, high):
|
|
return tuple(secrets.randbelow(high - low + 1) + low for _ in range(3))
|
|
|
|
|
|
def generate_captcha():
|
|
captcha_key = str(uuid.uuid4())
|
|
code = ''.join(secrets.choice(CAPTCHA_CHARS) for _ in range(4))
|
|
|
|
width, height = 120, 40
|
|
image = Image.new('RGB', (width, height), _random_color(200, 255))
|
|
draw = ImageDraw.Draw(image)
|
|
|
|
for _ in range(30):
|
|
x = secrets.randbelow(width)
|
|
y = secrets.randbelow(height)
|
|
draw.point((x, y), fill=_random_color(0, 255))
|
|
|
|
for _ in range(3):
|
|
x1 = secrets.randbelow(width)
|
|
y1 = secrets.randbelow(height)
|
|
x2 = secrets.randbelow(width)
|
|
y2 = secrets.randbelow(height)
|
|
draw.line([(x1, y1), (x2, y2)], fill=_random_color(0, 255), width=1)
|
|
|
|
font = ImageFont.load_default()
|
|
|
|
for i, char in enumerate(code):
|
|
font_size = secrets.randbelow(5) + 12
|
|
try:
|
|
char_font = ImageFont.load_default(size=font_size)
|
|
except TypeError:
|
|
char_font = ImageFont.load_default()
|
|
|
|
char_image = Image.new('RGBA', (30, 30), (0, 0, 0, 0))
|
|
char_draw = ImageDraw.Draw(char_image)
|
|
char_color = _random_color(0, 100)
|
|
char_draw.text((5, 5), char, font=char_font, fill=char_color)
|
|
|
|
angle = secrets.randbelow(61) - 30
|
|
rotated = char_image.rotate(angle, expand=True, resample=Image.BICUBIC)
|
|
|
|
x_offset = i * 25 + secrets.randbelow(6)
|
|
y_offset = secrets.randbelow(10)
|
|
image.paste(rotated, (x_offset, y_offset), rotated)
|
|
|
|
buffer = io.BytesIO()
|
|
image.save(buffer, format='PNG')
|
|
image_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
|
|
captcha_image = f'data:image/png;base64,{image_base64}'
|
|
|
|
default_cache.set(f'captcha_{captcha_key}', code.upper(), timeout=300)
|
|
|
|
return captcha_key, captcha_image
|
|
|
|
|
|
def verify_captcha(captcha_key, captcha_code):
|
|
cache_key = f'captcha_{captcha_key}'
|
|
stored_code = default_cache.get(cache_key)
|
|
|
|
if stored_code is None:
|
|
return 'expired'
|
|
|
|
if stored_code == captcha_code.upper():
|
|
default_cache.delete(cache_key)
|
|
return 'ok'
|
|
|
|
return 'wrong'
|
|
|
|
|
|
CAPTCHA_THRESHOLD = 3
|
|
CAPTCHA_TRIGGER_TTL = 600
|
|
|
|
|
|
def check_captcha_required(operation, identifier):
|
|
cache_key = f'captcha_trigger_{operation}_{identifier}'
|
|
count = default_cache.get(cache_key)
|
|
if count is not None and count >= CAPTCHA_THRESHOLD:
|
|
return True
|
|
return False
|
|
|
|
|
|
def record_failure(operation, identifier):
|
|
cache_key = f'captcha_trigger_{operation}_{identifier}'
|
|
count = default_cache.get(cache_key)
|
|
if count is None:
|
|
default_cache.set(cache_key, 1, timeout=CAPTCHA_TRIGGER_TTL)
|
|
else:
|
|
default_cache.set(cache_key, count + 1, timeout=CAPTCHA_TRIGGER_TTL)
|
|
|
|
|
|
def reset_failures(operation, identifier):
|
|
cache_key = f'captcha_trigger_{operation}_{identifier}'
|
|
default_cache.delete(cache_key)
|