This commit is contained in:
deathrow 2022-11-01 18:55:12 -04:00
commit 17903c4691
No known key found for this signature in database
GPG Key ID: FF39D67A22069F73
24 changed files with 2350 additions and 0 deletions

31
Dockerfile Normal file
View File

@ -0,0 +1,31 @@
ARG PYTHON_VERSION=3.8
FROM docker.io/python:${PYTHON_VERSION} as base
WORKDIR /app
FROM base as builder
ENV POETRY_VERSION=1.2.2
RUN pip install "poetry==$POETRY_VERSION"
RUN python -m venv /venv
COPY pyproject.toml poetry.lock config.sample.yaml matrix_registration ./
RUN . /venv/bin/activate && poetry install --no-dev --no-root
COPY . .
RUN . /venv/bin/activate && poetry build
# Runtime
FROM base as final
COPY --from=builder /venv /venv
COPY --from=builder /app/dist .
RUN . /venv/bin/activate && pip install *.whl
VOLUME ["/data"]
EXPOSE 5000/tcp
ENTRYPOINT ["/venv/bin/matrix-registration", "--config-path=/data/config.yaml"]

59
README.md Normal file
View File

@ -0,0 +1,59 @@
## Synapse-Captcha
A custom captcha for Synapse.
Disable registration in ``homeserver.yaml``
### Building
``
git clone https://codeberg.org/deathrow/synapse-captcha
``
``
cd synapse-captcha
``
``
docker build .
``
Modify `config.sample.yaml` to your needs and save as `config.yaml`
The `shared_secret` can be found in `homeserver.yaml`.
Redirect to your docker installation:
(Modify for your needs)
```
location /register {
include /config/nginx/proxy.conf;
include /config/nginx/resolver.conf;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
set $upstream_app matrix-registration;
set $upstream_port 5000;
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
}
```
ex. `matrix.example.tld/register`
Docker-compose example:
```
build: ./images/synapse-captcha
container_name: matrix-registration
restart: always
command: [
"--config-path=/data/config.yaml",
"serve"
]
ports:
- 127.0.0.1:5000:5000
volumes:
- ./matrix-registration_data:/data:Z
networks:
- matrix
```

38
config.sample.yaml Normal file
View File

@ -0,0 +1,38 @@
server_location: 'http://synapse:8008'
server_name: 'matrix.org'
shared_secret: 'RegistrationSharedSecret'
base_url: ''
riot_instance: 'https://riot.im/app/'
db: 'sqlite:////data/db.sqlite3'
host: '0.0.0.0'
port: 5000
rate_limit: ["10000 per day", "100 per minute"]
allow_cors: false
logging:
disable_existing_loggers: False
version: 1
root:
level: DEBUG
handlers: [console, file]
formatters:
brief:
format: '%(name)s - %(levelname)s - %(message)s'
precise:
format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
handlers:
console:
class: logging.StreamHandler
level: INFO
formatter: brief
stream: ext://sys.stdout
file:
class: logging.handlers.RotatingFileHandler
formatter: precise
level: INFO
filename: m_reg.log
maxBytes: 10485760 # 10MB
backupCount: 3
encoding: utf8
# password requirements
password:
min_length: 8

View File

@ -0,0 +1,5 @@
from . import api
from . import config
from . import captcha
name = 'matrix_registration'

194
matrix_registration/api.py Normal file
View File

@ -0,0 +1,194 @@
# Standard library imports...
import logging
from requests import exceptions
import re
from urllib.parse import urlparse
# Third-party imports...
from flask import (Blueprint, abort, jsonify, request, make_response,
render_template)
from wtforms import (Form, StringField, PasswordField, validators)
from wtforms.fields.simple import HiddenField
# Local imports...
from .matrix_api import create_account
from . import config
from . import captcha
logger = logging.getLogger(__name__)
api = Blueprint("api", __name__)
re_mxid = re.compile(r'^@?[a-zA-Z_\-=\.\/0-9]+(:[a-zA-Z\-\.:\/0-9]+)?$')
def validate_captcha(form, captcha_answer):
"""
validates captcha
Parameters
----------
arg1 : Form object
arg2 : str
captcha answer, e.g. '4tg'
Raises
-------
ValidationError
captcha is invalid
"""
if not captcha.captcha.validate(captcha_answer.data,
form.captcha_token.data):
raise validators.ValidationError("captcha is invalid")
def validate_username(form, username):
"""
validates username
Parameters
----------
arg1 : Form object
arg2 : str
username name, e.g: '@user:matrix.org' or 'user'
https://github.com/matrix-org/matrix-doc/blob/master/specification/appendices/identifier_grammar.rst#user-identifiers
Raises
-------
ValidationError
Username doesn't follow mxid requirements
"""
domain = urlparse(config.config.server_location).hostname
re_mxid = r'^@?[a-zA-Z_\-=\.\/0-9]+(:' + \
re.escape(domain) + \
r')?$'
err = "Username doesn't follow pattern: '%s'" % re_mxid
if not re.search(re_mxid, username.data):
raise validators.ValidationError(err)
def validate_password(form, password):
"""
validates username
Parameters
----------
arg1 : Form object
arg2 : str
password
Raises
-------
ValidationError
Password doesn't follow length requirements
"""
min_length = config.config.password['min_length']
err = 'Password should be between %s and 255 chars long' % min_length
if len(password.data) < min_length or len(password.data) > 255:
raise validators.ValidationError(err)
class RegistrationForm(Form):
"""
Registration Form
validates user account registration requests
"""
username = StringField(
'Username',
[
validators.Length(min=1, max=200),
# validators.Regexp(re_mxid)
validate_username
])
password = PasswordField(
'New Password',
[
# validators.Length(min=8),
validate_password,
validators.DataRequired(),
validators.EqualTo('confirm', message='Passwords must match')
])
confirm = PasswordField('Repeat Password')
captcha_answer = StringField("Captcha answer", [validate_captcha])
captcha_token = HiddenField("Captcha token")
@api.route('/register', methods=['GET', 'POST'])
def register():
"""
main user account registration endpoint
to register an account you need to send a
application/x-www-form-urlencoded request with
- username
- password
- confirm
- captcha_answer
- captcha_token
as described in the RegistrationForm
"""
if request.method == 'POST':
logger.debug('an account registration started...')
form = RegistrationForm(request.form)
logger.debug('validating request data...')
if form.validate():
logger.debug('request valid')
# remove sigil and the domain from the username
username = form.username.data.rsplit(':')[0].split('@')[-1]
logger.debug('creating account %s...' % username)
# send account creation request to the hs
try:
account_data = create_account(form.username.data,
form.password.data,
config.config.server_location,
config.config.shared_secret)
except exceptions.ConnectionError:
logger.error('can not connect to %s' %
config.config.server_location,
exc_info=True)
abort(500)
except exceptions.HTTPError as e:
resp = e.response
error = resp.json()
status_code = resp.status_code
if status_code == 404:
logger.error('no HS found at %s' %
config.config.server_location)
elif status_code == 403:
logger.error(
'wrong shared registration secret or not enabled')
elif status_code == 400:
# most likely this should only be triggered if a userid
# is already in use
return make_response(jsonify(error), 400)
else:
logger.error('failure communicating with HS',
exc_info=True)
abort(500)
logger.debug('account creation succeded!')
return jsonify(access_token=account_data['access_token'],
home_server=account_data['home_server'],
user_id=account_data['user_id'],
status='success',
status_code=200)
else:
logger.debug('account creation failed!')
captcha_data = captcha.captcha.generate()
resp = {
'errcode': 'MR_BAD_USER_REQUEST',
'error': form.errors,
"captcha_image": captcha_data["captcha_image"].decode(),
"captcha_token": captcha_data["captcha_token"]
}
return make_response(jsonify(resp), 400)
# for fieldName, errorMessages in form.errors.items():
# for err in errorMessages:
# # return error to user
else:
server_name = config.config.server_name
pw_length = config.config.password['min_length']
captcha_data = captcha.captcha.generate()
return render_template(
'register.html',
server_name=server_name,
pw_length=pw_length,
riot_instance=config.config.riot_instance,
base_url=config.config.base_url,
captcha_token=captcha_data["captcha_token"],
captcha_image=captcha_data["captcha_image"].decode())

View File

@ -0,0 +1,64 @@
import logging
import logging.config
import click
from flask import Flask
from flask.cli import FlaskGroup, pass_script_info
from flask_cors import CORS
from waitress import serve
from . import captcha
from .captcha import db
from . import config
import os
def create_app(testing=False):
app = Flask(__name__)
app.testing = testing
with app.app_context():
from .api import api
app.register_blueprint(api)
return app
@click.group(cls=FlaskGroup,
add_default_commands=False,
create_app=create_app,
context_settings=dict(help_option_names=['-h', '--help']))
@click.option("--config-path",
default="config.yaml",
help='specifies the config file to be used')
@pass_script_info
def cli(info, config_path):
"""a token based matrix registration app"""
config.config = config.Config(config_path)
logging.config.dictConfig(config.config.logging)
app = info.load_app()
with app.app_context():
app.config.from_mapping(
SQLALCHEMY_DATABASE_URI=config.config.db.format(
cwd=f"{os.getcwd()}/"),
SQLALCHEMY_TRACK_MODIFICATIONS=False)
db.init_app(app)
db.create_all()
captcha.captcha = captcha.CaptchaGenerator()
@cli.command("serve", help="start api server")
@pass_script_info
def run_server(info):
app = info.load_app()
if config.config.allow_cors:
CORS(app)
serve(app,
host=config.config.host,
port=config.config.port,
url_prefix=config.config.base_url)
if __name__ == "__main__":
cli()
run_server()

View File

@ -0,0 +1,71 @@
from captcha.image import ImageCaptcha
from flask_sqlalchemy import SQLAlchemy
import base64
import random
import string
import time
import uuid
CAPTCHA_TIMEOUT = 5 # minutes
CAPTCHA_LENGTH = 5 # characters
CAPTCHA_WIDTH = 320
CAPTCHA_HEIGHT = 94
db = SQLAlchemy()
class Captcha(db.Model):
__tablename__ = 'captcha'
token = db.Column(db.String(36), primary_key=True)
answer = db.Column(db.String(24))
timestamp = db.Column(db.Integer, default=0)
class CaptchaGenerator:
def clean(self):
Captcha.query.filter(
Captcha.timestamp < (time.time() - CAPTCHA_TIMEOUT * 60)).delete()
db.session.commit()
def validate(self, captcha_answer, captcha_token):
self.clean()
try:
cpt = Captcha.query.filter(Captcha.token == captcha_token).one()
except:
# when the user stay on the page too long the captcha is removed
return False
if cpt:
answer = cpt.answer
db.session.delete(cpt)
db.session.commit()
return captcha_answer.lower() == answer
return False
def generate(self):
self.clean()
captcha_token = str(uuid.uuid4())
captcha_answer = (''.join(
random.choice(string.ascii_lowercase + string.digits)
for _ in range(CAPTCHA_LENGTH)))
image = ImageCaptcha(width=CAPTCHA_WIDTH, height=CAPTCHA_HEIGHT)
captcha_image = base64.b64encode(
image.generate(captcha_answer).getvalue())
timestamp = time.time()
data = {
"captcha_image": captcha_image,
"captcha_token": captcha_token,
"captcha_answer": captcha_answer,
"timestamp": timestamp
}
cpt = Captcha(token=captcha_token,
answer=captcha_answer,
timestamp=timestamp)
db.session.add(cpt)
db.session.commit()
return data
captcha = None

View File

@ -0,0 +1,128 @@
# Standard library imports...
# from collections import namedtuple
import logging
import os
import sys
# Third-party imports...
import yaml
# Local imports...
from .constants import (CONFIG_PATH1, CONFIG_PATH2, CONFIG_PATH3, CONFIG_PATH4,
CONFIG_PATH5)
CONFIG_PATHS = [
CONFIG_PATH1, CONFIG_PATH2, CONFIG_PATH3, CONFIG_PATH4, CONFIG_PATH5
]
CONFIG_SAMPLE_NAME = "config.sample.yaml"
CONFIG_NAME = 'config.yaml'
logger = logging.getLogger(__name__)
class Config:
"""
Config
loads a dict or a yaml file to be accessible by all files in the module
"""
def __init__(self, data):
self.data = data
self.CONFIG_PATH = None
self.location = None
self.load()
def load(self):
"""
loads the dict/the yaml file and recursively sets dictionary to class properties
"""
logger.debug('loading config...')
dictionary = None
config_default = True
if type(self.data) is dict:
logger.debug('from dict...')
dictionary = self.data
config_default = False
else:
logger.debug('from file...')
# check work dir and all other pip install locations for config
if os.path.isfile(self.data):
config_default = False
else:
# provided file not found checking typical installation dirs
config_exists = False
for path in CONFIG_PATHS:
if os.path.isfile(path + CONFIG_NAME):
self.CONFIG_PATH = path
config_exists = True
config_default = False
if not config_exists:
# no config exists, use sample config instead
# check typical installation dirs for sample configs
for path in CONFIG_PATHS:
if os.path.isfile(path + CONFIG_SAMPLE_NAME):
self.CONFIG_PATH = path
config_exists = True
# check if still no config found
if not config_exists:
sys.exit('could not find any configuration file!')
self.data = os.path.join(self.CONFIG_PATH,
CONFIG_SAMPLE_NAME)
else:
self.data = os.path.join(self.CONFIG_PATH, CONFIG_NAME)
try:
with open(self.data, 'r') as stream:
dictionary = yaml.load(stream, Loader=yaml.SafeLoader)
except IOError as e:
sys.exit(e)
if config_default:
self.read_config(dictionary)
logger.debug('setting config...')
# recusively set dictionary to class properties
for k, v in dictionary.items():
setattr(self, k, v)
logger.debug('config set!')
# self.x = namedtuple('config',
# dictionary.keys())(*dictionary.values())
def update(self, data):
"""
resets all options and loads the new config
Parameters
----------
arg1 : dict or path to config file
"""
logger.debug('updating config...')
self.data = data
self.CONFIG_PATH = None
self.location = None
self.load()
logger.debug('config updated!')
def read_config(self, dictionary):
"""
asks the user how to set the essential options
Parameters
----------
arg1 : dict
with sample values
"""
# important keys that need to be changed
keys = ['server_location', 'server_name', 'shared_secret', 'port']
for key in keys:
temp = dictionary[key]
dictionary[key] = input('enter {}, e.g. {}\n'.format(key, temp))
if not dictionary[key].strip():
dictionary[key] = temp
# write to config file
new_config_path = self.CONFIG_PATH + CONFIG_NAME
relative_path = os.path.relpath(self.CONFIG_PATH + CONFIG_NAME)
with open(new_config_path, 'w') as stream:
yaml.dump(dictionary, stream, default_flow_style=False)
print('config file written to "%s"' % relative_path)
config = None

View File

@ -0,0 +1,19 @@
# Standard library imports...
import os
import site
import sys
# Third-party imports...
from appdirs import user_config_dir
__location__ = os.path.realpath(
os.path.join(os.getcwd(), os.path.dirname(__file__)))
WORD_LIST_PATH = os.path.join(__location__, 'wordlist.txt')
# first check in current working dir
CONFIG_PATH1 = os.path.join(os.getcwd() + '/')
CONFIG_PATH2 = os.path.join(os.getcwd() + '/config/')
# then check in XDG_CONFIG_HOME
CONFIG_PATH3 = os.path.join(user_config_dir('matrix-registration') + '/')
# check at installed location
CONFIG_PATH4 = os.path.join(__location__, '../')
CONFIG_PATH5 = os.path.join(sys.prefix, 'config/')

View File

@ -0,0 +1,75 @@
# Standard library imports...
import hashlib
import hmac
import requests
import logging
logger = logging.getLogger(__name__)
def create_account(user,
password,
server_location,
shared_secret,
admin=False):
"""
creates account
https://github.com/matrix-org/synapse/blob/master/synapse/_scripts/register_new_matrix_user.py
Parameters
----------
arg1 : str
local part of the new user
arg2 : str
password
arg3 : str
url to homeserver
arg4 : str
Registration Shared Secret as set in the homeserver.yaml
arg5 : bool
register new user as an admin.
Raises
-------
requests.exceptions.ConnectionError:
can't connect to homeserver
requests.exceptions.HTTPError:
something with the communciation to the homeserver failed
"""
nonce = _get_nonce(server_location)
mac = hmac.new(
key=str.encode(shared_secret),
digestmod=hashlib.sha1,
)
mac.update(nonce.encode())
mac.update(b'\x00')
mac.update(user.encode())
mac.update(b'\x00')
mac.update(password.encode())
mac.update(b'\x00')
mac.update(b'admin' if admin else b'notadmin')
mac = mac.hexdigest()
data = {
'nonce': nonce,
'username': user,
'password': password,
'admin': admin,
'mac': mac,
}
server_location = server_location.rstrip('/')
r = requests.post('%s/_synapse/admin/v1/register' % (server_location),
json=data)
r.raise_for_status()
return r.json()
def _get_nonce(server_location):
r = requests.get('%s/_synapse/admin/v1/register' % (server_location))
r.raise_for_status()
return r.json()['nonce']

View File

@ -0,0 +1,300 @@
html,
body {
height: 100%;
margin: 0;
}
body {
background-size: cover;
background-attachment: fixed;
overflow: hidden;
}
h1 {
font-size: 1.3em;
}
article {
color: white;
}
a:link,
a:visited {
color: #038db3 !important;
}
form {
width: 320px;
margin: 45px auto;
}
textarea {
resize: none;
}
input,
textarea {
background: none;
color: white;
font-size: 18px;
padding: 10px 10px 10px 5px;
display: block;
width: 320px;
border: none;
border-radius: 0;
border-bottom: 1px solid white;
}
input:focus,
textarea:focus {
outline: none;
}
input:focus~label,
input:not(:placeholder-shown)~label,
textarea:focus~label,
textarea:valid~label {
top: -14px;
font-size: 12px;
color: #03b381;
}
input:focus~.bar:before,
textarea:focus~.bar:before {
width: 320px;
}
input[type="password"] {
letter-spacing: 0.3em;
}
input:invalid {
box-shadow: none;
}
input:invalid~.bar:before {
background: #038db3;
}
input:invalid~label {
color: #038db3;
}
input[type="submit"] {
cursor: pointer;
}
label {
color: white;
font-size: 16px;
font-weight: normal;
position: absolute;
pointer-events: none;
left: 5px;
top: 10px;
transition: 300ms ease all;
}
*,
:before,
:after {
box-sizing: border-box;
}
.center {
text-align: center;
margin-top: 2em;
}
.hidden {
visibility: hidden;
opacity: 0;
}
.group {
position: relative;
margin: 45px 0;
}
.bar {
position: relative;
display: block;
width: 320px;
}
.bar:before {
content: '';
height: 2px;
width: 0;
bottom: 0px;
position: absolute;
background: #03b381;
transition: 300ms ease all;
left: 0%;
}
.btn {
background: white;
color: black;
border: none;
padding: 10px 20px;
border-radius: 3px;
letter-spacing: 0.06em;
text-transform: uppercase;
text-decoration: none;
outline: none;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
}
.btn:hover {
color: black;
box-shadow: 0 7px 14px rgba(0, 0, 0, 0.18), 0 5px 5px rgba(0, 0, 0, 0.12);
}
.btn.btn-submit {
background: #03b381;
color: #bce0fb;
}
.btn.btn-submit:hover {
background: #03b372;
color: #deeffd;
}
.btn-box {
text-align: center;
margin: 50px 0;
}
.info {
z-index: 2;
position: absolute;
bottom: .5vh;
right: 1vw;
text-align: left;
color: grey;
font-size: 0.8em;
opacity: 0.1;
transition: opacity 0.5s ease;
}
.info:hover {
opacity: 1;
}
.info a {
color: cyan;
}
.widget {
position: absolute;
left: 50%;
top: 50%;
border: 0px solid;
border-radius: 5px;
overflow: hidden;
background-color: #1f1f1f;
z-index: 1;
box-shadow: 2px 2px 5px 0px rgba(0, 0, 0, 0.5);
}
.widget::before {
position: absolute;
top: 0;
left: 0;
z-index: -1;
width: 100%;
height: 100%;
background-attachment: fixed;
background-size: cover;
opacity: 0.20;
content: "";
}
.blur:before {
content: "";
position: absolute;
width: 100%;
height: 100%;
background: inherit;
z-index: -1;
transform: scale(1.03);
filter: blur(10px);
}
.register {
margin-left: -15em;
margin-top: -25em;
width: 30em;
height: 50em;
}
.modal {
margin-left: -12.5em;
margin-top: -7.5em;
width: 25em;
background-color: #f7f7f7;
transition: visibility .3s, opacity .3s linear;
}
.modal article {
margin-top: -5em;
}
.modal article,
.modal p,
.modal h2,
.modal h3 {
color: #1f1f1f;
}
.error {
color: #b30335 !important;
}
@media only screen and (max-width: 500px) {
.info {
bottom: -2vh;
}
.widget {
margin-top: -40vh;
margin-left: -45vw;
width: 90vw;
min-width: 20em;
}
.modal {
margin-top: -15vh;
margin-left: -35vw;
width: 70vw;
min-width: 15em;
}
}
@media only screen and (max-height: 768px) {
body {
overflow-y: visible;
padding-bottom: -90vh;
}
.blur:before {
filter: none;
transform: none;
padding-bottom: 50em;
}
.info {
float: right;
padding-top: 57em;
position: static;
}
.widget {
margin-top: -40vh;
}
.modal {
margin-top: -15vh;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 849 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 530 KiB

View File

@ -0,0 +1,234 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width; initial-scale=1.0;" />
<meta property="og:title" content="{{ server_name }} registration">
<meta property="og:site_name" content="{{ server_name }}">
<meta property="og:type" content="website" />
<meta name="og:description" content="registrate an account on {{ server_name }}" />
<meta name="og:image" content="{{ url_for('static', filename='images/icon.png') }}" />
<link rel="apple-touch-icon" sizes="180x180" href="{{ url_for('static', filename='images/icon.png') }}">
<link rel="icon" type="image/png" href="{{ url_for('static', filename='images/icon32x32.png') }}" sizes="32x32">
<link rel="shortcut icon" href="{{ url_for('static', filename='images/favicon.ico') }}">
<meta name="msapplication-TileImage" content="{{ url_for('static', filename='images/tile.png') }}">
<meta name="msapplication-TileColor" content="#fff">
<title>{{ server_name }} registration</title>
<style>
body, .widget::before {
background-image: url("https://{{ server_name }}/background.jpg");
}
</style>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body class="blur">
<article class="widget register">
<div class="center">
<header>
<h1>{{ server_name }} registration</h1>
<p>the registration requires to solve a captcha<br>
registration does not require an email, just a username and a password that's longer than {{ pw_length }}
characters.</p>
</header>
<section>
<form id="registration" action="{{ base_url }}/register" method="post">
<input type="hidden" id="captcha_token" name="captcha_token" value="{{ captcha_token }}" />
<div class="group">
<input id="username" name="username" type="text" placeholder=" " required
pattern="^@?[a-zA-Z_\-=\.\/0-9]+(:{{ server_name|replace('.', '\.') }})?$" required minlength="1"
maxlength="200">
<span class="highlight"></span>
<span class="bar"></span>
<label for="username">Username</label>
</div>
<div class="group">
<input id="password" name="password" type="password" placeholder=" " required minlength="{{ pw_length }}"
maxlength="128">
<span class="highlight"></span>
<span class="bar"></span>
<label for="password">Password</label>
</div>
<div class="group">
<input id="confirm_password" name="confirm" type="password" placeholder=" " required>
<span class="highlight"></span>
<span class="bar"></span>
<label for="confirm_password">Confirm</label>
</div>
<div class="group">
<input id="captcha_answer" name="captcha_answer" type="text" placeholder=" " required >
<span class="highlight"></span>
<span class="bar"></span>
<label for="captcha">Captcha</label>
</div>
<div>
<img id="captcha_image" src="data:image/png;base64,{{ captcha_image }}" />
</div>
<div class="btn-box">
<input class="btn btn-submit" type="submit" value="register">
</div>
</form>
</section>
</div>
</article>
<article id="success" class="widget modal hidden">
<div class="center">
<header>
<h2 id="welcome"></h2>
</header>
<section>
<p>Click here to login in:</p>
<h3><a href="{{ riot_instance }}"><img src="static/images/element-logo.png" height="100px"></a></h3>
<p>or choose one of the many other clients here: <a href="https://matrix.org/docs/projects/clients-matrix"
a>https://matrix.org/docs/projects/clients-matrix</a></p>
</section>
</div>
</article>
<article id="error" class="widget modal hidden">
<div class="center">
<header>
<h2>Error</h2>
</header>
<section>
<p>There was an error while trying to register you.</p>
<h3 id="error_message" class="error"></h3>
<p id="error_dialog"></p>
</section>
</div>
</article>
<script>
// all javascript here is optional, the registration form works fine without
/*
What this script does:
- confirm password validator needs javascript, otherwise always valid as long as not empty
- set token with ?token query parameter
- set custom validity messages
*/
// see https://stackoverflow.com/a/3028037
function hideOnClickOutside(element) {
const outsideClickListener = event => {
if (!element.contains(event.target) && isVisible(
element)) {
element.classList.add("hidden");
removeClickListener()
}
}
const removeClickListener = () => {
document.removeEventListener("click", outsideClickListener)
}
document.addEventListener("click", outsideClickListener)
}
const isVisible = elem => !!elem && !!(elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length)
const urlParams = new URLSearchParams(window.location.search);
// html5 validators
var username = document.getElementById("username");
var password = document.getElementById("password");
var confirm_password = document.getElementById("confirm_password");
username.addEventListener("input", function (event) {
if (username.validity.typeMismatch) {
username.setCustomValidity("format: @username:{{ server_name }}");
} else {
username.setCustomValidity("");
}
});
password.addEventListener("input", function (event) {
if (password.validity.typeMismatch) {
password.setCustomValidity("atleast {{ pw_length }} characters long");
} else {
password.setCustomValidity("");
}
});
function validatePassword() {
if (password.value != confirm_password.value) {
confirm_password.setCustomValidity("passwords don't match");
} else {
confirm_password.setCustomValidity("");
}
}
password.onchange = validatePassword;
confirm_password.onkeyup = validatePassword;
function showError(message, dialog) {
document.getElementById("error_message").innerHTML = message;
document.getElementById("error_dialog").innerHTML = dialog;
let error = document.getElementById("error");
error.classList.remove("hidden");
hideOnClickOutside(error);
}
// hijack the submit button to display the json response in a neat modal
var form = document.getElementById("registration");
function sendData() {
let XHR = new XMLHttpRequest();
// Bind the FormData object and the form element
let FD = new FormData(form);
// Define what happens on successful data submission
XHR.addEventListener("load", function (event) {
console.log(XHR.responseText);
let response = JSON.parse(XHR.responseText);
try {
console.log(response);
} catch (e) {
if (e instanceof SyntaxError) {
showError("Internal Server Error!", "Please contact the server admin about this.");
return;
}
}
if ("errcode" in response) {
if (response["errcode"] == "MR_BAD_USER_REQUEST") {
if ("captcha_answer" in response["error"]) {
showError("Captcha Error", response["error"]["captcha_answer"][0]);
} else if ("password" in response["error"]) {
showError("Password Error", response["error"]["password"][0]);
} else if ("username" in response["error"]) {
showError("Username Error", response["error"]["username"][0]);
}
document.getElementById("captcha_image").src = "data:image/png;base64," + response["captcha_image"]
document.getElementById("captcha_token").value = response["captcha_token"]
return;
} else {
showError("Homeserver Error", response["error"]);
}
} else {
document.getElementById("welcome").innerHTML = "Welcome " + response['user_id'];
document.getElementById("success").classList.remove("hidden");
}
});
// Define what happens in case of error
XHR.addEventListener("error", function (event) {
showError("Internal Server Error!", "Please contact the server admin about this.");
});
// Set up our request
XHR.open("POST", "{{ base_url }}/register");
// The data sent is what the user provided in the form
XHR.send(FD);
}
// take over its submit event.
form.addEventListener("submit", function (event) {
event.preventDefault();
sendData();
});
</script>
</body>
</html>

689
poetry.lock generated Normal file
View File

@ -0,0 +1,689 @@
[[package]]
name = "appdirs"
version = "1.4.4"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "captcha"
version = "0.4"
description = "A captcha library that generates audio and image CAPTCHAs."
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
Pillow = "*"
[[package]]
name = "certifi"
version = "2021.10.8"
description = "Python package for providing Mozilla's CA Bundle."
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "charset-normalizer"
version = "2.0.12"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "main"
optional = false
python-versions = ">=3.5.0"
[package.extras]
unicode_backport = ["unicodedata2"]
[[package]]
name = "click"
version = "7.1.2"
description = "Composable command line interface toolkit"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "flake8"
version = "4.0.1"
description = "the modular source code checker: pep8 pyflakes and co"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
mccabe = ">=0.6.0,<0.7.0"
pycodestyle = ">=2.8.0,<2.9.0"
pyflakes = ">=2.4.0,<2.5.0"
[[package]]
name = "flask"
version = "1.1.4"
description = "A simple framework for building complex web applications."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.dependencies]
click = ">=5.1,<8.0"
itsdangerous = ">=0.24,<2.0"
Jinja2 = ">=2.10.1,<3.0"
Werkzeug = ">=0.15,<2.0"
[package.extras]
dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"]
docs = ["sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"]
dotenv = ["python-dotenv"]
[[package]]
name = "flask-cors"
version = "3.0.10"
description = "A Flask extension adding a decorator for CORS support"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
Flask = ">=0.9"
Six = "*"
[[package]]
name = "flask-httpauth"
version = "4.5.0"
description = "HTTP authentication for Flask routes"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
flask = "*"
[[package]]
name = "flask-sqlalchemy"
version = "2.4.4"
description = "Adds SQLAlchemy support to your Flask application."
category = "main"
optional = false
python-versions = ">= 2.7, != 3.0.*, != 3.1.*, != 3.2.*, != 3.3.*"
[package.dependencies]
Flask = ">=0.10"
SQLAlchemy = ">=0.8.0"
[[package]]
name = "idna"
version = "3.3"
description = "Internationalized Domain Names in Applications (IDNA)"
category = "main"
optional = false
python-versions = ">=3.5"
[[package]]
name = "itsdangerous"
version = "1.1.0"
description = "Various helpers to pass data to untrusted environments and back."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "jinja2"
version = "2.11.3"
description = "A very fast and expressive template engine."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.dependencies]
MarkupSafe = ">=0.23"
[package.extras]
i18n = ["Babel (>=0.8)"]
[[package]]
name = "markupsafe"
version = "2.0.1"
description = "Safely add untrusted strings to HTML/XML markup."
category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "mccabe"
version = "0.6.1"
description = "McCabe checker, plugin for flake8"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "parameterized"
version = "0.8.1"
description = "Parameterized testing with any Python test framework"
category = "dev"
optional = false
python-versions = "*"
[package.extras]
dev = ["jinja2"]
[[package]]
name = "pillow"
version = "9.1.0"
description = "Python Imaging Library (Fork)"
category = "main"
optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinx-rtd-theme (>=1.0)", "sphinxext-opengraph"]
tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
[[package]]
name = "psycopg2-binary"
version = "2.9.3"
description = "psycopg2 - Python-PostgreSQL Database Adapter"
category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "pycodestyle"
version = "2.8.0"
description = "Python style guide checker"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "pyflakes"
version = "2.4.0"
description = "passive checker of Python programs"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "python-dateutil"
version = "2.8.2"
description = "Extensions to the standard Python datetime module"
category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
[package.dependencies]
six = ">=1.5"
[[package]]
name = "pyyaml"
version = "5.4.1"
description = "YAML parser and emitter for Python"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
[[package]]
name = "requests"
version = "2.27.1"
description = "Python HTTP for Humans."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
[package.dependencies]
certifi = ">=2017.4.17"
charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""}
idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""}
urllib3 = ">=1.21.1,<1.27"
[package.extras]
socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "sqlalchemy"
version = "1.3.24"
description = "Database Abstraction Library"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.extras]
mssql = ["pyodbc"]
mssql_pymssql = ["pymssql"]
mssql_pyodbc = ["pyodbc"]
mysql = ["mysqlclient"]
oracle = ["cx-oracle"]
postgresql = ["psycopg2"]
postgresql_pg8000 = ["pg8000 (<1.16.6)"]
postgresql_psycopg2binary = ["psycopg2-binary"]
postgresql_psycopg2cffi = ["psycopg2cffi"]
pymysql = ["pymysql (<1)", "pymysql"]
[[package]]
name = "toml"
version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language"
category = "dev"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "urllib3"
version = "1.26.9"
description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
[package.extras]
brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"]
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[[package]]
name = "waitress"
version = "1.4.4"
description = "Waitress WSGI server"
category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[package.extras]
docs = ["Sphinx (>=1.8.1)", "docutils", "pylons-sphinx-themes (>=1.0.9)"]
testing = ["pytest", "pytest-cover", "coverage (>=5.0)"]
[[package]]
name = "werkzeug"
version = "1.0.1"
description = "The comprehensive WSGI web application library."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.extras]
dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"]
watchdog = ["watchdog"]
[[package]]
name = "wtforms"
version = "2.3.3"
description = "A flexible forms validation and rendering library for Python web development."
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
MarkupSafe = "*"
[package.extras]
email = ["email-validator"]
ipaddress = ["ipaddress"]
locale = ["Babel (>=1.3)"]
[[package]]
name = "yapf"
version = "0.32.0"
description = "A formatter for Python code."
category = "dev"
optional = false
python-versions = "*"
[extras]
postgres = []
[metadata]
lock-version = "1.1"
python-versions = '^3.8'
content-hash = "14688a3571abc43e9b65a9077b2f0a777d7d2c517c13732c4b2d4ecd37dda303"
[metadata.files]
appdirs = [
{file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
{file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
]
captcha = [
{file = "captcha-0.4-py3-none-any.whl", hash = "sha256:529941705c01c20143d030805f82a362ba5a2af898e59426acc2c8d649ba034c"},
{file = "captcha-0.4.tar.gz", hash = "sha256:2ae5e8daac4f1649b57b34328bcc45ba691b5c707fc6fbdd016e213aece9a8b8"},
]
certifi = [
{file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"},
{file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"},
]
charset-normalizer = [
{file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"},
{file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"},
]
click = [
{file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
{file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"},
]
flake8 = [
{file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"},
{file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"},
]
flask = [
{file = "Flask-1.1.4-py2.py3-none-any.whl", hash = "sha256:c34f04500f2cbbea882b1acb02002ad6fe6b7ffa64a6164577995657f50aed22"},
{file = "Flask-1.1.4.tar.gz", hash = "sha256:0fbeb6180d383a9186d0d6ed954e0042ad9f18e0e8de088b2b419d526927d196"},
]
flask-cors = [
{file = "Flask-Cors-3.0.10.tar.gz", hash = "sha256:b60839393f3b84a0f3746f6cdca56c1ad7426aa738b70d6c61375857823181de"},
{file = "Flask_Cors-3.0.10-py2.py3-none-any.whl", hash = "sha256:74efc975af1194fc7891ff5cd85b0f7478be4f7f59fe158102e91abb72bb4438"},
]
flask-httpauth = [
{file = "Flask-HTTPAuth-4.5.0.tar.gz", hash = "sha256:395040fda2854df800d15e84bc4a81a5f32f1d4a5e91eee554936f36f330aa29"},
{file = "Flask_HTTPAuth-4.5.0-py3-none-any.whl", hash = "sha256:e16067ba3378ea366edf8de4b9d55f38c0a0cbddefcc0f777a54b3fce1d99392"},
]
flask-sqlalchemy = [
{file = "Flask-SQLAlchemy-2.4.4.tar.gz", hash = "sha256:bfc7150eaf809b1c283879302f04c42791136060c6eeb12c0c6674fb1291fae5"},
{file = "Flask_SQLAlchemy-2.4.4-py2.py3-none-any.whl", hash = "sha256:05b31d2034dd3f2a685cbbae4cfc4ed906b2a733cff7964ada450fd5e462b84e"},
]
idna = [
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
{file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
]
itsdangerous = [
{file = "itsdangerous-1.1.0-py2.py3-none-any.whl", hash = "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"},
{file = "itsdangerous-1.1.0.tar.gz", hash = "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19"},
]
jinja2 = [
{file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"},
{file = "Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"},
]
markupsafe = [
{file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"},
{file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"},
{file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"},
{file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"},
{file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"},
{file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"},
{file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"},
{file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"},
{file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"},
{file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"},
{file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"},
{file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"},
{file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"},
{file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"},
{file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"},
{file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"},
{file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"},
{file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"},
{file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"},
{file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"},
{file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"},
{file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"},
{file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"},
{file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"},
{file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"},
]
mccabe = [
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
]
parameterized = [
{file = "parameterized-0.8.1-py2.py3-none-any.whl", hash = "sha256:9cbb0b69a03e8695d68b3399a8a5825200976536fe1cb79db60ed6a4c8c9efe9"},
{file = "parameterized-0.8.1.tar.gz", hash = "sha256:41bbff37d6186430f77f900d777e5bb6a24928a1c46fb1de692f8b52b8833b5c"},
]
pillow = [
{file = "Pillow-9.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:af79d3fde1fc2e33561166d62e3b63f0cc3e47b5a3a2e5fea40d4917754734ea"},
{file = "Pillow-9.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:55dd1cf09a1fd7c7b78425967aacae9b0d70125f7d3ab973fadc7b5abc3de652"},
{file = "Pillow-9.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66822d01e82506a19407d1afc104c3fcea3b81d5eb11485e593ad6b8492f995a"},
{file = "Pillow-9.1.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5eaf3b42df2bcda61c53a742ee2c6e63f777d0e085bbc6b2ab7ed57deb13db7"},
{file = "Pillow-9.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01ce45deec9df310cbbee11104bae1a2a43308dd9c317f99235b6d3080ddd66e"},
{file = "Pillow-9.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:aea7ce61328e15943d7b9eaca87e81f7c62ff90f669116f857262e9da4057ba3"},
{file = "Pillow-9.1.0-cp310-cp310-win32.whl", hash = "sha256:7a053bd4d65a3294b153bdd7724dce864a1d548416a5ef61f6d03bf149205160"},
{file = "Pillow-9.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:97bda660702a856c2c9e12ec26fc6d187631ddfd896ff685814ab21ef0597033"},
{file = "Pillow-9.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:21dee8466b42912335151d24c1665fcf44dc2ee47e021d233a40c3ca5adae59c"},
{file = "Pillow-9.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b6d4050b208c8ff886fd3db6690bf04f9a48749d78b41b7a5bf24c236ab0165"},
{file = "Pillow-9.1.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5cfca31ab4c13552a0f354c87fbd7f162a4fafd25e6b521bba93a57fe6a3700a"},
{file = "Pillow-9.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed742214068efa95e9844c2d9129e209ed63f61baa4d54dbf4cf8b5e2d30ccf2"},
{file = "Pillow-9.1.0-cp37-cp37m-win32.whl", hash = "sha256:c9efef876c21788366ea1f50ecb39d5d6f65febe25ad1d4c0b8dff98843ac244"},
{file = "Pillow-9.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:de344bcf6e2463bb25179d74d6e7989e375f906bcec8cb86edb8b12acbc7dfef"},
{file = "Pillow-9.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:17869489de2fce6c36690a0c721bd3db176194af5f39249c1ac56d0bb0fcc512"},
{file = "Pillow-9.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:25023a6209a4d7c42154073144608c9a71d3512b648a2f5d4465182cb93d3477"},
{file = "Pillow-9.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8782189c796eff29dbb37dd87afa4ad4d40fc90b2742704f94812851b725964b"},
{file = "Pillow-9.1.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:463acf531f5d0925ca55904fa668bb3461c3ef6bc779e1d6d8a488092bdee378"},
{file = "Pillow-9.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f42364485bfdab19c1373b5cd62f7c5ab7cc052e19644862ec8f15bb8af289e"},
{file = "Pillow-9.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3fddcdb619ba04491e8f771636583a7cc5a5051cd193ff1aa1ee8616d2a692c5"},
{file = "Pillow-9.1.0-cp38-cp38-win32.whl", hash = "sha256:4fe29a070de394e449fd88ebe1624d1e2d7ddeed4c12e0b31624561b58948d9a"},
{file = "Pillow-9.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:c24f718f9dd73bb2b31a6201e6db5ea4a61fdd1d1c200f43ee585fc6dcd21b34"},
{file = "Pillow-9.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fb89397013cf302f282f0fc998bb7abf11d49dcff72c8ecb320f76ea6e2c5717"},
{file = "Pillow-9.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c870193cce4b76713a2b29be5d8327c8ccbe0d4a49bc22968aa1e680930f5581"},
{file = "Pillow-9.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69e5ddc609230d4408277af135c5b5c8fe7a54b2bdb8ad7c5100b86b3aab04c6"},
{file = "Pillow-9.1.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35be4a9f65441d9982240e6966c1eaa1c654c4e5e931eaf580130409e31804d4"},
{file = "Pillow-9.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82283af99c1c3a5ba1da44c67296d5aad19f11c535b551a5ae55328a317ce331"},
{file = "Pillow-9.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a325ac71914c5c043fa50441b36606e64a10cd262de12f7a179620f579752ff8"},
{file = "Pillow-9.1.0-cp39-cp39-win32.whl", hash = "sha256:a598d8830f6ef5501002ae85c7dbfcd9c27cc4efc02a1989369303ba85573e58"},
{file = "Pillow-9.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0c51cb9edac8a5abd069fd0758ac0a8bfe52c261ee0e330f363548aca6893595"},
{file = "Pillow-9.1.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a336a4f74baf67e26f3acc4d61c913e378e931817cd1e2ef4dfb79d3e051b481"},
{file = "Pillow-9.1.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb1b89b11256b5b6cad5e7593f9061ac4624f7651f7a8eb4dfa37caa1dfaa4d0"},
{file = "Pillow-9.1.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:255c9d69754a4c90b0ee484967fc8818c7ff8311c6dddcc43a4340e10cd1636a"},
{file = "Pillow-9.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5a3ecc026ea0e14d0ad7cd990ea7f48bfcb3eb4271034657dc9d06933c6629a7"},
{file = "Pillow-9.1.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5b0ff59785d93b3437c3703e3c64c178aabada51dea2a7f2c5eccf1bcf565a3"},
{file = "Pillow-9.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7110ec1701b0bf8df569a7592a196c9d07c764a0a74f65471ea56816f10e2c8"},
{file = "Pillow-9.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:8d79c6f468215d1a8415aa53d9868a6b40c4682165b8cb62a221b1baa47db458"},
{file = "Pillow-9.1.0.tar.gz", hash = "sha256:f401ed2bbb155e1ade150ccc63db1a4f6c1909d3d378f7d1235a44e90d75fb97"},
]
psycopg2-binary = [
{file = "psycopg2-binary-2.9.3.tar.gz", hash = "sha256:761df5313dc15da1502b21453642d7599d26be88bff659382f8f9747c7ebea4e"},
{file = "psycopg2_binary-2.9.3-cp310-cp310-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:539b28661b71da7c0e428692438efbcd048ca21ea81af618d845e06ebfd29478"},
{file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e82d38390a03da28c7985b394ec3f56873174e2c88130e6966cb1c946508e65"},
{file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57804fc02ca3ce0dbfbef35c4b3a4a774da66d66ea20f4bda601294ad2ea6092"},
{file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:083a55275f09a62b8ca4902dd11f4b33075b743cf0d360419e2051a8a5d5ff76"},
{file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_24_ppc64le.whl", hash = "sha256:0a29729145aaaf1ad8bafe663131890e2111f13416b60e460dae0a96af5905c9"},
{file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a79d622f5206d695d7824cbf609a4f5b88ea6d6dab5f7c147fc6d333a8787e4"},
{file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:090f3348c0ab2cceb6dfbe6bf721ef61262ddf518cd6cc6ecc7d334996d64efa"},
{file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a9e1f75f96ea388fbcef36c70640c4efbe4650658f3d6a2967b4cc70e907352e"},
{file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c3ae8e75eb7160851e59adc77b3a19a976e50622e44fd4fd47b8b18208189d42"},
{file = "psycopg2_binary-2.9.3-cp310-cp310-win32.whl", hash = "sha256:7b1e9b80afca7b7a386ef087db614faebbf8839b7f4db5eb107d0f1a53225029"},
{file = "psycopg2_binary-2.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:8b344adbb9a862de0c635f4f0425b7958bf5a4b927c8594e6e8d261775796d53"},
{file = "psycopg2_binary-2.9.3-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:e847774f8ffd5b398a75bc1c18fbb56564cda3d629fe68fd81971fece2d3c67e"},
{file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68641a34023d306be959101b345732360fc2ea4938982309b786f7be1b43a4a1"},
{file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3303f8807f342641851578ee7ed1f3efc9802d00a6f83c101d21c608cb864460"},
{file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_24_aarch64.whl", hash = "sha256:e3699852e22aa68c10de06524a3721ade969abf382da95884e6a10ff798f9281"},
{file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_24_ppc64le.whl", hash = "sha256:526ea0378246d9b080148f2d6681229f4b5964543c170dd10bf4faaab6e0d27f"},
{file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:b1c8068513f5b158cf7e29c43a77eb34b407db29aca749d3eb9293ee0d3103ca"},
{file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:15803fa813ea05bef089fa78835118b5434204f3a17cb9f1e5dbfd0b9deea5af"},
{file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:152f09f57417b831418304c7f30d727dc83a12761627bb826951692cc6491e57"},
{file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:404224e5fef3b193f892abdbf8961ce20e0b6642886cfe1fe1923f41aaa75c9d"},
{file = "psycopg2_binary-2.9.3-cp36-cp36m-win32.whl", hash = "sha256:1f6b813106a3abdf7b03640d36e24669234120c72e91d5cbaeb87c5f7c36c65b"},
{file = "psycopg2_binary-2.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:2d872e3c9d5d075a2e104540965a1cf898b52274a5923936e5bfddb58c59c7c2"},
{file = "psycopg2_binary-2.9.3-cp37-cp37m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:10bb90fb4d523a2aa67773d4ff2b833ec00857f5912bafcfd5f5414e45280fb1"},
{file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:874a52ecab70af13e899f7847b3e074eeb16ebac5615665db33bce8a1009cf33"},
{file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a29b3ca4ec9defec6d42bf5feb36bb5817ba3c0230dd83b4edf4bf02684cd0ae"},
{file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:12b11322ea00ad8db8c46f18b7dfc47ae215e4df55b46c67a94b4effbaec7094"},
{file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_24_ppc64le.whl", hash = "sha256:53293533fcbb94c202b7c800a12c873cfe24599656b341f56e71dd2b557be063"},
{file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c381bda330ddf2fccbafab789d83ebc6c53db126e4383e73794c74eedce855ef"},
{file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d29409b625a143649d03d0fd7b57e4b92e0ecad9726ba682244b73be91d2fdb"},
{file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:183a517a3a63503f70f808b58bfbf962f23d73b6dccddae5aa56152ef2bcb232"},
{file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:15c4e4cfa45f5a60599d9cec5f46cd7b1b29d86a6390ec23e8eebaae84e64554"},
{file = "psycopg2_binary-2.9.3-cp37-cp37m-win32.whl", hash = "sha256:adf20d9a67e0b6393eac162eb81fb10bc9130a80540f4df7e7355c2dd4af9fba"},
{file = "psycopg2_binary-2.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:2f9ffd643bc7349eeb664eba8864d9e01f057880f510e4681ba40a6532f93c71"},
{file = "psycopg2_binary-2.9.3-cp38-cp38-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:def68d7c21984b0f8218e8a15d514f714d96904265164f75f8d3a70f9c295667"},
{file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dffc08ca91c9ac09008870c9eb77b00a46b3378719584059c034b8945e26b272"},
{file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:280b0bb5cbfe8039205c7981cceb006156a675362a00fe29b16fbc264e242834"},
{file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:af9813db73395fb1fc211bac696faea4ca9ef53f32dc0cfa27e4e7cf766dcf24"},
{file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_24_ppc64le.whl", hash = "sha256:63638d875be8c2784cfc952c9ac34e2b50e43f9f0a0660b65e2a87d656b3116c"},
{file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ffb7a888a047696e7f8240d649b43fb3644f14f0ee229077e7f6b9f9081635bd"},
{file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0c9d5450c566c80c396b7402895c4369a410cab5a82707b11aee1e624da7d004"},
{file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:d1c1b569ecafe3a69380a94e6ae09a4789bbb23666f3d3a08d06bbd2451f5ef1"},
{file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8fc53f9af09426a61db9ba357865c77f26076d48669f2e1bb24d85a22fb52307"},
{file = "psycopg2_binary-2.9.3-cp38-cp38-win32.whl", hash = "sha256:6472a178e291b59e7f16ab49ec8b4f3bdada0a879c68d3817ff0963e722a82ce"},
{file = "psycopg2_binary-2.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:35168209c9d51b145e459e05c31a9eaeffa9a6b0fd61689b48e07464ffd1a83e"},
{file = "psycopg2_binary-2.9.3-cp39-cp39-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:47133f3f872faf28c1e87d4357220e809dfd3fa7c64295a4a148bcd1e6e34ec9"},
{file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91920527dea30175cc02a1099f331aa8c1ba39bf8b7762b7b56cbf54bc5cce42"},
{file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887dd9aac71765ac0d0bac1d0d4b4f2c99d5f5c1382d8b770404f0f3d0ce8a39"},
{file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:1f14c8b0942714eb3c74e1e71700cbbcb415acbc311c730370e70c578a44a25c"},
{file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_24_ppc64le.whl", hash = "sha256:7af0dd86ddb2f8af5da57a976d27cd2cd15510518d582b478fbb2292428710b4"},
{file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:93cd1967a18aa0edd4b95b1dfd554cf15af657cb606280996d393dadc88c3c35"},
{file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bda845b664bb6c91446ca9609fc69f7db6c334ec5e4adc87571c34e4f47b7ddb"},
{file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:01310cf4cf26db9aea5158c217caa92d291f0500051a6469ac52166e1a16f5b7"},
{file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:99485cab9ba0fa9b84f1f9e1fef106f44a46ef6afdeec8885e0b88d0772b49e8"},
{file = "psycopg2_binary-2.9.3-cp39-cp39-win32.whl", hash = "sha256:46f0e0a6b5fa5851bbd9ab1bc805eef362d3a230fbdfbc209f4a236d0a7a990d"},
{file = "psycopg2_binary-2.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:accfe7e982411da3178ec690baaceaad3c278652998b2c45828aaac66cd8285f"},
]
pycodestyle = [
{file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"},
{file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"},
]
pyflakes = [
{file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"},
{file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"},
]
python-dateutil = [
{file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
{file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
]
pyyaml = [
{file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"},
{file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"},
{file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"},
{file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"},
{file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"},
{file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"},
{file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"},
{file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"},
{file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"},
{file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"},
{file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"},
{file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"},
{file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"},
{file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"},
{file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"},
{file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"},
{file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"},
{file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"},
{file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"},
{file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"},
{file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"},
{file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"},
{file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"},
{file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"},
{file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"},
{file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"},
{file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"},
{file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"},
{file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"},
]
requests = [
{file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"},
{file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"},
]
six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
sqlalchemy = [
{file = "SQLAlchemy-1.3.24-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:87a2725ad7d41cd7376373c15fd8bf674e9c33ca56d0b8036add2d634dba372e"},
{file = "SQLAlchemy-1.3.24-cp27-cp27m-win32.whl", hash = "sha256:f597a243b8550a3a0b15122b14e49d8a7e622ba1c9d29776af741f1845478d79"},
{file = "SQLAlchemy-1.3.24-cp27-cp27m-win_amd64.whl", hash = "sha256:fc4cddb0b474b12ed7bdce6be1b9edc65352e8ce66bc10ff8cbbfb3d4047dbf4"},
{file = "SQLAlchemy-1.3.24-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:f1149d6e5c49d069163e58a3196865e4321bad1803d7886e07d8710de392c548"},
{file = "SQLAlchemy-1.3.24-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:14f0eb5db872c231b20c18b1e5806352723a3a89fb4254af3b3e14f22eaaec75"},
{file = "SQLAlchemy-1.3.24-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:e98d09f487267f1e8d1179bf3b9d7709b30a916491997137dd24d6ae44d18d79"},
{file = "SQLAlchemy-1.3.24-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:fc1f2a5a5963e2e73bac4926bdaf7790c4d7d77e8fc0590817880e22dd9d0b8b"},
{file = "SQLAlchemy-1.3.24-cp35-cp35m-win32.whl", hash = "sha256:f3c5c52f7cb8b84bfaaf22d82cb9e6e9a8297f7c2ed14d806a0f5e4d22e83fb7"},
{file = "SQLAlchemy-1.3.24-cp35-cp35m-win_amd64.whl", hash = "sha256:0352db1befcbed2f9282e72843f1963860bf0e0472a4fa5cf8ee084318e0e6ab"},
{file = "SQLAlchemy-1.3.24-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:2ed6343b625b16bcb63c5b10523fd15ed8934e1ed0f772c534985e9f5e73d894"},
{file = "SQLAlchemy-1.3.24-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:34fcec18f6e4b24b4a5f6185205a04f1eab1e56f8f1d028a2a03694ebcc2ddd4"},
{file = "SQLAlchemy-1.3.24-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:e47e257ba5934550d7235665eee6c911dc7178419b614ba9e1fbb1ce6325b14f"},
{file = "SQLAlchemy-1.3.24-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:816de75418ea0953b5eb7b8a74933ee5a46719491cd2b16f718afc4b291a9658"},
{file = "SQLAlchemy-1.3.24-cp36-cp36m-win32.whl", hash = "sha256:26155ea7a243cbf23287f390dba13d7927ffa1586d3208e0e8d615d0c506f996"},
{file = "SQLAlchemy-1.3.24-cp36-cp36m-win_amd64.whl", hash = "sha256:f03bd97650d2e42710fbe4cf8a59fae657f191df851fc9fc683ecef10746a375"},
{file = "SQLAlchemy-1.3.24-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:a006d05d9aa052657ee3e4dc92544faae5fcbaafc6128217310945610d862d39"},
{file = "SQLAlchemy-1.3.24-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1e2f89d2e5e3c7a88e25a3b0e43626dba8db2aa700253023b82e630d12b37109"},
{file = "SQLAlchemy-1.3.24-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:0d5d862b1cfbec5028ce1ecac06a3b42bc7703eb80e4b53fceb2738724311443"},
{file = "SQLAlchemy-1.3.24-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:0172423a27fbcae3751ef016663b72e1a516777de324a76e30efa170dbd3dd2d"},
{file = "SQLAlchemy-1.3.24-cp37-cp37m-win32.whl", hash = "sha256:d37843fb8df90376e9e91336724d78a32b988d3d20ab6656da4eb8ee3a45b63c"},
{file = "SQLAlchemy-1.3.24-cp37-cp37m-win_amd64.whl", hash = "sha256:c10ff6112d119f82b1618b6dc28126798481b9355d8748b64b9b55051eb4f01b"},
{file = "SQLAlchemy-1.3.24-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:861e459b0e97673af6cc5e7f597035c2e3acdfb2608132665406cded25ba64c7"},
{file = "SQLAlchemy-1.3.24-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5de2464c254380d8a6c20a2746614d5a436260be1507491442cf1088e59430d2"},
{file = "SQLAlchemy-1.3.24-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d375d8ccd3cebae8d90270f7aa8532fe05908f79e78ae489068f3b4eee5994e8"},
{file = "SQLAlchemy-1.3.24-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:014ea143572fee1c18322b7908140ad23b3994036ef4c0d630110faf942652f8"},
{file = "SQLAlchemy-1.3.24-cp38-cp38-win32.whl", hash = "sha256:6607ae6cd3a07f8a4c3198ffbf256c261661965742e2b5265a77cd5c679c9bba"},
{file = "SQLAlchemy-1.3.24-cp38-cp38-win_amd64.whl", hash = "sha256:fcb251305fa24a490b6a9ee2180e5f8252915fb778d3dafc70f9cc3f863827b9"},
{file = "SQLAlchemy-1.3.24-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:01aa5f803db724447c1d423ed583e42bf5264c597fd55e4add4301f163b0be48"},
{file = "SQLAlchemy-1.3.24-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4d0e3515ef98aa4f0dc289ff2eebb0ece6260bbf37c2ea2022aad63797eacf60"},
{file = "SQLAlchemy-1.3.24-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:bce28277f308db43a6b4965734366f533b3ff009571ec7ffa583cb77539b84d6"},
{file = "SQLAlchemy-1.3.24-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:8110e6c414d3efc574543109ee618fe2c1f96fa31833a1ff36cc34e968c4f233"},
{file = "SQLAlchemy-1.3.24-cp39-cp39-win32.whl", hash = "sha256:ee5f5188edb20a29c1cc4a039b074fdc5575337c9a68f3063449ab47757bb064"},
{file = "SQLAlchemy-1.3.24-cp39-cp39-win_amd64.whl", hash = "sha256:09083c2487ca3c0865dc588e07aeaa25416da3d95f7482c07e92f47e080aa17b"},
{file = "SQLAlchemy-1.3.24.tar.gz", hash = "sha256:ebbb777cbf9312359b897bf81ba00dae0f5cb69fba2a18265dcc18a6f5ef7519"},
]
toml = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
urllib3 = [
{file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"},
{file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"},
]
waitress = [
{file = "waitress-1.4.4-py2.py3-none-any.whl", hash = "sha256:3d633e78149eb83b60a07dfabb35579c29aac2d24bb803c18b26fb2ab1a584db"},
{file = "waitress-1.4.4.tar.gz", hash = "sha256:1bb436508a7487ac6cb097ae7a7fe5413aefca610550baf58f0940e51ecfb261"},
]
werkzeug = [
{file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"},
{file = "Werkzeug-1.0.1.tar.gz", hash = "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"},
]
wtforms = [
{file = "WTForms-2.3.3-py2.py3-none-any.whl", hash = "sha256:7b504fc724d0d1d4d5d5c114e778ec88c37ea53144683e084215eed5155ada4c"},
{file = "WTForms-2.3.3.tar.gz", hash = "sha256:81195de0ac94fbc8368abbaf9197b88c4f3ffd6c2719b5bf5fc9da744f3d829c"},
]
yapf = [
{file = "yapf-0.32.0-py2.py3-none-any.whl", hash = "sha256:8fea849025584e486fd06d6ba2bed717f396080fd3cc236ba10cb97c4c51cf32"},
{file = "yapf-0.32.0.tar.gz", hash = "sha256:a3f5085d37ef7e3e004c4ba9f9b3e40c54ff1901cd111f05145ae313a7c67d1b"},
]

47
pyproject.toml Normal file
View File

@ -0,0 +1,47 @@
[tool.poetry]
name = 'synapse-captcha'
version = '0.7.2.3'
readme = 'README.md'
repository = 'https://codeberg.org/deathrow/synapse-captcha'
packages = [{include = 'matrix_registration'}]
include = [
'matrix_registration/templates/*.html',
'matrix_registration/static/css/*.css',
'matrix_registration/static/images/*.jpg',
'matrix_registration/static/images/*.png',
'matrix_registration/static/images/*.ico'
]
[tool.poetry.dependencies]
python = '^3.8'
appdirs = '~=1.4.3'
captcha = '^0.4'
Flask = '~=1.1'
Flask-SQLAlchemy = '~=2.4.1'
Flask-Cors = '~=3.0.7'
Flask-HTTPAuth = '^4.5.0'
python-dateutil = '~=2.8.1'
PyYAML = '~=5.1'
requests = '^2.27.1'
SQLAlchemy = '~=1.3.13'
waitress = '~=1.4.4'
WTForms = '~=2.1'
MarkupSafe = '2.0.1'
psycopg2-binary = "^2.9.3"
[tool.poetry.dev-dependencies]
parameterized = '^0.8.1'
flake8 = '^4.0.1'
yapf = "^0.32.0"
toml = "^0.10.2"
[tool.poetry.scripts]
matrix-registration = 'matrix_registration.app:cli'
[tool.poetry.extras]
postgres = ['psycopg2-binary>=2.8.4']
[build-system]
requires = ['poetry-core>=1.0.0']
build-backend = 'poetry.core.masonry.api'

0
tests/__init__.py Normal file
View File

7
tests/context.py Normal file
View File

@ -0,0 +1,7 @@
import os
import sys
sys.path.insert(0,
os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
import matrix_registration

389
tests/test_registration.py Normal file
View File

@ -0,0 +1,389 @@
# -*- coding: utf-8 -*-
# Standard library imports...
import hashlib
import hmac
import logging
import logging.config
import json
import os
import yaml
import random
import re
from requests import exceptions
import string
import sys
import time
import unittest
from unittest.mock import patch
from urllib.parse import urlparse
# Third-party imports...
from parameterized import parameterized
from dateutil import parser
# Local imports...
try:
from .context import matrix_registration
except ModuleNotFoundError:
from context import matrix_registration
from matrix_registration.config import Config
from matrix_registration.app import create_app
from matrix_registration.captcha import db
logger = logging.getLogger(__name__)
LOGGING = {
"version": 1,
"root": {
"level": "NOTSET",
"handlers": ["console"]
},
"formatters": {
"precise": {
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "NOTSET",
"formatter": "precise",
"stream": "ext://sys.stdout"
}
}
}
GOOD_CONFIG = {
'server_location': 'https://righths.org',
'shared_secret': 'coolsharesecret',
'admin_secret': 'coolpassword',
'db': 'sqlite:///%s/tests/db.sqlite' % (os.getcwd(), ),
'port': 5000,
'password': {
'min_length': 8
},
'logging': LOGGING
}
BAD_CONFIG1 = dict( # wrong matrix server location -> 500
GOOD_CONFIG.items(),
server_location='https://wronghs.org',
)
BAD_CONFIG2 = dict( # wrong admin secret password -> 401
GOOD_CONFIG.items(),
admin_secret='wrongpassword',
)
BAD_CONFIG3 = dict( # wrong matrix shared password -> 500
GOOD_CONFIG.items(),
shared_secret='wrongsecret',
)
usernames = []
nonces = []
logging.config.dictConfig(LOGGING)
def mock_new_user(username):
access_token = ''.join(
random.choices(string.ascii_lowercase + string.digits, k=256))
device_id = ''.join(random.choices(string.ascii_uppercase, k=8))
home_server = matrix_registration.config.config.server_location
username = username.rsplit(":")[0].split("@")[-1]
user_id = "@{}:{}".format(username, home_server)
usernames.append(username)
user = {
'access_token': access_token,
'device_id': device_id,
'home_server': home_server,
'user_id': user_id
}
return user
def mocked__get_nonce(server_location):
nonce = ''.join(
random.choices(string.ascii_lowercase + string.digits, k=129))
nonces.append(nonce)
return nonce
def mocked_requests_post(*args, **kwargs):
class MockResponse:
def __init__(self, json_data, status_code):
self.json_data = json_data
self.status_code = status_code
def json(self):
return self.json_data
def raise_for_status(self):
if self.status_code == 200:
return self.status_code
else:
raise exceptions.HTTPError(response=self)
# print(args[0])
# print(matrix_registration.config.config.server_location)
domain = urlparse(GOOD_CONFIG['server_location']).hostname
re_mxid = r"^@?[a-zA-Z_\-=\.\/0-9]+(:" + \
re.escape(domain) + \
r")?$"
location = '_synapse/admin/v1/register'
if args[0] == '%s/%s' % (GOOD_CONFIG['server_location'], location):
if kwargs:
req = kwargs['json']
if not req['nonce'] in nonces:
return MockResponse(
{"'errcode': 'M_UNKOWN", "'error': 'unrecognised nonce'"},
400)
mac = hmac.new(
key=str.encode(GOOD_CONFIG['shared_secret']),
digestmod=hashlib.sha1,
)
mac.update(req['nonce'].encode())
mac.update(b'\x00')
mac.update(req['username'].encode())
mac.update(b'\x00')
mac.update(req['password'].encode())
mac.update(b'\x00')
mac.update(b'admin' if req['admin'] else b'notadmin')
mac = mac.hexdigest()
if not re.search(re_mxid, req['username']):
return MockResponse(
{
"'errcode': 'M_INVALID_USERNAME",
"'error': 'User ID can only contain" +
"characters a-z, 0-9, or '=_-./'"
}, 400)
if req['username'].rsplit(":")[0].split("@")[-1] in usernames:
return MockResponse(
{
'errcode': 'M_USER_IN_USE',
'error': 'User ID already taken.'
}, 400)
if req['mac'] != mac:
return MockResponse(
{
'errcode': 'M_UNKNOWN',
'error': 'HMAC incorrect'
}, 403)
return MockResponse(mock_new_user(req['username']), 200)
return MockResponse(None, 404)
class TokensTest(unittest.TestCase):
def setUp(self):
matrix_registration.config.config = Config(GOOD_CONFIG)
app = create_app(testing=True)
with app.app_context():
app.config.from_mapping(
SQLALCHEMY_DATABASE_URI=matrix_registration.config.config.db,
SQLALCHEMY_TRACK_MODIFICATIONS=False)
db.init_app(app)
db.create_all()
self.app = app
def tearDown(self):
os.remove(matrix_registration.config.config.db[10:])
def test_captcha_valid(self):
with self.app.app_context():
test_captcha_gen = matrix_registration.captcha.CaptchaGenerator()
test_captcha = test_captcha_gen.generate()
# validate that the captcha is correct
self.assertTrue(
test_captcha_gen.validate(test_captcha['captcha_answer'],
test_captcha['captcha_token']))
# captcha can only be used once
self.assertFalse(
test_captcha_gen.validate(test_captcha['captcha_answer'],
test_captcha['captcha_token']))
def test_captcha_empty(self):
with self.app.app_context():
test_captcha_gen = matrix_registration.captcha.CaptchaGenerator()
# no captcha should exist at this point
self.assertFalse(test_captcha_gen.validate("", ""))
test_captcha = test_captcha_gen.generate()
# no empty captcha should have been created
self.assertFalse(test_captcha_gen.validate("", ""))
def test_captcha_clean(self):
with self.app.app_context():
test_captcha_gen = matrix_registration.captcha.CaptchaGenerator()
valid_captcha = test_captcha_gen.generate()
# validate a wrong captcha
self.assertFalse(
test_captcha_gen.validate("WRONG",
valid_captcha['captcha_token']))
# valid captcha should be removed when it was wrong
self.assertFalse(
test_captcha_gen.validate(valid_captcha['captcha_answer'],
valid_captcha['captcha_token']))
timeout = matrix_registration.captcha.CAPTCHA_TIMEOUT
matrix_registration.captcha.CAPTCHA_TIMEOUT = 0
try:
valid_captcha = test_captcha_gen.generate()
time.sleep(1)
# captcha older than the timeout value should not be valid
self.assertFalse(
test_captcha_gen.validate(valid_captcha['captcha_answer'],
valid_captcha['captcha_token']))
finally:
matrix_registration.captcha.CAPTCHA_TIMEOUT = timeout
class ApiTest(unittest.TestCase):
def setUp(self):
matrix_registration.config.config = Config(GOOD_CONFIG)
app = create_app(testing=True)
with app.app_context():
app.config.from_mapping(
SQLALCHEMY_DATABASE_URI=matrix_registration.config.config.db,
SQLALCHEMY_TRACK_MODIFICATIONS=False)
db.init_app(app)
db.create_all()
self.client = app.test_client()
self.app = app
def tearDown(self):
os.remove(matrix_registration.config.config.db[10:])
@parameterized.expand(
[['test1', 'test1234', 'test1234', True, 200],
[None, 'test1234', 'test1234', True, 400],
['test2', None, 'test1234', True, 400],
['test3', 'test1234', None, True, 400],
['test4', 'test1234', 'test1234', False, 400],
['@test5:matrix.org', 'test1234', 'test1234', True, 200],
['@test6:wronghs.org', 'test1234', 'test1234', True, 400],
['test7', 'test1234', 'tet1234', True, 400],
['teüst8', 'test1234', 'test1234', True, 400],
['@test9@matrix.org', 'test1234', 'test1234', True, 400],
['test11@matrix.org', 'test1234', 'test1234', True, 400],
['', 'test1234', 'test1234', True, 400],
[
''.join(random.choices(string.ascii_uppercase, k=256)),
'test1234', 'test1234', True, 400
]])
# check form validators
@patch('matrix_registration.matrix_api._get_nonce',
side_effect=mocked__get_nonce)
@patch('matrix_registration.matrix_api.requests.post',
side_effect=mocked_requests_post)
def test_register(self, username, password, confirm, captcha, status,
mock_get, mock_nonce):
matrix_registration.config.config = Config(GOOD_CONFIG)
with self.app.app_context():
matrix_registration.captcha.captcha = matrix_registration.captcha.CaptchaGenerator(
)
test_captcha = matrix_registration.captcha.captcha.generate()
# replace matrix with in config set hs
domain = urlparse(
matrix_registration.config.config.server_location).hostname
if username:
username = username.replace("matrix.org", domain)
if not captcha:
test_captcha['captcha_answer'] = ""
rv = self.client.post(
'/register',
data=dict(username=username,
password=password,
confirm=confirm,
captcha_answer=test_captcha['captcha_answer'],
captcha_token=test_captcha['captcha_token']))
if rv.status_code == 200:
account_data = json.loads(
rv.data.decode('utf8').replace("'", '"'))
# print(account_data)
self.assertEqual(rv.status_code, status)
@patch('matrix_registration.matrix_api._get_nonce',
side_effect=mocked__get_nonce)
@patch('matrix_registration.matrix_api.requests.post',
side_effect=mocked_requests_post)
def test_register_wrong_hs(self, mock_get, mock_nonce):
matrix_registration.config.config = Config(BAD_CONFIG1)
with self.app.app_context():
matrix_registration.captcha.captcha = matrix_registration.captcha.CaptchaGenerator(
)
test_captcha = matrix_registration.captcha.captcha.generate()
rv = self.client.post(
'/register',
data=dict(username='username',
password='password',
confirm='password',
captcha_answer=test_captcha['captcha_answer'],
captcha_token=test_captcha['captcha_token']))
self.assertEqual(rv.status_code, 500)
@patch('matrix_registration.matrix_api._get_nonce',
side_effect=mocked__get_nonce)
@patch('matrix_registration.matrix_api.requests.post',
side_effect=mocked_requests_post)
def test_register_wrong_secret(self, mock_get, mock_nonce):
matrix_registration.config.config = Config(BAD_CONFIG3)
with self.app.app_context():
matrix_registration.captcha.captcha = matrix_registration.captcha.CaptchaGenerator(
)
test_captcha = matrix_registration.captcha.captcha.generate()
rv = self.client.post(
'/register',
data=dict(username='username',
password='password',
confirm='password',
captcha_answer=test_captcha['captcha_answer'],
captcha_token=test_captcha['captcha_token']))
self.assertEqual(rv.status_code, 500)
class ConfigTest(unittest.TestCase):
def test_config_update(self):
matrix_registration.config.config = Config(GOOD_CONFIG)
self.assertEqual(matrix_registration.config.config.port,
GOOD_CONFIG['port'])
self.assertEqual(matrix_registration.config.config.server_location,
GOOD_CONFIG['server_location'])
matrix_registration.config.config.update(BAD_CONFIG1)
self.assertEqual(matrix_registration.config.config.port,
BAD_CONFIG1['port'])
self.assertEqual(matrix_registration.config.config.server_location,
BAD_CONFIG1['server_location'])
def test_config_path(self):
# BAD_CONFIG1_path = "x"
good_config_path = "tests/test_config.yaml"
with open(good_config_path, 'w') as outfile:
yaml.dump(GOOD_CONFIG, outfile, default_flow_style=False)
matrix_registration.config.config = Config(good_config_path)
self.assertIsNotNone(matrix_registration.config.config)
os.remove(good_config_path)
# TODO: - tests for /token/<token>
# - a nonce is only valid for 60s
if "logging" in sys.argv:
logging.basicConfig(level=logging.DEBUG)
if __name__ == '__main__':
unittest.main()