Compare commits

...

2 Commits

Author SHA1 Message Date
missytake
fbcf071e89 doveauth-http: deploying HTTP route for account creation 2023-12-01 18:32:45 +01:00
missytake
ccfbb59e17 doveauth: flask app to create accounts via HTTP 2023-12-01 18:05:40 +01:00
7 changed files with 125 additions and 37 deletions

View File

@@ -6,11 +6,14 @@ build-backend = "setuptools.build_meta"
name = "chatmaild"
version = "0.1"
dependencies = [
"aiosmtpd"
"aiosmtpd",
"flask",
"gunicorn",
]
[project.scripts]
doveauth = "chatmaild.doveauth:main"
doveauth-http = "chatmaild.web:main"
filtermail = "chatmaild.filtermail:main"
[tool.pytest.ini_options]

View File

@@ -0,0 +1,10 @@
[Unit]
Description=HTTP endpoint for creating chatmail accounts
[Service]
ExecStart=/usr/local/bin/gunicorn --timeout 60 -b :3691 -w 1 chatmaild.web:main
Restart=always
RestartSec=30
[Install]
WantedBy=multi-user.target

View File

@@ -3,7 +3,6 @@ import os
import time
import sys
import json
import crypt
from socketserver import (
UnixStreamServer,
StreamRequestHandler,
@@ -12,39 +11,7 @@ from socketserver import (
import pwd
from .database import Database
NOCREATE_FILE = "/etc/chatmail-nocreate"
def encrypt_password(password: str):
# https://doc.dovecot.org/configuration_manual/authentication/password_schemes/
passhash = crypt.crypt(password, crypt.METHOD_SHA512)
return "{SHA512-CRYPT}" + passhash
def is_allowed_to_create(user, cleartext_password) -> bool:
"""Return True if user and password are admissable."""
if os.path.exists(NOCREATE_FILE):
logging.warning(f"blocked account creation because {NOCREATE_FILE!r} exists.")
return False
if len(cleartext_password) < 10:
logging.warning("Password needs to be at least 10 characters long")
return False
parts = user.split("@")
if len(parts) != 2:
logging.warning(f"user {user!r} is not a proper e-mail address")
return False
localpart, domain = parts
if domain == "nine.testrun.org":
# nine.testrun.org policy, username has to be exactly nine chars
if len(localpart) != 9:
logging.warning(f"localpart {localpart!r} has not exactly nine chars")
return False
return True
from .util import is_allowed_to_create, encrypt_password, get_mail_domain
def get_user_data(db, user):
@@ -123,8 +90,7 @@ def main():
socket = sys.argv[1]
passwd_entry = pwd.getpwnam(sys.argv[2])
db = Database(sys.argv[3])
with open("/etc/mailname", "r") as fp:
mail_domain = fp.read().strip()
mail_domain = get_mail_domain()
class Handler(StreamRequestHandler):
def handle(self):

View File

@@ -0,0 +1,56 @@
import base64
import random
import logging
import os
import crypt
NOCREATE_FILE = "/etc/chatmail-nocreate"
def is_allowed_to_create(user, cleartext_password) -> bool:
"""Return True if user and password are admissable."""
if os.path.exists(NOCREATE_FILE):
logging.warning(f"blocked account creation because {NOCREATE_FILE!r} exists.")
return False
if len(cleartext_password) < 10:
logging.warning("Password needs to be at least 10 characters long")
return False
parts = user.split("@")
if len(parts) != 2:
logging.warning(f"user {user!r} is not a proper e-mail address")
return False
localpart, domain = parts
if domain == "nine.testrun.org":
# nine.testrun.org policy, username has to be exactly nine chars
if len(localpart) != 9:
logging.warning(f"localpart {localpart!r} has not exactly nine chars")
return False
return True
def gen_password():
with open("/dev/urandom", "rb") as f:
s = f.read(21)
return base64.b64encode(s).decode("ascii")[:12]
def encrypt_password(password: str):
# https://doc.dovecot.org/configuration_manual/authentication/password_schemes/
passhash = crypt.crypt(password, crypt.METHOD_SHA512)
return "{SHA512-CRYPT}" + passhash
def get_mail_domain():
with open("/etc/mailname", "r") as fp:
return fp.read().strip()
def get_valid_email_addr(length=9, chars="2345789acdefghjkmnpqrstuvwxyz"):
localpart = "".join(random.choice(chars) for i in range(length))
mail_domain = get_mail_domain()
return f"{localpart}@{mail_domain}"

View File

@@ -0,0 +1,48 @@
from flask import Flask, jsonify, request
import time
import os
from database import Database
from util import gen_password, get_valid_email_addr, encrypt_password
from doveauth import get_user_data
def create_app_from_db_path(db_path=None):
db = Database(db_path)
return create_app_from_db(db)
def create_app_from_db(db):
app = Flask("chatmaild-http")
app.db = db
@app.route("/", methods=["POST"])
def new_email():
for i in range(10):
addr = get_valid_email_addr()
if not get_user_data(db, addr):
cleartext_password = gen_password()
encrypted_password = encrypt_password(cleartext_password)
q = """INSERT INTO users (addr, password, last_login)
VALUES (?, ?, ?)"""
with db.write_transaction() as conn:
conn.execute(q, (addr, encrypted_password, int(time.time())))
return jsonify(
email=addr,
password=cleartext_password,
)
return jsonify(
type="error",
status_code=409,
reason="all 10 email addresses we tried are taken"
)
return app
def main():
"""(debugging-only!) serve http account creation Web API on localhost"""
db_path = os.getenv("CHATMAIL_DATABASE", "/home/vmail/passdb.sqlite")
app = create_app_from_db_path(db_path)
if __name__ == "__main__":
app.run(debug=True, host="localhost", port=3691)

View File

@@ -46,6 +46,7 @@ def _install_chatmaild() -> None:
for fn in (
"doveauth",
"doveauth-http",
"filtermail",
):
files.put(

View File

@@ -42,6 +42,10 @@ http {
# as directory, then fall back to displaying a 404.
try_files $uri $uri/ =404;
}
location /new_email {
proxy_pass http://localhost:3691/;
}
}
}