Files
Mailu/core/admin/mailu/internal/nginx.py
bors[bot] 589c426601 Merge #2818
2818: Improve auth-related logging r=mergify[bot] a=nextgens

## What type of PR?

enhancement

## What does this PR do?

Improve auth-related logging

### Related issue(s)
- closes #2803 

## Prerequisites
Before we can consider review and merge, please make sure the following list is done and checked.
If an entry in not applicable, you can check it or remove it from the list.

- [ ] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
Co-authored-by: Florent Daigniere <nextgens@users.noreply.github.com>
2023-05-30 09:01:42 +00:00

155 lines
6.3 KiB
Python

from mailu import models, utils
from flask import current_app as app
from socrate import system
import urllib
import ipaddress
import sqlalchemy.exc
SUPPORTED_AUTH_METHODS = ["none", "plain"]
STATUSES = {
"authentication": ("Authentication credentials invalid", {
"imap": "AUTHENTICATIONFAILED",
"smtp": "535 5.7.8",
"pop3": "-ERR Authentication failed",
"sieve": "AuthFailed"
}),
"encryption": ("Must issue a STARTTLS command first", {
"smtp": "530 5.7.0"
}),
"ratelimit": ("Temporary authentication failure (rate-limit)", {
"imap": "LIMIT",
"smtp": "451 4.3.2",
"pop3": "-ERR [LOGIN-DELAY] Retry later"
}),
}
WEBMAIL_PORTS = ['14190', '10143', '10025']
def check_credentials(user, password, ip, protocol=None, auth_port=None, source_port=None):
if not user or not user.enabled or (protocol == "imap" and not user.enable_imap and not auth_port in WEBMAIL_PORTS) or (protocol == "pop3" and not user.enable_pop):
app.logger.info(f'Login attempt for: {user}/{protocol}/{auth_port} from: {ip}/{source_port}: failed: account disabled')
return False
# webmails
if auth_port in WEBMAIL_PORTS and password.startswith('token-'):
if utils.verify_temp_token(user.get_id(), password):
app.logger.debug(f'Login attempt for: {user}/{protocol}/{auth_port} from: {ip}/{source_port}: success: webmail-token')
return True
if utils.is_app_token(password):
for token in user.tokens:
if (token.check_password(password) and
(not token.ip or token.ip == ip)):
app.logger.info(f'Login attempt for: {user}/{protocol}/{auth_port} from: {ip}/{source_port}: success: token-{token.id}: {token.comment or ""!r}')
return True
if user.check_password(password):
app.logger.info(f'Login attempt for: {user}/{protocol}/{auth_port} from: {ip}/{source_port}: success: password')
return True
app.logger.info(f'Login attempt for: {user}/{protocol}/{auth_port} from: {ip}/{source_port}: failed: badauth: {utils.truncated_pw_hash(password)}')
return False
def handle_authentication(headers):
""" Handle an HTTP nginx authentication request
See: http://nginx.org/en/docs/mail/ngx_mail_auth_http_module.html#protocol
"""
method = headers["Auth-Method"].lower()
protocol = headers["Auth-Protocol"].lower()
# Incoming mail, no authentication
if method == "none" and protocol == "smtp":
server, port = get_server(protocol, False)
if app.config["INBOUND_TLS_ENFORCE"]:
if "Auth-SSL" in headers and headers["Auth-SSL"] == "on":
return {
"Auth-Status": "OK",
"Auth-Server": server,
"Auth-Port": port
}
else:
status, code = get_status(protocol, "encryption")
return {
"Auth-Status": status,
"Auth-Error-Code" : code,
"Auth-Wait": 0
}
else:
return {
"Auth-Status": "OK",
"Auth-Server": server,
"Auth-Port": port
}
# Authenticated user
elif method == "plain":
is_valid_user = False
# According to RFC2616 section 3.7.1 and PEP 3333, HTTP headers should
# be ASCII and are generally considered ISO8859-1. However when passing
# the password, nginx does not transcode the input UTF string, thus
# we need to manually decode.
raw_user_email = urllib.parse.unquote(headers["Auth-User"])
raw_password = urllib.parse.unquote(headers["Auth-Pass"])
user_email = 'invalid'
password = 'invalid'
try:
user_email = raw_user_email.encode("iso8859-1").decode("utf8")
password = raw_password.encode("iso8859-1").decode("utf8")
ip = urllib.parse.unquote(headers["Client-Ip"])
except:
app.logger.warn(f'Received undecodable user/password from nginx: {raw_user_email!r}/{raw_password!r}')
else:
try:
user = models.User.query.get(user_email) if '@' in user_email else None
except sqlalchemy.exc.StatementError as exc:
exc = str(exc).split('\n', 1)[0]
app.logger.warn(f'Invalid user {user_email!r}: {exc}')
else:
is_valid_user = user is not None
ip = urllib.parse.unquote(headers["Client-Ip"])
if check_credentials(user, password, ip, protocol, headers["Auth-Port"], headers['Client-Port']):
server, port = get_server(headers["Auth-Protocol"], True)
return {
"Auth-Status": "OK",
"Auth-Server": server,
"Auth-User": user_email,
"Auth-User-Exists": is_valid_user,
"Auth-Password": password,
"Auth-Port": port
}
status, code = get_status(protocol, "authentication")
return {
"Auth-Status": status,
"Auth-Error-Code": code,
"Auth-User": user_email,
"Auth-User-Exists": is_valid_user,
"Auth-Password": password,
"Auth-Wait": 0
}
# Unexpected
raise Exception("SHOULD NOT HAPPEN")
def get_status(protocol, status):
""" Return the proper error code depending on the protocol
"""
status, codes = STATUSES[status]
return status, codes[protocol]
def get_server(protocol, authenticated=False):
if protocol == "imap":
hostname, port = app.config['IMAP_ADDRESS'], 143
elif protocol == "pop3":
hostname, port = app.config['IMAP_ADDRESS'], 110
elif protocol == "smtp":
if authenticated:
hostname, port = app.config['SMTP_ADDRESS'], 10025
else:
hostname, port = app.config['SMTP_ADDRESS'], 25
elif protocol == "sieve":
hostname, port = app.config['IMAP_ADDRESS'], 4190
try:
# test if hostname is already resolved to an ip address
ipaddress.ip_address(hostname)
except:
# hostname is not an ip address - so we need to resolve it
hostname = system.resolve_hostname(hostname)
return hostname, port