123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225 |
- import json
- import os
- import random
- import subprocess
- import tempfile
- from io import BytesIO
- from PIL import Image, ImageDraw, ImageFont
- from ranged_response import RangedFileResponse
- from django.core.exceptions import ImproperlyConfigured
- from django.http import Http404, HttpResponse
- from captcha.conf import settings
- from captcha.helpers import captcha_audio_url, captcha_image_url
- from captcha.models import CaptchaStore
- # Distance of the drawn text from the top of the captcha image
- DISTANCE_FROM_TOP = 4
- def getsize(font, text):
- if hasattr(font, "getbbox"):
- _top, _left, _right, _bottom = font.getbbox(text)
- return _right - _left, _bottom - _top
- elif hasattr(font, "getoffset"):
- return tuple([x + y for x, y in zip(font.getsize(text), font.getoffset(text))])
- else:
- return font.getsize(text)
- def makeimg(size):
- if settings.CAPTCHA_BACKGROUND_COLOR == "transparent":
- image = Image.new("RGBA", size)
- else:
- image = Image.new("RGB", size, settings.CAPTCHA_BACKGROUND_COLOR)
- return image
- def captcha_image(request, key, scale=1):
- if scale == 2 and not settings.CAPTCHA_2X_IMAGE:
- raise Http404
- try:
- store = CaptchaStore.objects.get(hashkey=key)
- except CaptchaStore.DoesNotExist:
- # HTTP 410 Gone status so that crawlers don't index these expired urls.
- return HttpResponse(status=410)
- random.seed(key) # Do not generate different images for the same key
- text = store.challenge
- if isinstance(settings.CAPTCHA_FONT_PATH, str):
- fontpath = settings.CAPTCHA_FONT_PATH
- elif isinstance(settings.CAPTCHA_FONT_PATH, (list, tuple)):
- fontpath = random.choice(settings.CAPTCHA_FONT_PATH)
- else:
- raise ImproperlyConfigured(
- "settings.CAPTCHA_FONT_PATH needs to be a path to a font or list of paths to fonts"
- )
- if fontpath.lower().strip().endswith("ttf"):
- font = ImageFont.truetype(fontpath, settings.CAPTCHA_FONT_SIZE * scale)
- else:
- font = ImageFont.load(fontpath)
- if settings.CAPTCHA_IMAGE_SIZE:
- size = settings.CAPTCHA_IMAGE_SIZE
- else:
- size = getsize(font, text)
- size = (size[0] * 2, int(size[1] * 1.4))
- image = makeimg(size)
- xpos = 2
- charlist = []
- for char in text:
- if char in settings.CAPTCHA_PUNCTUATION and len(charlist) >= 1:
- charlist[-1] += char
- else:
- charlist.append(char)
- for char in charlist:
- fgimage = Image.new("RGB", size, settings.CAPTCHA_FOREGROUND_COLOR)
- charimage = Image.new("L", getsize(font, " %s " % char), "#000000")
- chardraw = ImageDraw.Draw(charimage)
- chardraw.text((0, 0), " %s " % char, font=font, fill="#ffffff")
- if settings.CAPTCHA_LETTER_ROTATION:
- charimage = charimage.rotate(
- random.randrange(*settings.CAPTCHA_LETTER_ROTATION),
- expand=0,
- resample=Image.BICUBIC,
- )
- charimage = charimage.crop(charimage.getbbox())
- maskimage = Image.new("L", size)
- maskimage.paste(
- charimage,
- (
- xpos,
- DISTANCE_FROM_TOP,
- xpos + charimage.size[0],
- DISTANCE_FROM_TOP + charimage.size[1],
- ),
- )
- size = maskimage.size
- image = Image.composite(fgimage, image, maskimage)
- xpos = xpos + 2 + charimage.size[0]
- if settings.CAPTCHA_IMAGE_SIZE:
- # centering captcha on the image
- tmpimg = makeimg(size)
- tmpimg.paste(
- image,
- (
- int((size[0] - xpos) / 2),
- int((size[1] - charimage.size[1]) / 2 - DISTANCE_FROM_TOP),
- ),
- )
- image = tmpimg.crop((0, 0, size[0], size[1]))
- else:
- image = image.crop((0, 0, xpos + 1, size[1]))
- draw = ImageDraw.Draw(image)
- for f in settings.noise_functions():
- draw = f(draw, image)
- for f in settings.filter_functions():
- image = f(image)
- out = BytesIO()
- image.save(out, "PNG")
- out.seek(0)
- response = HttpResponse(content_type="image/png")
- response.write(out.read())
- response["Content-length"] = out.tell()
- # At line :50 above we fixed the random seed so that we always generate the
- # same image, see: https://github.com/mbi/django-simple-captcha/pull/194
- # This is a problem though, because knowledge of the seed will let an attacker
- # predict the next random (globally). We therefore reset the random here.
- # Reported in https://github.com/mbi/django-simple-captcha/pull/221
- random.seed()
- return response
- def captcha_audio(request, key):
- if settings.CAPTCHA_FLITE_PATH:
- try:
- store = CaptchaStore.objects.get(hashkey=key)
- except CaptchaStore.DoesNotExist:
- # HTTP 410 Gone status so that crawlers don't index these expired urls.
- return HttpResponse(status=410)
- text = store.challenge
- if "captcha.helpers.math_challenge" == settings.CAPTCHA_CHALLENGE_FUNCT:
- text = text.replace("*", "times").replace("-", "minus").replace("+", "plus")
- else:
- text = ", ".join(list(text))
- path = str(os.path.join(tempfile.gettempdir(), "%s.wav" % key))
- subprocess.call([settings.CAPTCHA_FLITE_PATH, "-t", text, "-o", path])
- # Add arbitrary noise if sox is installed
- if settings.CAPTCHA_SOX_PATH:
- arbnoisepath = str(
- os.path.join(tempfile.gettempdir(), "%s_arbitrary.wav") % key
- )
- mergedpath = str(os.path.join(tempfile.gettempdir(), "%s_merged.wav") % key)
- subprocess.call(
- [
- settings.CAPTCHA_SOX_PATH,
- "-r",
- "8000",
- "-n",
- arbnoisepath,
- "synth",
- "2",
- "brownnoise",
- "gain",
- "-15",
- ]
- )
- subprocess.call(
- [
- settings.CAPTCHA_SOX_PATH,
- "-m",
- arbnoisepath,
- path,
- "-t",
- "wavpcm",
- "-b",
- "16",
- mergedpath,
- ]
- )
- os.remove(arbnoisepath)
- os.remove(path)
- os.rename(mergedpath, path)
- if os.path.isfile(path):
- response = RangedFileResponse(
- request, open(path, "rb"), content_type="audio/wav"
- )
- response["Content-Disposition"] = 'attachment; filename="{}.wav"'.format(
- key
- )
- return response
- raise Http404
- def captcha_refresh(request):
- """Return json with new captcha for ajax refresh request"""
- if not request.headers.get("x-requested-with") == "XMLHttpRequest":
- raise Http404
- new_key = CaptchaStore.pick()
- to_json_response = {
- "key": new_key,
- "image_url": captcha_image_url(new_key),
- "audio_url": captcha_audio_url(new_key)
- if settings.CAPTCHA_FLITE_PATH
- else None,
- }
- return HttpResponse(json.dumps(to_json_response), content_type="application/json")
|