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,
+ )