Python 3:如何从服务器端记录SSL握手错误

时间:2014-10-24 18:32:43

标签: python logging ssl

我正在使用HTTPServer作为使用SSL的基本HTTP服务器。我想记录客户端何时启动SSL握手(或者可能是任何时候接受套接字?)以及任何相关的错误。我想我需要扩展一些类或覆盖一些方法,但我不确定哪个或如何正确地实现它。我非常感谢任何帮助。提前谢谢!

修剪示例代码:

from http.server import BaseHTTPRequestHandler, HTTPServer
from socketserver import ThreadingMixIn
from threading import Thread
import ssl
import logging
import sys

class MyHTTPHandler(BaseHTTPRequestHandler):
    def log_message(self, format, *args):
        logger.info("%s - - %s" % (self.address_string(), format%args))
    def do_GET(self):
        self.send_response(200)
        self.end_headers()
        self.wfile.write('test'.encode("utf-8"))

class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
    pass

logger = logging.getLogger('myserver')
handler = logging.FileHandler('server.log')
formatter = logging.Formatter('[%(asctime)s] %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(logging.DEBUG)

server = ThreadedHTTPServer(('', 443), MyHTTPHandler)
server.socket = ssl.wrap_socket (server.socket, keyfile='server.key', certfile='server.crt', server_side=True, cert_reqs=ssl.CERT_REQUIRED, ca_certs='client.crt')
Thread(target=server.serve_forever).start()

try: 
    quitcheck = input("Type 'quit' at any time to quit.\n")
    if quitcheck == "quit":
        server.shutdown()
except (KeyboardInterrupt) as error:
    server.shutdown()

1 个答案:

答案 0 :(得分:8)

通过查看ssl模块,大多数相关魔法发生在SSLSocket类中。

ssl.wrap_socket()只是一个很小的便利函数,基本上用作具有一些合理默认值的SSLSocket的工厂,并包装现有套接字。

不幸的是,SSLSocket似乎没有自己的任何日志记录,因此没有简单的方法来调高日志记录级别,设置debug标志或注册任何处理程序。

所以你可以做的是子类 SSLSocket,覆盖你自己感兴趣的方法,做一些日志记录,并创建和使用你自己的{{1帮助函数。


子类化wrap_socket

首先,将Python SSLSocket中的ssl.wrap_socket()复制到您的代码中。 (确保您复制和修改的任何代码实际上来自您正在使用的Python安装 - 代码可能在不同的Python版本之间发生了变化)。

现在将.../lib/python2.7/ssl.py的副本调整为

  • 创建wrap_socket()的实例(我们将在下面实施)而不是LoggingSSLSocket
  • 并在必要时使用SSLSocket模块中的常量(本例中为sslssl.CERT_NONE
ssl.PROTOCOL_SSLv23

现在改变你的行

def wrap_socket(sock, keyfile=None, certfile=None,
                server_side=False, cert_reqs=ssl.CERT_NONE,
                ssl_version=ssl.PROTOCOL_SSLv23, ca_certs=None,
                do_handshake_on_connect=True,
                suppress_ragged_eofs=True,
                ciphers=None):

    return LoggingSSLSocket(sock=sock, keyfile=keyfile, certfile=certfile,
                            server_side=server_side, cert_reqs=cert_reqs,
                            ssl_version=ssl_version, ca_certs=ca_certs,
                            do_handshake_on_connect=do_handshake_on_connect,
                            suppress_ragged_eofs=suppress_ragged_eofs,
                            ciphers=ciphers)

server.socket = ssl.wrap_socket (server.socket, ...)

为了使用您自己的server.socket = wrap_socket(server.socket, ...)

现在为子类化wrap_socket()。通过在代码中添加以下内容,创建子类SSLSocket的类LoggingSSLSocket

SSLSocket

我们在此处覆盖class LoggingSSLSocket(ssl.SSLSocket): def accept(self, *args, **kwargs): logger.debug('Accepting connection...') result = super(LoggingSSLSocket, self).accept(*args, **kwargs) logger.debug('Done accepting connection.') return result def do_handshake(self, *args, **kwargs): logger.debug('Starting handshake...') result = super(LoggingSSLSocket, self).do_handshake(*args, **kwargs) logger.debug('Done with handshake.') return result 的{​​{3}}和accept()方法 - 其他一切都保持不变,因为该类继承自ssl.SSLSocket


覆盖方法的通用方法

我使用特定模式来覆盖这些方法,以便更容易应用于您将覆盖的几乎任何方法:

SSLSocket

def methodname(self, *args, **kwargs): 确保我们的方法接受任意数量的位置和关键字参数(如果有的话)。 *args, **kwargs实际上并不接受任何这些,但它仍然有效,因为Python的do_handshake()

accept

在调用超类'方法之前,你有机会做自己的事情。

        logger.debug('Before call to superclass method')

这是对超类'方法的实际调用。有关其工作原理的详细信息,请参阅packing / unpacking of argument lists,但它基本上会在 result = super(LoggingSSLSocket, self).methodname(*args, **kwargs) 的超类(.methodname())上调用LoggingSSLSocket。因为我们将SSLSocket传递给方法,所以我们只传递我们的方法获得的任何位置和关键字参数 - 我们甚至不需要知道它们是什么,方法签名将始终匹配。

因为某些方法(例如docs on super())会返回一个结果,我们会存储*args, **kwargs并在我们的方法结束时返回它,就在我们进行调用后工作之前:

result

记录更多详细信息

如果要在日志记录语句中包含更多信息,则可能必须完全覆盖相应的方法。因此,将它们复制并根据需要进行修改,并确保满足任何缺失的导入。

以下是 logger.debug('After call.') return result 的示例,其中包含尝试连接的客户端的IP地址和本地端口:

accept()

(请务必在代码顶部的导入中加入 def accept(self): """Accepts a new connection from a remote client, and returns a tuple containing that new connection wrapped with a server-side SSL channel, and the address of the remote client.""" newsock, addr = socket.accept(self) logger.debug("Accepting connection from '%s'..." % (addr, )) newsock = self.context.wrap_socket(newsock, do_handshake_on_connect=self.do_handshake_on_connect, suppress_ragged_eofs=self.suppress_ragged_eofs, server_side=True) logger.debug('Done accepting connection.') return newsock, addr - 如果您获得from socket import socket,请参阅accept()以确定导入丢失名称的位置配置ssl module's imports的优秀文本编辑器非常有助于将缺少的导入指向您。)

此方法将导致记录输出如下:

NameError

因为它涉及到遍布各处的相当多的变化,所以这里是PyFlakes