【问题标题】:SMTP STARTTLS formatSMTP STARTTLS 格式
【发布时间】:2022-01-18 14:35:29
【问题描述】:

建立 TLS 连接后是否需要 EHLO 消息?我正在使用 acorn ltl-6511M 野生动物相机,它在建立 TLS 连接后似乎没有发送 EHLO 消息,导致我的基于 aiosmtpd 的 SMTP 服务器出现 503 错误。但它适用于 gmail SMTP。是摄像头遵循协议还是我的服务器不够健壮?

我使用的代码是:

import email
from email.header import decode_header
from email import message_from_bytes
from email.policy import default
from aiosmtpd.controller import Controller
from aiosmtpd.smtp import LoginPassword, AuthResult
import os
import sys
import time
import signal 
import logging
import ssl

##setting timezone
os.environ['TZ'] = "Europe/London"
time.tzset()

def onExit( sig, func=None):
    print("*************Stopping program*****************")
    controller.stop()
    exit()
 
signal.signal(signal.SIGTERM, onExit)

# removes the spaces and replaces with _ so they're valid folder names
def clean(text):
    return "".join(c if c.isalnum() else "_" for c in text)


log = logging.getLogger('mail.log')

auth_db = {
    b"TestCamera1@gmail.com": b"password1",
    b"user2": b"password2",
    b"TestCamera1": b"password1",
}

def authenticator_func(server, session, envelope, mechanism, auth_data):
    #this deliberately lets everything through
    assert isinstance(auth_data, LoginPassword)
    username = auth_data.login
    password = auth_data.password
    return AuthResult(success=True)


def configure_logging():
    file_handler = logging.FileHandler("aiosmtpd.log", "a")
    stderr_handler = logging.StreamHandler(sys.stderr)
    logger = logging.getLogger("mail.log")
    fmt = "[%(asctime)s %(levelname)s] %(message)s"
    datefmt = None
    formatter = logging.Formatter(fmt, datefmt, "%")
    stderr_handler.setFormatter(formatter)
    logger.addHandler(stderr_handler)
    file_handler.setFormatter(formatter)
    logger.addHandler(file_handler)
    logger.setLevel(logging.DEBUG)

class CustomHandler:
    def handle_exception(self, error):
        print("exception occured")
        print(error)
        return '542 Internal Server Error'

    async def handle_DATA(self, server, session, envelope):
        peer = session.peer
        data = envelope.content         # type: bytes
        msg = message_from_bytes(envelope.content, policy=default)
        # decode the email subject
        print("Msg:{}".format(msg))
        print("Data:{}".format(data))
        print("All of the relevant data has been extracted from the email")
        return '250 OK'


if __name__ == '__main__':
    configure_logging()
    handler = CustomHandler()
    #update hostname to your IP
    context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
    context.load_cert_chain('cert.pem', 'key.pem')    
    controller = Controller(handler, hostname='0.0.0.0', port=587, authenticator=authenticator_func, auth_required=True,auth_require_tls=True,tls_context=context)    
    # Run the event loop in a separate thread.
    controller.start()
    while True:
        time.sleep(10)

尝试整合后的代码是:

import email
from email.header import decode_header
from email import message_from_bytes
from email.policy import default
from aiosmtpd.controller import Controller
from aiosmtpd.smtp import LoginPassword, AuthResult, SMTP
import os
import json
import re
import sys
import time
import signal 
import logging
import ssl

from datetime import datetime
import configparser

##setting timezone
os.environ['TZ'] = "Europe/London"
time.tzset()

spacer = "*"*100

def onExit( sig, func=None):
    print("*************Stopping program*****************",3)
    controller.stop()
    exit()
 
signal.signal(signal.SIGTERM, onExit)

# removes the spaces and replaces with _ so they're valid folder names
def clean(text):
    return "".join(c if c.isalnum() else "_" for c in text)

log = logging.getLogger('mail.log')

auth_db = {
    b"TestCamera1@gmail.com": b"password1",
    b"user2": b"password2",
    b"TestCamera1": b"password1",
}

def authenticator_func(server, session, envelope, mechanism, auth_data):
    # Simple auth - is only being used because of the reolink cam
    assert isinstance(auth_data, LoginPassword)
    username = auth_data.login
    password = auth_data.password
    log.warning("Authenticator is being used")
    return AuthResult(success=True)

def configure_logging():
    file_handler = logging.FileHandler("aiosmtpd.log", "a")
    stderr_handler = logging.StreamHandler(sys.stderr)
    logger = logging.getLogger("mail.log")
    fmt = "[%(asctime)s %(levelname)s] %(message)s"
    datefmt = None
    formatter = logging.Formatter(fmt, datefmt, "%")
    stderr_handler.setFormatter(formatter)
    logger.addHandler(stderr_handler)
    file_handler.setFormatter(formatter)
    logger.addHandler(file_handler)
    logger.setLevel(logging.DEBUG)

class SMTPNoEhloAfterStarttls(SMTP):
    async def smtp_STARTTLS(self, arg: str):
        print(spacer)
        print("using starttls")
        host_name = self.session.host_name
        extended_smtp = self.session.extended_smtp
        await super().smtp_STARTTLS(arg)
        if host_name and extended_smtp and not self.session.host_name:
            # There was an EHLO before the STARTTLS.
            # RFC3207 says that we MUST reset the state
            # and forget the EHLO, but unfortunately
            # the client doesn't re-send the EHLO after STARTTLS,
            # so we need to pretend as if an EHLO has been sent.
            self.session.host_name = host_name
            self.session.extended_smtp = True

class ControllerNoEhloAfterStarttls(Controller):
    def factory(self):
        print(spacer)
        print("updating default settings")
        return SMTPNoEhloAfterStarttls(self.handler, **self.SMTP_kwargs)

class CustomHandler:
    def handle_exception(self, error):
        print("exception occured",3)
        print(error)
        return '542 Internal Server Error'

    async def handle_DATA(self, server, session, envelope):
        peer = session.peer
        data = envelope.content         # type: bytes
        msg = message_from_bytes(envelope.content, policy=default)
        # decode the email subject
        print("Msg:{}".format(msg),3)
        print("Data:{}".format(data),3)
        print("All of the relevant data has been extracted from the email",3)
        print(spacer,3)
        return '250 OK'

if __name__ == '__main__':
    configure_logging()
    handler = CustomHandler()
    # controller = Controller(handler, hostname='10.200.68.132', port=587)
    context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
    context.load_cert_chain('cert.pem', 'key.pem')
    controller = Controller(handler, hostname='10.200.68.133', port=587, authenticator=authenticator_func, auth_required=True,auth_require_tls=True,tls_context=context)    
    
    # Run the event loop in a separate thread.
    controller.start()
    #Confirmed that this is needed to keep the SMTP server running constantly
    while True:
        time.sleep(10)

但是,这对错误日志没有任何影响。

【问题讨论】:

标签: smtp aiosmtpd


【解决方案1】:

是的,在 STARTTLS 之后需要 EHLO,请参阅 RFC3207 Section 4.2(其中特别提到忘记了 EHLO 行 - 强调我的):

TLS 握手完成后,SMTP 协议将重置为 初始状态(服务器发出 220 后 SMTP 中的状态 服务就绪问候语)。服务器必须丢弃任何知识 从客户端获取,如 EHLO 命令的参数, 这不是从 TLS 协商本身获得的。

这意味着很遗憾您的相机没有遵循 SMTP 协议。不幸的是,GMail SMTP 不遵循协议(它不需要 EHLO 在 STARTTLS 和 AUTH LOGIN 之间)。

aiosmtpd 非常坚持遵循 SMTP 协议,并适时忘记了 STARTTLS 之前的 EHLO 数据; EHLO 主机名存储在aiosmtpd.smtp.SMTP 对象上的self.session.host_nameself.session is reset in SMTP.connection_made(),在STARTTLS 之后调用。

可能使 aiosmtpd 违反 SMTP 规范并以高度不符合的方式行事。显然,这是您不得在生产中做的事情。使用下面定义的 ControllerNoEhloAfterStarttls 而不是标准的 aiosmtpd Controller,然后它应该可以工作。

from aiosmtpd.smtp import SMTP
from aiosmtpd.controller import Controller

class SMTPNoEhloAfterStarttls(SMTP):
    async def smtp_STARTTLS(self, arg: str):
        host_name = self.session.host_name
        extended_smtp = self.session.extended_smtp
        await super().smtp_STARTTLS(arg)
        if host_name and extended_smtp and not self.session.host_name:
            # There was an EHLO before the STARTTLS.
            # RFC3207 says that we MUST reset the state
            # and forget the EHLO, but unfortunately
            # the client doesn't re-send the EHLO after STARTTLS,
            # so we need to pretend as if an EHLO has been sent.
            self.session.host_name = host_name
            self.session.extended_smtp = True

class ControllerNoEhloAfterStarttls(Controller):
    def factory(self):
        return SMTPNoEhloAfterStarttls(self.handler, **self.SMTP_kwargs)

...然后在if __name__ == "__main__":下,实例化自定义控制器类而不是默认控制器:

controller = ControllerNoEhloAfterStarttls(handler, hostname='10.200.68.133', port=587, ......)

【讨论】:

  • 我已将代码添加到我的问题中。我如何将您的代码与我的代码集成?
  • 我已经用底部的文字更新了我的答案,显示了如何集成它 - 请再看一看。
  • 我决定改用 nodejs 库。让它在 30 分钟内工作。不过感谢您的帮助。
猜你喜欢
  • 2018-07-06
  • 1970-01-01
  • 2013-06-21
  • 1970-01-01
  • 2013-08-21
  • 2011-08-01
  • 2015-10-11
  • 2018-03-19
  • 1970-01-01
相关资源
最近更新 更多