This commit is contained in:
Alexandre Aubin 2023-01-30 17:35:08 +01:00
parent 2c8179530f
commit e1b0bcb0b0
3 changed files with 36 additions and 23 deletions

41
app.py
View file

@ -9,7 +9,9 @@ from flask import Flask, jsonify, request
from flask_limiter import Limiter from flask_limiter import Limiter
from flask_limiter.util import get_remote_address from flask_limiter.util import get_remote_address
DOMAIN_REGEX = re.compile(r"^([a-z0-9]{1}([a-z0-9\-]*[a-z0-9])*)(\.[a-z0-9]{1}([a-z0-9\-]*[a-z0-9])*)*(\.[a-z]{1}([a-z0-9\-]*[a-z0-9])*)$") DOMAIN_REGEX = re.compile(
r"^([a-z0-9]{1}([a-z0-9\-]*[a-z0-9])*)(\.[a-z0-9]{1}([a-z0-9\-]*[a-z0-9])*)*(\.[a-z]{1}([a-z0-9\-]*[a-z0-9])*)$"
)
app = Flask(__name__) app = Flask(__name__)
app.config.from_file("config.yml", load=yaml.safe_load) app.config.from_file("config.yml", load=yaml.safe_load)
@ -20,14 +22,20 @@ limiter = Limiter(
storage_uri="memory://", storage_uri="memory://",
) )
assert os.path.isdir(app.config['DB_FOLDER']), "You should create the DB folder declared in the config" assert os.path.isdir(
app.config["DB_FOLDER"]
), "You should create the DB folder declared in the config"
def _validate_domain(domain): def _validate_domain(domain):
if not DOMAIN_REGEX.match(domain): if not DOMAIN_REGEX.match(domain):
return {"error": f"This is not a valid domain: {domain}"}, 400 return {"error": f"This is not a valid domain: {domain}"}, 400
if len(domain.split(".")) != 3 or domain.split(".", 1)[-1] not in app.config["DOMAINS"]: if (
len(domain.split(".")) != 3
or domain.split(".", 1)[-1] not in app.config["DOMAINS"]
):
return {"error": f"This subdomain is not handled by this dynette server."}, 400 return {"error": f"This subdomain is not handled by this dynette server."}, 400
@ -36,19 +44,19 @@ def _is_available(domain):
return not os.path.exists(f"{app.config['DB_FOLDER']}/{domain}.key") return not os.path.exists(f"{app.config['DB_FOLDER']}/{domain}.key")
@app.route('/') @app.route("/")
@limiter.exempt @limiter.exempt
def home(): def home():
return 'Wanna play the dynette?' return "Wanna play the dynette?"
@app.route('/domains') @app.route("/domains")
@limiter.exempt @limiter.exempt
def domains(): def domains():
return jsonify(app.config["DOMAINS"]), 200 return jsonify(app.config["DOMAINS"]), 200
@app.route('/test/<domain>') @app.route("/test/<domain>")
@limiter.limit("3 per minute") @limiter.limit("3 per minute")
def availability(domain): def availability(domain):
@ -62,7 +70,7 @@ def availability(domain):
return {"error": f"Subdomain already taken: {domain}"}, 409 return {"error": f"Subdomain already taken: {domain}"}, 409
@app.route('/key/<key>', methods=['POST']) @app.route("/key/<key>", methods=["POST"])
@limiter.limit("5 per hour") @limiter.limit("5 per hour")
def register(key): def register(key):
@ -97,10 +105,11 @@ def register(key):
return {"error": "Recovery password too long"}, 409 return {"error": "Recovery password too long"}, 409
r_init = recovery_password r_init = recovery_password
recovery_password = bcrypt.hashpw(password=recovery_password.encode(), salt=bcrypt.gensalt(14)) recovery_password = bcrypt.hashpw(
password=recovery_password.encode(), salt=bcrypt.gensalt(14)
)
recovery_password = base64.b64encode(recovery_password).decode() recovery_password = base64.b64encode(recovery_password).decode()
with open(f"{app.config['DB_FOLDER']}/{subdomain}.key", "w") as f: with open(f"{app.config['DB_FOLDER']}/{subdomain}.key", "w") as f:
f.write(key) f.write(key)
@ -110,7 +119,8 @@ def register(key):
return "OK", 201 return "OK", 201
@app.route('/domains/<subdomain>', methods=['DELETE'])
@app.route("/domains/<subdomain>", methods=["DELETE"])
@limiter.limit("5 per hour") @limiter.limit("5 per hour")
def delete_using_recovery_password_or_key(subdomain): def delete_using_recovery_password_or_key(subdomain):
@ -120,8 +130,9 @@ def delete_using_recovery_password_or_key(subdomain):
assert isinstance(data, dict) assert isinstance(data, dict)
recovery_password = data.get("recovery_password") recovery_password = data.get("recovery_password")
key = data.get("key") key = data.get("key")
assert (recovery_password and isinstance(recovery_password, str)) \ assert (recovery_password and isinstance(recovery_password, str)) or (
or (key and isinstance(key, str)) key and isinstance(key, str)
)
if key: if key:
key = base64.b64decode(key).decode() key = base64.b64decode(key).decode()
except Exception: except Exception:
@ -139,7 +150,9 @@ def delete_using_recovery_password_or_key(subdomain):
if not hmac.compare_digest(key, f.read()): if not hmac.compare_digest(key, f.read()):
return "Access denied", 403 return "Access denied", 403
if recovery_password: if recovery_password:
if not os.path.exists(f"{app.config['DB_FOLDER']}/{subdomain}.recovery_password"): if not os.path.exists(
f"{app.config['DB_FOLDER']}/{subdomain}.recovery_password"
):
return "Access denied", 403 return "Access denied", 403
with open(f"{app.config['DB_FOLDER']}/{subdomain}.recovery_password") as f: with open(f"{app.config['DB_FOLDER']}/{subdomain}.recovery_password") as f:
hashed = base64.b64decode(f.read()) hashed = base64.b64decode(f.read())

View file

@ -1,11 +1,11 @@
command = '/var/www/dynette/venv/bin/gunicorn' command = "/var/www/dynette/venv/bin/gunicorn"
pythonpath = '/var/www/dynette' pythonpath = "/var/www/dynette"
workers = 4 workers = 4
user = 'dynette' user = "dynette"
bind = 'unix:/var/www/dynette/sock' bind = "unix:/var/www/dynette/sock"
pid = '/run/gunicorn/dynette-pid' pid = "/run/gunicorn/dynette-pid"
errorlog = '/var/log/dynette/error.log' errorlog = "/var/log/dynette/error.log"
accesslog = '/var/log/dynette/access.log' accesslog = "/var/log/dynette/access.log"
access_log_format = '%({X-Real-IP}i)s %({X-Forwarded-For}i)s %(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"' access_log_format = '%({X-Real-IP}i)s %({X-Forwarded-For}i)s %(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
loglevel = 'warning' loglevel = "warning"
capture_output = True capture_output = True