Skip to content

Commit

Permalink
New Example: authrelay (shows off AUTH system) (#249)
Browse files Browse the repository at this point in the history
* Add simple example for Authentication using DB
* Add documentation
* Bump version + NEWS
* Add new example requirements for pytype stage
* Add examples' deps to tox.ini
* Version Bump to 1.4.0a2
* Optimize release.py
* Enable multiple afterbar's for housekeep.py
  • Loading branch information
pepoluan committed Feb 21, 2021
1 parent 4336a05 commit d137f8d
Show file tree
Hide file tree
Showing 12 changed files with 164 additions and 11 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/unit-testing-and-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ jobs:
- name: "Static Type Checking"
# language=bash
run: |
# Required by examples
pip install dnspython argon2-cffi
# Install pytype
pip install pytype
pytype --keep-going --jobs auto .
- name: "Other QA Checks"
Expand Down
2 changes: 1 addition & 1 deletion aiosmtpd/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2014-2021 The aiosmtpd Developers
# SPDX-License-Identifier: Apache-2.0

__version__ = "1.4.0a1"
__version__ = "1.4.0a2"
1 change: 1 addition & 0 deletions aiosmtpd/docs/NEWS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
Added
-----
* Support for |PROXY Protocol|_ (Closes #174)
* Example for authentication

.. _`PROXY Protocol`: https://www.haproxy.com/blog/using-haproxy-with-the-proxy-protocol-to-better-secure-your-database/
.. |PROXY Protocol| replace:: **PROXY Protocol**
Expand Down
5 changes: 5 additions & 0 deletions aiosmtpd/docs/auth.rst
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,8 @@ AuthResult API
This is to cater for possible backward-compatibility requirements,
where legacy handlers might be looking for ``session.login_data`` for some reasons.


Example
=======

An example is provided in ``examples/authenticated_relayer``.
1 change: 1 addition & 0 deletions examples/authenticated_relayer/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mail.db
Empty file.
36 changes: 36 additions & 0 deletions examples/authenticated_relayer/make_user_db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Copyright 2014-2021 The aiosmtpd Developers
# SPDX-License-Identifier: Apache-2.0

import sqlite3
from argon2 import PasswordHasher
from pathlib import Path
from typing import Dict


DB_FILE = "mail.db~"
USER_AND_PASSWORD: Dict[str, str] = {
"user1": "not@password",
"user2": "correctbatteryhorsestaple",
"user3": "1d0ntkn0w",
"user4": "password",
"user5": "password123",
"user6": "a quick brown fox jumps over a lazy dog"
}


if __name__ == '__main__':
dbfp = Path(DB_FILE).absolute()
if dbfp.exists():
dbfp.unlink()
conn = sqlite3.connect(DB_FILE)
curs = conn.cursor()
curs.execute("CREATE TABLE userauth (username text, hashpass text)")
ph = PasswordHasher()
insert_up = "INSERT INTO userauth VALUES (?, ?)"
for u, p in USER_AND_PASSWORD.items():
h = ph.hash(p)
curs.execute(insert_up, (u, h))
conn.commit()
conn.close()
assert dbfp.exists()
print(f"database created at {dbfp}")
2 changes: 2 additions & 0 deletions examples/authenticated_relayer/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
argon2-cffi
dnspython
102 changes: 102 additions & 0 deletions examples/authenticated_relayer/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Copyright 2014-2021 The aiosmtpd Developers
# SPDX-License-Identifier: Apache-2.0

import asyncio
import dns.resolver
import logging
import sqlite3
import sys

from aiosmtpd.controller import Controller
from aiosmtpd.smtp import AuthResult, LoginPassword
from argon2 import PasswordHasher
from functools import lru_cache
from pathlib import Path
from smtplib import SMTP as SMTPCLient


DEST_PORT = 25
DB_AUTH = Path("mail.db~")


class Authenticator:
def __init__(self, auth_database):
self.auth_db = Path(auth_database)
self.ph = PasswordHasher()

def __call__(self, server, session, envelope, mechanism, auth_data):
fail_nothandled = AuthResult(success=False, handled=False)
if mechanism not in ("LOGIN", "PLAIN"):
return fail_nothandled
if not isinstance(auth_data, LoginPassword):
return fail_nothandled
username = auth_data.login
password = auth_data.password
hashpass = self.ph.hash(password)
conn = sqlite3.connect(self.auth_db)
curs = conn.execute(
"SELECT hashpass FROM userauth WHERE username=?", (username,)
)
hash_db = curs.fetchone()
conn.close()
if not hash_db:
return fail_nothandled
if hashpass != hash_db[0]:
return fail_nothandled
return AuthResult(success=True)


@lru_cache(maxsize=256)
def get_mx(domain):
records = dns.resolver.resolve(domain, "MX")
if not records:
return None
records = sorted(records, key=lambda r: r.preference)
return str(records[0].exchange)


class RelayHandler:
def handle_data(self, server, session, envelope, data):
mx_rcpt = {}
for rcpt in envelope.rcpt_tos:
_, _, domain = rcpt.partition("@")
mx = get_mx(domain)
if mx is None:
continue
mx_rcpt.setdefault(mx, []).append(rcpt)

for mx, rcpts in mx_rcpt.items():
with SMTPCLient(mx, 25) as client:
client.sendmail(
from_addr=envelope.mail_from,
to_addrs=rcpts,
msg=envelope.original_content
)


# noinspection PyShadowingNames
async def amain():
handler = RelayHandler()
cont = Controller(
handler,
hostname='',
port=8025,
authenticator=Authenticator(DB_AUTH)
)
try:
cont.start()
finally:
cont.stop()


if __name__ == '__main__':
if not DB_AUTH.exists():
print(f"Please create {DB_AUTH} first using make_user_db.py")
sys.exit(1)
logging.basicConfig(level=logging.DEBUG)
loop = asyncio.get_event_loop()
loop.create_task(amain())
try:
loop.run_forever()
except KeyboardInterrupt:
pass
10 changes: 6 additions & 4 deletions housekeep.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,10 +215,12 @@ def __call__(self, *args, **kwargs):
"--force", "-F", action="store_true", help="Force action even if in CI"
)
parser.add_argument(
"-A",
"--afterbar",
default=False,
action="store_true",
help="Print horizontal bar after action",
dest="afterbar",
default=0,
action="count",
help="Print horizontal bar after action. Repeat this option for more bars.",
)

# From: https://stackoverflow.com/a/49999185/149900
Expand Down Expand Up @@ -262,7 +264,7 @@ def python_interp_details():
)
dispatcher = globals().get(f"dispatch_{opts.cmd}")
dispatcher()
if opts.afterbar:
for _ in range(opts.afterbar):
print(Fore.CYAN + ("\u2550" * (TERM_WIDTH - 1)))
# Defensive reset
print(Style.RESET_ALL, end="", flush=True)
8 changes: 3 additions & 5 deletions release.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,10 @@
f"dist/aiosmtpd-{version}-py3-none-any.whl",
]

try:
subprocess.run(["twine", "--version"], stdout=subprocess.PIPE)
except FileNotFoundError:
print("Please install 'twine' first")
sys.exit(1)
result = subprocess.run(["pip", "freeze"], stdout=subprocess.PIPE)
if b"\ntwine==" not in result.stdout:
print("ERROR: twine not installed. Please install 'twine' first")
sys.exit(1)
if b"\ntwine-verify-upload==" not in result.stdout:
print("*** Package twine-verify-upload is not yet installed.")
print("*** Consider installing it. It is very useful :)")
Expand Down
5 changes: 4 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ commands =
diffcov: diff-cover coverage-{env:INTERP}.xml --html-report diffcov-{env:INTERP}.html
diffcov: diff-cover coverage-{env:INTERP}.xml --fail-under=100
profile: pytest --profile {posargs}
python housekeep.py --afterbar gather
python housekeep.py --afterbar --afterbar gather
#sitepackages = True
usedevelop = True
deps =
Expand Down Expand Up @@ -96,6 +96,9 @@ deps:
pytest
pytest-mock
packaging
# Deps of examples
argon2-cffi
dnspython

# I'd love to fold flake8 into pyproject.toml, because the flake8 settings
# should be "project-wide" settings (enforced not only during tox).
Expand Down

0 comments on commit d137f8d

Please sign in to comment.