diff --git a/Pipfile b/Pipfile index ceb6984..d53e65d 100644 --- a/Pipfile +++ b/Pipfile @@ -8,6 +8,7 @@ name = "pypi" [packages] aiosmtpd = "*" python-jata = "*" +asyncbasehttp = "*" [requires] python_version = "3" diff --git a/Pipfile.lock b/Pipfile.lock index 64aded1..a70183f 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "5c16e20a67c73101d465516d657e21c6c1d3f853ae16dcfe782b7f9e8ba139e5" + "sha256": "6b224d3d72187a39e4fba357ffb4b162dc28994bd869c7960fcfd5acbb56c959" }, "pipfile-spec": 6, "requires": { @@ -24,6 +24,14 @@ "index": "pypi", "version": "==1.4.4.post2" }, + "asyncbasehttp": { + "hashes": [ + "sha256:2cea7b61113bb1e69d1c90f32919f004e7f56a7e1e2955e8a6286ce40f9da188", + "sha256:70fb983c4df630a9b63081cb8ef6d2e06beb51c89f42a88da6c9a9c7ebf9fb89" + ], + "index": "pypi", + "version": "==1.0" + }, "atpublic": { "hashes": [ "sha256:0f40433219e124edf115c6c363808ca6f0e1cfa7d160d86b2fb94793086d1294", diff --git a/mail4one/pwhash.py b/mail4one/pwhash.py index d8566c4..107aeaa 100644 --- a/mail4one/pwhash.py +++ b/mail4one/pwhash.py @@ -16,6 +16,8 @@ VERSION = b"\x01" SALT_LEN = 30 KEY_LEN = 64 # This is python default +# len(VERSION) + SALT_LEN + KEY_LEN should be multiple of 5 to avoid base32 padding + def gen_pwhash(password: str) -> str: salt = os.urandom(SALT_LEN) diff --git a/mail4one/server.py b/mail4one/server.py index 05e26c9..af326ca 100644 --- a/mail4one/server.py +++ b/mail4one/server.py @@ -10,6 +10,7 @@ from getpass import getpass from .smtp import create_smtp_server_starttls, create_smtp_server from .pop3 import create_pop_server from .version import VERSION +from .web_config import create_web_config_server from . import config from . import pwhash @@ -98,6 +99,14 @@ async def a_main(cfg: config.Config) -> None: ssl_context=get_tls_context(smtp.tls), ) servers.append(smtp_server) + elif scfg.server_type == "web_config": + web = config.ServerCfg(scfg) + web_server = await create_web_config_server( + host=get_host(web.host), + port=web.port, + ssl_context=get_tls_context(web.tls), + ) + servers.append(web_server) else: logging.error(f"Unknown server {scfg.server_type=}") diff --git a/mail4one/template_web_config.html b/mail4one/template_web_config.html new file mode 100644 index 0000000..a1cde7b --- /dev/null +++ b/mail4one/template_web_config.html @@ -0,0 +1,5 @@ + + + Mail4one Web config +

Hello World

+ diff --git a/mail4one/web_config.py b/mail4one/web_config.py new file mode 100644 index 0000000..37db4bf --- /dev/null +++ b/mail4one/web_config.py @@ -0,0 +1,85 @@ +import asyncio +from asyncbasehttp import request_handler, Request, Response, RequestHandler +from typing import Optional, Tuple +import ssl +import logging +from pprint import pprint +import http +from base64 import b64decode +from .pwhash import gen_pwhash, parse_hash, PWInfo, check_pass +import pkgutil + +def get_template() -> bytes: + if data:= pkgutil.get_data('mail4one', 'template_web_config.html'): + return data + raise Exception("Failed to get template data from 'template_web_config.html'") + + +def get_dummy_pwinfo() -> PWInfo: + pwhash = gen_pwhash("world") + return parse_hash(pwhash) + + +class WebonfigHandler(RequestHandler): + def __init__(self, username: str, pwinfo: PWInfo): + self.username = username.encode() + self.pwinfo = pwinfo + self.auth_required = True + + def do_auth(self, req: Request) -> Tuple[bool, Optional[Response]]: + + def resp_unauthorized(): + resp = Response.no_body_response(http.HTTPStatus.UNAUTHORIZED) + resp.add_header("WWW-Authenticate", 'Basic realm="Mail4one"') + return resp + + auth_header = req.headers["Authorization"] + + if not auth_header: + return False, resp_unauthorized() + + if not auth_header.startswith("Basic "): + logging.error("Authorization header malformed") + return False, Response.no_body_response(http.HTTPStatus.BAD_REQUEST) + + userpassb64 = auth_header[len("Basic ") :] + try: + userpass = b64decode(userpassb64) + except: + logging.exception("bad request") + return False, Response.no_body_response(http.HTTPStatus.BAD_REQUEST) + + try: + user, passwd = userpass.split(b":") + except: + logging.exception("bad request") + return False, Response.no_body_response(http.HTTPStatus.BAD_REQUEST) + + if user == self.username and check_pass(passwd.decode(), self.pwinfo): + return True, None + + return False, resp_unauthorized() + + + async def process_request(self, req: Request) -> Response: + if self.auth_required: + ok, resp = self.do_auth(req) + if not ok: + if resp: + return resp + else: + raise Exception("Something went wrong!") + return Response.create_ok_response(get_template()) + + +async def create_web_config_server( + host: str, port: int, ssl_context: Optional[ssl.SSLContext] +) -> asyncio.Server: + logging.info(f"template: {get_template().decode()}") + logging.info(f"Starting Webconfig server {host=}, {port=}, {ssl_context != None=}") + return await asyncio.start_server( + WebonfigHandler("hello", get_dummy_pwinfo()), + host=host, + port=port, + ssl=ssl_context, + )