We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
The captcha does not work under Python 3
@morxa [reported](https://github.com/fedora-infra/bodhi/issues/2700#issuecomment-441245759) that the captcha was not working in Fedora's Bodhi deployment:


![screenshot from 2018-11-23 13-44-15](https://user-images.githubusercontent.com/4573064/48946644-49c14a00-ef26-11e8-9644-130ccf3267e9.png)

It turns out that it does work if Bodhi is run with Python 2, but fails under Python 3. My guess is that there is a ```str```/```bytes``` mismatch somewhere that we need to find and fix.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `bodhi/server/captcha.py`
Content:
```
1 # -*- coding: utf-8 -*-
2 # Copyright © 2014-2018 Red Hat, Inc.
3 #
4 # This file is part of Bodhi.
5 #
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 # 02110-1301, USA.
20 #
21 # Authors:  Ralph Bean <rbean@redhat.com>
22 """Define utilities and a view pertaining to captcha images for unauthenticated users."""
23 
24 from __future__ import division
25 import base64
26 import math
27 import random
28 import binascii
29 
30 from PIL import Image, ImageDraw, ImageFont
31 from pyramid.httpexceptions import HTTPGone, HTTPNotFound
32 from pyramid.view import view_config
33 import cryptography.fernet
34 import six
35 
36 
37 def math_generator(plainkey, settings):
38     """
39     Given a plainkey, return its expected value.
40 
41     Args:
42         plainkey (basestring or None): The key to encrypt as ciphertext. If None, a random key is
43             generated.
44         settings (bodhi.server.config.BodhiConfig): Bodhi's settings, unsued.
45     Returns:
46         tuple: A 2-tuple is returned. The first element is the plainkey, and the second is its
47             encrypted value.
48     Raises:
49         ValueError: If the plainkey is not valid.
50     """
51     if not plainkey:
52         x = random.randint(1, 100)
53         y = random.randint(1, 100)
54         plainkey = "%i + %i =" % (x, y)
55 
56     tokens = plainkey.split()
57     if not len(tokens) == 4:
58         raise ValueError("%s is an invalid plainkey" % plainkey)
59 
60     if tokens[1] != '+' or tokens[3] != '=':
61         raise ValueError("%s is an invalid plainkey" % plainkey)
62 
63     x, y = int(tokens[0]), int(tokens[2])
64 
65     value = six.text_type(x + y)
66     return plainkey, value
67 
68 
69 def jpeg_generator(plainkey, settings):
70     """
71     Generate an image with plainkey written in it.
72 
73     Args:
74         plainkey (basestring): The text to include in the generated image.
75         settings (bodhi.server.config.BodhiConfig): Bodhi's settings.
76     Returns:
77         PIL.Image.Image: An image containing the given text.
78     """
79     image_size = image_width, image_height = (
80         settings.get('captcha.image_width'),
81         settings.get('captcha.image_height'),
82     )
83     font_path = settings.get('captcha.font_path')
84     font_size = settings.get('captcha.font_size')
85     font_color = settings.get('captcha.font_color')
86     background_color = settings.get('captcha.background_color')
87     padding = settings.get('captcha.padding')
88 
89     img = Image.new('RGB', image_size, color=background_color)
90 
91     font = ImageFont.truetype(font_path, font_size)
92     width, height = font.getsize(plainkey)
93 
94     draw = ImageDraw.Draw(img)
95     position = (
96         random.randint(padding, (image_width - width - padding)),
97         random.randint(padding, (image_height - height - padding)))
98     draw.text(position, plainkey, font=font, fill=font_color)
99 
100     # Make it crazy!
101     img = warp_image(img)
102 
103     return img
104 
105 
106 def warp_image(image):
107     """
108     Apply some random bending operations to the given image.
109 
110     This function attempts to make it harder for bots to read the text inside the image, while
111     allowing humans to read it.
112 
113     Args:
114         image (PIL.Image.Image): The image to warp.
115     Returns:
116         PIL.Image.Image: A warped transformation of the given image.
117     """
118     r = 10  # individually warp a bunch of 10x10 tiles.
119     mesh_x = (image.size[0] // r) + 2
120     mesh_y = (image.size[1] // r) + 2
121 
122     # Set up some random values we'll use over and over...
123     amplitude = random.uniform(6, 10)
124     period = random.uniform(0.65, 0.74)
125     offset = (
126         random.uniform(0, math.pi * 2 / period),
127         random.uniform(0, math.pi * 2 / period),
128     )
129 
130     def _sine(x, y, a=amplitude, p=period, o=offset):
131         """Given a single point, warp it."""
132         return (
133             math.sin((y + o[0]) * p) * a + x,
134             math.sin((x + o[1]) * p) * a + y,
135         )
136 
137     def _clamp(x, y):
138         """Don't warp things outside the bounds of the image."""
139         return (
140             max(0, min(image.size[0] - 1, x)),
141             max(0, min(image.size[1] - 1, y)),
142         )
143 
144     # Build a map of the corners of our r by r tiles, warping each one.
145     warp = [
146         [
147             _clamp(*_sine(i * r, j * r))
148             for j in range(mesh_y)
149         ] for i in range(mesh_x)
150     ]
151 
152     def _destination_rectangle(i, j):
153         """Return a happy tile from the original space."""
154         return (i * r, j * r, (i + 1) * r, (j + 1) * r)
155 
156     def _source_quadrilateral(i, j):
157         """
158         Return the set of warped corners for a given tile.
159 
160         Specified counter-clockwise as a tuple.
161         """
162         return (
163             warp[i][j][0], warp[i][j][1],
164             warp[i][j + 1][0], warp[i][j + 1][1],
165             warp[i + 1][j + 1][0], warp[i + 1][j + 1][1],
166             warp[i + 1][j][0], warp[i + 1][j][1],
167         )
168 
169     # Finally, prepare our list of sources->destinations for PIL.
170     mesh = [
171         (
172             _destination_rectangle(i, j),
173             _source_quadrilateral(i, j),
174         )
175         for j in range(mesh_y - 1)
176         for i in range(mesh_x - 1)
177     ]
178     # And do it.
179     return image.transform(image.size, Image.MESH, mesh, Image.BILINEAR)
180 
181 
182 def validate(request, cipherkey, value):
183     """
184     Return whether the value matches the expected value, based on the cipherkey.
185 
186     Args:
187         request (pyramid.util.Request): The current web request.
188         cipherkey (basestring): The encrypted Fernet key.
189         value (basestring): The value to be validated.
190     Returns:
191         bool: True if value matches the expected value based on the cipherkey, False otherwise.
192             False is also returned if the cipherkey is not found to be a valid Fernet token.
193     """
194     settings = request.registry.settings
195 
196     plainkey = decrypt(cipherkey, settings)
197 
198     _, expected_value = math_generator(plainkey=plainkey, settings=settings)
199     return value == expected_value
200 
201 
202 def generate_captcha(context, request):
203     """
204     Generate a key and a URL to a captcha image that matches the key.
205 
206     Args:
207         context (mako.runtime.Context): Unused.
208         request (pyramid.util.Request): The current web request.
209     Returns:
210         tuple: A 2-tuple of strings. The first is the ciphertext key for a captcha, and the second
211             is a URL to the captcha image that matches that key.
212     """
213     settings = request.registry.settings
214     plainkey, value = math_generator(plainkey=None, settings=settings)
215     cipherkey = encrypt(plainkey, settings)
216     url = request.route_url('captcha_image', cipherkey=cipherkey)
217     request.session['captcha'] = cipherkey  # Remember this to stop replay.
218     return cipherkey, url
219 
220 
221 def encrypt(plaintext, settings):
222     """
223     Calculate and return the ciphertext key from the given plaintext key.
224 
225     Args:
226         plaintext (basestring): A key you wish you encrypt.
227         settings (bodhi.server.config.BodhiConfig): Bodhi's settings.
228     Returns:
229         str: The ciphertext version of the given captcha key.
230     """
231     secret = settings['captcha.secret']
232     engine = cryptography.fernet.Fernet(secret)
233     ciphertext = engine.encrypt(plaintext.encode('utf-8'))
234     ciphertext = base64.urlsafe_b64encode(ciphertext)
235     return ciphertext
236 
237 
238 def decrypt(ciphertext, settings):
239     """
240     Calculate and return the plaintext key from the given ciphertext.
241 
242     Args:
243         ciphertext (str): The encrypted secret for a captcha image.
244         settings (bodhi.server.config.BodhiConfig): Bodhi's settings.
245     Returns:
246         unicode: The plaintext secret for a captcha image.
247     Raises:
248         pyramid.httpexceptions.HTTPNotFound: If the ciphertext can not be decoded as base64.
249         pyramid.httpexceptions.HTTPGone: If the captcha token has expired.
250     """
251     ttl = settings['captcha.ttl']
252     secret = settings['captcha.secret']
253     engine = cryptography.fernet.Fernet(secret)
254 
255     if isinstance(ciphertext, six.text_type):
256         ciphertext = ciphertext.encode('utf-8')
257 
258     try:
259         ciphertext = base64.urlsafe_b64decode(ciphertext)
260     except (TypeError, binascii.Error):
261         raise HTTPNotFound("%s is garbage" % ciphertext.decode('utf-8'))
262 
263     try:
264         plaintext = engine.decrypt(ciphertext, ttl=ttl)
265     except cryptography.fernet.InvalidToken:
266         raise HTTPGone('captcha token is no longer valid')
267 
268     return plaintext.decode('utf-8')
269 
270 
271 @view_config(route_name='captcha_image', renderer='jpeg')
272 def captcha_image(request):
273     """
274     Generate and return a captcha image.
275 
276     Args:
277         request (pyramid.util.Request): The current web request.
278     Returns:
279         PIL.Image.Image: The generated captcha image.
280     """
281     cipherkey = request.matchdict['cipherkey']
282     plainkey = decrypt(cipherkey, request.registry.settings)
283     image = jpeg_generator(plainkey, request.registry.settings)
284     return image
285
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/bodhi/server/captcha.py b/bodhi/server/captcha.py
--- a/bodhi/server/captcha.py
+++ b/bodhi/server/captcha.py
@@ -212,7 +212,7 @@
     """
     settings = request.registry.settings
     plainkey, value = math_generator(plainkey=None, settings=settings)
-    cipherkey = encrypt(plainkey, settings)
+    cipherkey = encrypt(plainkey, settings).decode('utf8')
     url = request.route_url('captcha_image', cipherkey=cipherkey)
     request.session['captcha'] = cipherkey  # Remember this to stop replay.
     return cipherkey, url

{"golden_diff": "diff --git a/bodhi/server/captcha.py b/bodhi/server/captcha.py\n--- a/bodhi/server/captcha.py\n+++ b/bodhi/server/captcha.py\n@@ -212,7 +212,7 @@\n     \"\"\"\n     settings = request.registry.settings\n     plainkey, value = math_generator(plainkey=None, settings=settings)\n-    cipherkey = encrypt(plainkey, settings)\n+    cipherkey = encrypt(plainkey, settings).decode('utf8')\n     url = request.route_url('captcha_image', cipherkey=cipherkey)\n     request.session['captcha'] = cipherkey  # Remember this to stop replay.\n     return cipherkey, url\n", "issue": "The captcha does not work under Python 3\n@morxa [reported](https://github.com/fedora-infra/bodhi/issues/2700#issuecomment-441245759) that the captcha was not working in Fedora's Bodhi deployment:\r\n\r\n\r\n![screenshot from 2018-11-23 13-44-15](https://user-images.githubusercontent.com/4573064/48946644-49c14a00-ef26-11e8-9644-130ccf3267e9.png)\r\n\r\nIt turns out that it does work if Bodhi is run with Python 2, but fails under Python 3. My guess is that there is a ```str```/```bytes``` mismatch somewhere that we need to find and fix.\n", "before_files": [{"content": "# -*- coding: utf-8 -*-\n# Copyright \u00a9 2014-2018 Red Hat, Inc.\n#\n# This file is part of Bodhi.\n#\n# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA\n# 02110-1301, USA.\n#\n# Authors:  Ralph Bean <rbean@redhat.com>\n\"\"\"Define utilities and a view pertaining to captcha images for unauthenticated users.\"\"\"\n\nfrom __future__ import division\nimport base64\nimport math\nimport random\nimport binascii\n\nfrom PIL import Image, ImageDraw, ImageFont\nfrom pyramid.httpexceptions import HTTPGone, HTTPNotFound\nfrom pyramid.view import view_config\nimport cryptography.fernet\nimport six\n\n\ndef math_generator(plainkey, settings):\n    \"\"\"\n    Given a plainkey, return its expected value.\n\n    Args:\n        plainkey (basestring or None): The key to encrypt as ciphertext. If None, a random key is\n            generated.\n        settings (bodhi.server.config.BodhiConfig): Bodhi's settings, unsued.\n    Returns:\n        tuple: A 2-tuple is returned. The first element is the plainkey, and the second is its\n            encrypted value.\n    Raises:\n        ValueError: If the plainkey is not valid.\n    \"\"\"\n    if not plainkey:\n        x = random.randint(1, 100)\n        y = random.randint(1, 100)\n        plainkey = \"%i + %i =\" % (x, y)\n\n    tokens = plainkey.split()\n    if not len(tokens) == 4:\n        raise ValueError(\"%s is an invalid plainkey\" % plainkey)\n\n    if tokens[1] != '+' or tokens[3] != '=':\n        raise ValueError(\"%s is an invalid plainkey\" % plainkey)\n\n    x, y = int(tokens[0]), int(tokens[2])\n\n    value = six.text_type(x + y)\n    return plainkey, value\n\n\ndef jpeg_generator(plainkey, settings):\n    \"\"\"\n    Generate an image with plainkey written in it.\n\n    Args:\n        plainkey (basestring): The text to include in the generated image.\n        settings (bodhi.server.config.BodhiConfig): Bodhi's settings.\n    Returns:\n        PIL.Image.Image: An image containing the given text.\n    \"\"\"\n    image_size = image_width, image_height = (\n        settings.get('captcha.image_width'),\n        settings.get('captcha.image_height'),\n    )\n    font_path = settings.get('captcha.font_path')\n    font_size = settings.get('captcha.font_size')\n    font_color = settings.get('captcha.font_color')\n    background_color = settings.get('captcha.background_color')\n    padding = settings.get('captcha.padding')\n\n    img = Image.new('RGB', image_size, color=background_color)\n\n    font = ImageFont.truetype(font_path, font_size)\n    width, height = font.getsize(plainkey)\n\n    draw = ImageDraw.Draw(img)\n    position = (\n        random.randint(padding, (image_width - width - padding)),\n        random.randint(padding, (image_height - height - padding)))\n    draw.text(position, plainkey, font=font, fill=font_color)\n\n    # Make it crazy!\n    img = warp_image(img)\n\n    return img\n\n\ndef warp_image(image):\n    \"\"\"\n    Apply some random bending operations to the given image.\n\n    This function attempts to make it harder for bots to read the text inside the image, while\n    allowing humans to read it.\n\n    Args:\n        image (PIL.Image.Image): The image to warp.\n    Returns:\n        PIL.Image.Image: A warped transformation of the given image.\n    \"\"\"\n    r = 10  # individually warp a bunch of 10x10 tiles.\n    mesh_x = (image.size[0] // r) + 2\n    mesh_y = (image.size[1] // r) + 2\n\n    # Set up some random values we'll use over and over...\n    amplitude = random.uniform(6, 10)\n    period = random.uniform(0.65, 0.74)\n    offset = (\n        random.uniform(0, math.pi * 2 / period),\n        random.uniform(0, math.pi * 2 / period),\n    )\n\n    def _sine(x, y, a=amplitude, p=period, o=offset):\n        \"\"\"Given a single point, warp it.\"\"\"\n        return (\n            math.sin((y + o[0]) * p) * a + x,\n            math.sin((x + o[1]) * p) * a + y,\n        )\n\n    def _clamp(x, y):\n        \"\"\"Don't warp things outside the bounds of the image.\"\"\"\n        return (\n            max(0, min(image.size[0] - 1, x)),\n            max(0, min(image.size[1] - 1, y)),\n        )\n\n    # Build a map of the corners of our r by r tiles, warping each one.\n    warp = [\n        [\n            _clamp(*_sine(i * r, j * r))\n            for j in range(mesh_y)\n        ] for i in range(mesh_x)\n    ]\n\n    def _destination_rectangle(i, j):\n        \"\"\"Return a happy tile from the original space.\"\"\"\n        return (i * r, j * r, (i + 1) * r, (j + 1) * r)\n\n    def _source_quadrilateral(i, j):\n        \"\"\"\n        Return the set of warped corners for a given tile.\n\n        Specified counter-clockwise as a tuple.\n        \"\"\"\n        return (\n            warp[i][j][0], warp[i][j][1],\n            warp[i][j + 1][0], warp[i][j + 1][1],\n            warp[i + 1][j + 1][0], warp[i + 1][j + 1][1],\n            warp[i + 1][j][0], warp[i + 1][j][1],\n        )\n\n    # Finally, prepare our list of sources->destinations for PIL.\n    mesh = [\n        (\n            _destination_rectangle(i, j),\n            _source_quadrilateral(i, j),\n        )\n        for j in range(mesh_y - 1)\n        for i in range(mesh_x - 1)\n    ]\n    # And do it.\n    return image.transform(image.size, Image.MESH, mesh, Image.BILINEAR)\n\n\ndef validate(request, cipherkey, value):\n    \"\"\"\n    Return whether the value matches the expected value, based on the cipherkey.\n\n    Args:\n        request (pyramid.util.Request): The current web request.\n        cipherkey (basestring): The encrypted Fernet key.\n        value (basestring): The value to be validated.\n    Returns:\n        bool: True if value matches the expected value based on the cipherkey, False otherwise.\n            False is also returned if the cipherkey is not found to be a valid Fernet token.\n    \"\"\"\n    settings = request.registry.settings\n\n    plainkey = decrypt(cipherkey, settings)\n\n    _, expected_value = math_generator(plainkey=plainkey, settings=settings)\n    return value == expected_value\n\n\ndef generate_captcha(context, request):\n    \"\"\"\n    Generate a key and a URL to a captcha image that matches the key.\n\n    Args:\n        context (mako.runtime.Context): Unused.\n        request (pyramid.util.Request): The current web request.\n    Returns:\n        tuple: A 2-tuple of strings. The first is the ciphertext key for a captcha, and the second\n            is a URL to the captcha image that matches that key.\n    \"\"\"\n    settings = request.registry.settings\n    plainkey, value = math_generator(plainkey=None, settings=settings)\n    cipherkey = encrypt(plainkey, settings)\n    url = request.route_url('captcha_image', cipherkey=cipherkey)\n    request.session['captcha'] = cipherkey  # Remember this to stop replay.\n    return cipherkey, url\n\n\ndef encrypt(plaintext, settings):\n    \"\"\"\n    Calculate and return the ciphertext key from the given plaintext key.\n\n    Args:\n        plaintext (basestring): A key you wish you encrypt.\n        settings (bodhi.server.config.BodhiConfig): Bodhi's settings.\n    Returns:\n        str: The ciphertext version of the given captcha key.\n    \"\"\"\n    secret = settings['captcha.secret']\n    engine = cryptography.fernet.Fernet(secret)\n    ciphertext = engine.encrypt(plaintext.encode('utf-8'))\n    ciphertext = base64.urlsafe_b64encode(ciphertext)\n    return ciphertext\n\n\ndef decrypt(ciphertext, settings):\n    \"\"\"\n    Calculate and return the plaintext key from the given ciphertext.\n\n    Args:\n        ciphertext (str): The encrypted secret for a captcha image.\n        settings (bodhi.server.config.BodhiConfig): Bodhi's settings.\n    Returns:\n        unicode: The plaintext secret for a captcha image.\n    Raises:\n        pyramid.httpexceptions.HTTPNotFound: If the ciphertext can not be decoded as base64.\n        pyramid.httpexceptions.HTTPGone: If the captcha token has expired.\n    \"\"\"\n    ttl = settings['captcha.ttl']\n    secret = settings['captcha.secret']\n    engine = cryptography.fernet.Fernet(secret)\n\n    if isinstance(ciphertext, six.text_type):\n        ciphertext = ciphertext.encode('utf-8')\n\n    try:\n        ciphertext = base64.urlsafe_b64decode(ciphertext)\n    except (TypeError, binascii.Error):\n        raise HTTPNotFound(\"%s is garbage\" % ciphertext.decode('utf-8'))\n\n    try:\n        plaintext = engine.decrypt(ciphertext, ttl=ttl)\n    except cryptography.fernet.InvalidToken:\n        raise HTTPGone('captcha token is no longer valid')\n\n    return plaintext.decode('utf-8')\n\n\n@view_config(route_name='captcha_image', renderer='jpeg')\ndef captcha_image(request):\n    \"\"\"\n    Generate and return a captcha image.\n\n    Args:\n        request (pyramid.util.Request): The current web request.\n    Returns:\n        PIL.Image.Image: The generated captcha image.\n    \"\"\"\n    cipherkey = request.matchdict['cipherkey']\n    plainkey = decrypt(cipherkey, request.registry.settings)\n    image = jpeg_generator(plainkey, request.registry.settings)\n    return image\n", "path": "bodhi/server/captcha.py"}], "after_files": [{"content": "# -*- coding: utf-8 -*-\n# Copyright \u00a9 2014-2018 Red Hat, Inc.\n#\n# This file is part of Bodhi.\n#\n# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA\n# 02110-1301, USA.\n#\n# Authors:  Ralph Bean <rbean@redhat.com>\n\"\"\"Define utilities and a view pertaining to captcha images for unauthenticated users.\"\"\"\n\nfrom __future__ import division\nimport base64\nimport math\nimport random\nimport binascii\n\nfrom PIL import Image, ImageDraw, ImageFont\nfrom pyramid.httpexceptions import HTTPGone, HTTPNotFound\nfrom pyramid.view import view_config\nimport cryptography.fernet\nimport six\n\n\ndef math_generator(plainkey, settings):\n    \"\"\"\n    Given a plainkey, return its expected value.\n\n    Args:\n        plainkey (basestring or None): The key to encrypt as ciphertext. If None, a random key is\n            generated.\n        settings (bodhi.server.config.BodhiConfig): Bodhi's settings, unsued.\n    Returns:\n        tuple: A 2-tuple is returned. The first element is the plainkey, and the second is its\n            encrypted value.\n    Raises:\n        ValueError: If the plainkey is not valid.\n    \"\"\"\n    if not plainkey:\n        x = random.randint(1, 100)\n        y = random.randint(1, 100)\n        plainkey = \"%i + %i =\" % (x, y)\n\n    tokens = plainkey.split()\n    if not len(tokens) == 4:\n        raise ValueError(\"%s is an invalid plainkey\" % plainkey)\n\n    if tokens[1] != '+' or tokens[3] != '=':\n        raise ValueError(\"%s is an invalid plainkey\" % plainkey)\n\n    x, y = int(tokens[0]), int(tokens[2])\n\n    value = six.text_type(x + y)\n    return plainkey, value\n\n\ndef jpeg_generator(plainkey, settings):\n    \"\"\"\n    Generate an image with plainkey written in it.\n\n    Args:\n        plainkey (basestring): The text to include in the generated image.\n        settings (bodhi.server.config.BodhiConfig): Bodhi's settings.\n    Returns:\n        PIL.Image.Image: An image containing the given text.\n    \"\"\"\n    image_size = image_width, image_height = (\n        settings.get('captcha.image_width'),\n        settings.get('captcha.image_height'),\n    )\n    font_path = settings.get('captcha.font_path')\n    font_size = settings.get('captcha.font_size')\n    font_color = settings.get('captcha.font_color')\n    background_color = settings.get('captcha.background_color')\n    padding = settings.get('captcha.padding')\n\n    img = Image.new('RGB', image_size, color=background_color)\n\n    font = ImageFont.truetype(font_path, font_size)\n    width, height = font.getsize(plainkey)\n\n    draw = ImageDraw.Draw(img)\n    position = (\n        random.randint(padding, (image_width - width - padding)),\n        random.randint(padding, (image_height - height - padding)))\n    draw.text(position, plainkey, font=font, fill=font_color)\n\n    # Make it crazy!\n    img = warp_image(img)\n\n    return img\n\n\ndef warp_image(image):\n    \"\"\"\n    Apply some random bending operations to the given image.\n\n    This function attempts to make it harder for bots to read the text inside the image, while\n    allowing humans to read it.\n\n    Args:\n        image (PIL.Image.Image): The image to warp.\n    Returns:\n        PIL.Image.Image: A warped transformation of the given image.\n    \"\"\"\n    r = 10  # individually warp a bunch of 10x10 tiles.\n    mesh_x = (image.size[0] // r) + 2\n    mesh_y = (image.size[1] // r) + 2\n\n    # Set up some random values we'll use over and over...\n    amplitude = random.uniform(6, 10)\n    period = random.uniform(0.65, 0.74)\n    offset = (\n        random.uniform(0, math.pi * 2 / period),\n        random.uniform(0, math.pi * 2 / period),\n    )\n\n    def _sine(x, y, a=amplitude, p=period, o=offset):\n        \"\"\"Given a single point, warp it.\"\"\"\n        return (\n            math.sin((y + o[0]) * p) * a + x,\n            math.sin((x + o[1]) * p) * a + y,\n        )\n\n    def _clamp(x, y):\n        \"\"\"Don't warp things outside the bounds of the image.\"\"\"\n        return (\n            max(0, min(image.size[0] - 1, x)),\n            max(0, min(image.size[1] - 1, y)),\n        )\n\n    # Build a map of the corners of our r by r tiles, warping each one.\n    warp = [\n        [\n            _clamp(*_sine(i * r, j * r))\n            for j in range(mesh_y)\n        ] for i in range(mesh_x)\n    ]\n\n    def _destination_rectangle(i, j):\n        \"\"\"Return a happy tile from the original space.\"\"\"\n        return (i * r, j * r, (i + 1) * r, (j + 1) * r)\n\n    def _source_quadrilateral(i, j):\n        \"\"\"\n        Return the set of warped corners for a given tile.\n\n        Specified counter-clockwise as a tuple.\n        \"\"\"\n        return (\n            warp[i][j][0], warp[i][j][1],\n            warp[i][j + 1][0], warp[i][j + 1][1],\n            warp[i + 1][j + 1][0], warp[i + 1][j + 1][1],\n            warp[i + 1][j][0], warp[i + 1][j][1],\n        )\n\n    # Finally, prepare our list of sources->destinations for PIL.\n    mesh = [\n        (\n            _destination_rectangle(i, j),\n            _source_quadrilateral(i, j),\n        )\n        for j in range(mesh_y - 1)\n        for i in range(mesh_x - 1)\n    ]\n    # And do it.\n    return image.transform(image.size, Image.MESH, mesh, Image.BILINEAR)\n\n\ndef validate(request, cipherkey, value):\n    \"\"\"\n    Return whether the value matches the expected value, based on the cipherkey.\n\n    Args:\n        request (pyramid.util.Request): The current web request.\n        cipherkey (basestring): The encrypted Fernet key.\n        value (basestring): The value to be validated.\n    Returns:\n        bool: True if value matches the expected value based on the cipherkey, False otherwise.\n            False is also returned if the cipherkey is not found to be a valid Fernet token.\n    \"\"\"\n    settings = request.registry.settings\n\n    plainkey = decrypt(cipherkey, settings)\n\n    _, expected_value = math_generator(plainkey=plainkey, settings=settings)\n    return value == expected_value\n\n\ndef generate_captcha(context, request):\n    \"\"\"\n    Generate a key and a URL to a captcha image that matches the key.\n\n    Args:\n        context (mako.runtime.Context): Unused.\n        request (pyramid.util.Request): The current web request.\n    Returns:\n        tuple: A 2-tuple of strings. The first is the ciphertext key for a captcha, and the second\n            is a URL to the captcha image that matches that key.\n    \"\"\"\n    settings = request.registry.settings\n    plainkey, value = math_generator(plainkey=None, settings=settings)\n    cipherkey = encrypt(plainkey, settings).decode('utf8')\n    url = request.route_url('captcha_image', cipherkey=cipherkey)\n    request.session['captcha'] = cipherkey  # Remember this to stop replay.\n    return cipherkey, url\n\n\ndef encrypt(plaintext, settings):\n    \"\"\"\n    Calculate and return the ciphertext key from the given plaintext key.\n\n    Args:\n        plaintext (basestring): A key you wish you encrypt.\n        settings (bodhi.server.config.BodhiConfig): Bodhi's settings.\n    Returns:\n        str: The ciphertext version of the given captcha key.\n    \"\"\"\n    secret = settings['captcha.secret']\n    engine = cryptography.fernet.Fernet(secret)\n    ciphertext = engine.encrypt(plaintext.encode('utf-8'))\n    ciphertext = base64.urlsafe_b64encode(ciphertext)\n    return ciphertext\n\n\ndef decrypt(ciphertext, settings):\n    \"\"\"\n    Calculate and return the plaintext key from the given ciphertext.\n\n    Args:\n        ciphertext (str): The encrypted secret for a captcha image.\n        settings (bodhi.server.config.BodhiConfig): Bodhi's settings.\n    Returns:\n        unicode: The plaintext secret for a captcha image.\n    Raises:\n        pyramid.httpexceptions.HTTPNotFound: If the ciphertext can not be decoded as base64.\n        pyramid.httpexceptions.HTTPGone: If the captcha token has expired.\n    \"\"\"\n    ttl = settings['captcha.ttl']\n    secret = settings['captcha.secret']\n    engine = cryptography.fernet.Fernet(secret)\n\n    if isinstance(ciphertext, six.text_type):\n        ciphertext = ciphertext.encode('utf-8')\n\n    try:\n        ciphertext = base64.urlsafe_b64decode(ciphertext)\n    except (TypeError, binascii.Error):\n        raise HTTPNotFound(\"%s is garbage\" % ciphertext.decode('utf-8'))\n\n    try:\n        plaintext = engine.decrypt(ciphertext, ttl=ttl)\n    except cryptography.fernet.InvalidToken:\n        raise HTTPGone('captcha token is no longer valid')\n\n    return plaintext.decode('utf-8')\n\n\n@view_config(route_name='captcha_image', renderer='jpeg')\ndef captcha_image(request):\n    \"\"\"\n    Generate and return a captcha image.\n\n    Args:\n        request (pyramid.util.Request): The current web request.\n    Returns:\n        PIL.Image.Image: The generated captcha image.\n    \"\"\"\n    cipherkey = request.matchdict['cipherkey']\n    plainkey = decrypt(cipherkey, request.registry.settings)\n    image = jpeg_generator(plainkey, request.registry.settings)\n    return image\n", "path": "bodhi/server/captcha.py"}]}