简体   繁体   中英

Python 3: How to log SSL handshake errors from server side

I'm using HTTPServer for a basic HTTP server using SSL. I would like to log any time a client initiates an SSL Handshake (or perhaps any time a socket is accepted?) along with any associated errors. I imagine that I'd need to extend some class or override some method, but I'm not sure which or how to properly go about implementing it. I'd greatly appreciate any help. Thanks in advance!

Trimmed down sample code:

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()

From looking at the ssl module, most of the relevant magic happens in the SSLSocket class.

ssl.wrap_socket() is just a tiny convenience function that basically serves as a factory for an SSLSocket with some reasonable defaults, and wraps an existing socket.

Unfortunately, SSLSocket does not seem to do any logging of its own, so there's no easy way to turn up a logging level, set a debug flag or register any handlers.

So what you can do instead is to subclass SSLSocket , override the methods you're interested in with your own that do some logging, and create and use your own wrap_socket helper function.


Subclassing SSLSocket

First, copy over ssl.wrap_socket() from your Python's .../lib/python2.7/ssl.py into your code. (Make sure that any code you copy and modify actually comes from the Python installation you're using - the code may have changed between different Python versions).

Now adapt your copy of wrap_socket() to

  • create an instance of a LoggingSSLSocket (which we'll implement below) instead of SSLSocket
  • and use constants from the ssl module where necessary ( ssl.CERT_NONE and ssl.PROTOCOL_SSLv23 in this example)
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)

Now change your line

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

to

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

in order to use your own wrap_socket() .

Now for subclassing SSLSocket . Create a class LoggingSSLSocket that subclasses SSLSocket by adding the following to your code:

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

Here we override the accept() and do_handshake() methods of ssl.SSLSocket - everything else stays the same, since the class inherits from SSLSocket .


Generic approach to overriding methods

I used a particular pattern for overriding these methods in order to make it easier to apply to pretty much any method you'll ever override:

    def methodname(self, *args, **kwargs):

*args, **kwargs makes sure our method accepts any number of positional and keyword arguments, if any. accept doesn't actually take any of those, but it still works because of Python's packing / unpacking of argument lists .

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

Here you get the opportunity to do your own thing before calling the superclass' method.

        result = super(LoggingSSLSocket, self).methodname(*args, **kwargs)

This is the actual call to the superclass' method. See the docs on super() for details on how this works, but it basically calls .methodname() on LoggingSSLSocket 's superclass ( SSLSocket ). Because we pass *args, **kwargs to the method, we just pass on any positional and keyword arguments our method got - we don't even need to know what they are, the method signatures will always match.

Because some methods (like accept() ) will return a result, we store that result and return it at the end of our method, just before doing our post-call work:

        logger.debug('After call.')
        return result

Logging more details

If you want to include more information in your logging statements, you'll likely have to completely overwrite the respective methods. So copy them over and modify them as required, and make sure you satisfy any missing imports.

Here's an example for accept() that includes the IP address and local port of the client that's trying to connect:

    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

(Make sure to include from socket import socket in your imports at the top of your code - refer to the ssl module's imports to determine where you need to import missing names from if you get a NameError . An good text editor with PyFlakes configured is very helpful in pointing those missing imports out to you).

This method will result in logging output like this:

[2014-10-24 22:01:40,299] Accepting connection from '('127.0.0.1', 64152)'...
[2014-10-24 22:01:40,300] Done accepting connection.
[2014-10-24 22:01:40,301] Accepting connection from '('127.0.0.1', 64153)'...
[2014-10-24 22:01:40,302] Done accepting connection.
[2014-10-24 22:01:40,306] Accepting connection from '('127.0.0.1', 64155)'...
[2014-10-24 22:01:40,307] Done accepting connection.
[2014-10-24 22:01:40,308] 127.0.0.1 - - "GET / HTTP/1.1" 200 - 

Because it involves quite a few changes scattered all over the place, here's a gist containing all the changes to your example code .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM