Fix Python 3.11 STARTTLS handling

This commit is contained in:
Jan Dittberner 2023-08-04 19:35:27 +02:00
parent c8f93d56dc
commit ea68a363ca
3 changed files with 136 additions and 108 deletions

View file

@ -1,8 +1,12 @@
# change log # change log
## version 0.3.3 2023-08-04
* fix starttls behaviour with Python 3.11
## version 0.3.2 2021-03-07 ## version 0.3.2 2021-03-07
* remove brokebn CA certificate statistics * remove broken CA certificate statistics
## version 0.3.1 2019-06-23 ## version 0.3.1 2019-06-23

View file

@ -33,7 +33,6 @@ import nagiosplugin
__author__ = "Jan Dittberner" __author__ = "Jan Dittberner"
__version__ = "0.3.2" __version__ = "0.3.2"
NS_IETF_XMPP_SASL = 'urn:ietf:params:xml:ns:xmpp-sasl' NS_IETF_XMPP_SASL = 'urn:ietf:params:xml:ns:xmpp-sasl'
NS_IETF_XMPP_TLS = 'urn:ietf:params:xml:ns:xmpp-tls' NS_IETF_XMPP_TLS = 'urn:ietf:params:xml:ns:xmpp-tls'
NS_IETF_XMPP_STREAMS = 'urn:ietf:params:xml:ns:xmpp-streams' NS_IETF_XMPP_STREAMS = 'urn:ietf:params:xml:ns:xmpp-streams'
@ -55,10 +54,14 @@ class XmppException(Exception):
Custom exception class. Custom exception class.
""" """
def __init__(self, message): def __init__(self, message):
self.message = message self.message = message
super(XmppException, self).__init__() super(XmppException, self).__init__()
def __str__(self):
return self.message
class XmppStreamError(object): class XmppStreamError(object):
""" """
@ -69,7 +72,7 @@ class XmppStreamError(object):
text = None text = None
other_elements = {} other_elements = {}
def str(self): def __str__(self):
if self.text: if self.text:
return "{condition}: {text}".format( return "{condition}: {text}".format(
condition=self.condition, text=self.text) condition=self.condition, text=self.text)
@ -84,12 +87,13 @@ class XmppResponseHandler(ContentHandler):
seen_elements = set() seen_elements = set()
mechanisms = [] mechanisms = []
starttls = False starttls = False
tlsrequired = False tls_required = False
capabilities = {} capabilities = {}
state = XMPP_STATE_NEW state = XMPP_STATE_NEW
streaminfo = None stream_info = None
error_instance = None
inelem = [] in_elem = []
level = 0 level = 0
def __init__(self, expect_starttls): def __init__(self, expect_starttls):
@ -97,35 +101,35 @@ class XmppResponseHandler(ContentHandler):
super(XmppResponseHandler, self).__init__() super(XmppResponseHandler, self).__init__()
def startElementNS(self, name, qname, attrs): def startElementNS(self, name, qname, attrs):
self.inelem.append(name) self.in_elem.append(name)
self.seen_elements.add(name) self.seen_elements.add(name)
if name == (NS_ETHERX_STREAMS, 'stream'): if name == (NS_ETHERX_STREAMS, 'stream'):
self.state = XMPP_STATE_STREAM_START self.state = XMPP_STATE_STREAM_START
self.streaminfo = dict([ self.stream_info = dict([
(qname, attrs.getValueByQName(qname)) for (qname, attrs.getValueByQName(qname)) for
qname in attrs.getQNames()]) qname in attrs.getQNames()])
elif name == (NS_IETF_XMPP_TLS, 'starttls'): elif name == (NS_IETF_XMPP_TLS, 'starttls'):
self.starttls = True self.starttls = True
elif ( elif (
self.inelem[-2] == (NS_IETF_XMPP_TLS, 'starttls') and self.in_elem[-2] == (NS_IETF_XMPP_TLS, 'starttls') and
name == (NS_IETF_XMPP_TLS, 'required') name == (NS_IETF_XMPP_TLS, 'required')
): ):
self.tlsrequired = True self.tls_required = True
_LOG.info("info other side requires TLS") _LOG.info("info other side requires TLS")
elif name == (NS_JABBER_CAPS, 'c'): elif name == (NS_JABBER_CAPS, 'c'):
for qname in attrs.getQNames(): for qname in attrs.getQNames():
self.capabilities[qname] = attrs.getValueByQName(qname) self.capabilities[qname] = attrs.getValueByQName(qname)
elif name == (NS_ETHERX_STREAMS, 'error'): elif name == (NS_ETHERX_STREAMS, 'error'):
self.state = XMPP_STATE_ERROR self.state = XMPP_STATE_ERROR
self.errorinstance = XmppStreamError() self.error_instance = XmppStreamError()
elif ( elif (
self.state == XMPP_STATE_ERROR and self.state == XMPP_STATE_ERROR and
name != (NS_IETF_XMPP_STREAMS, 'text') name != (NS_IETF_XMPP_STREAMS, 'text')
): ):
if name[0] == NS_IETF_XMPP_STREAMS: if name[0] == NS_IETF_XMPP_STREAMS:
self.errorinstance.condition = name[1] self.error_instance.condition = name[1]
else: else:
self.errorinstance.other_elements[name] = {'attrs': dict([ self.error_instance.other_elements[name] = {'attrs': dict([
(qname, attrs.getValueByQName(qname)) for (qname, attrs.getValueByQName(qname)) for
qname in attrs.getQNames() qname in attrs.getQNames()
])} ])}
@ -138,25 +142,25 @@ class XmppResponseHandler(ContentHandler):
self.state = XMPP_STATE_FINISHED self.state = XMPP_STATE_FINISHED
elif name == (NS_ETHERX_STREAMS, 'error'): elif name == (NS_ETHERX_STREAMS, 'error'):
raise XmppException("XMPP stream error: {error}".format( raise XmppException("XMPP stream error: {error}".format(
error=self.errorinstance)) error=self.error_instance))
elif name == (NS_IETF_XMPP_TLS, 'proceed'): elif name == (NS_IETF_XMPP_TLS, 'proceed'):
self.state = XMPP_STATE_PROCEED_STARTTLS self.state = XMPP_STATE_PROCEED_STARTTLS
elif name == (NS_IETF_XMPP_TLS, 'failure'): elif name == (NS_IETF_XMPP_TLS, 'failure'):
raise XmppException("starttls initiation failed") raise XmppException("starttls initiation failed")
_LOG.debug('end %s', name) _LOG.debug('end %s', name)
del self.inelem[-1] del self.in_elem[-1]
def characters(self, content): def characters(self, content):
elem = self.inelem[-1] elem = self.in_elem[-1]
if elem == (NS_IETF_XMPP_SASL, 'mechanism'): if elem == (NS_IETF_XMPP_SASL, 'mechanism'):
self.mechanisms.append(content) self.mechanisms.append(content)
elif self.state == XMPP_STATE_ERROR: elif self.state == XMPP_STATE_ERROR:
if elem == (NS_IETF_XMPP_STREAMS, 'text'): if elem == (NS_IETF_XMPP_STREAMS, 'text'):
self.errorinstance.text = content self.error_instance.text = content
else: else:
self.errorinstance.other_elements[elem]['text'] = content self.error_instance.other_elements[elem]['text'] = content
else: else:
_LOG.warning('ignored content in %s: %s', self.inelem, content) _LOG.warning('ignored content in %s: %s', self.in_elem, content)
def is_valid_start(self): def is_valid_start(self):
if not self.state == XMPP_STATE_RECEIVED_FEATURES: if not self.state == XMPP_STATE_RECEIVED_FEATURES:
@ -164,14 +168,43 @@ class XmppResponseHandler(ContentHandler):
if self.expect_starttls is True and self.starttls is False: if self.expect_starttls is True and self.starttls is False:
raise XmppException('expected STARTTLS capable service') raise XmppException('expected STARTTLS capable service')
if ( if (
'version' not in self.streaminfo or 'version' not in self.stream_info or
self.streaminfo['version'] != '1.0' self.stream_info['version'] != '1.0'
): ):
_LOG.warning( _LOG.warning(
'unknown stream version %s', self.streaminfo['version']) 'unknown stream version %s', self.stream_info['version'])
return True return True
def open_socket(addrinfo):
"""
Open a client socket based on information in the addrinfo list of
tuples.
"""
new_socket = None
for res in addrinfo:
af, socktype, proto, canonname, sa = res
try:
new_socket = socket.socket(af, socktype, proto)
except socket.error:
new_socket = None
continue
try:
new_socket.connect(sa)
except socket.error:
new_socket.close()
new_socket = None
continue
break
if new_socket is None:
raise XmppException("could not open socket")
return new_socket
class Xmpp(nagiosplugin.Resource): class Xmpp(nagiosplugin.Resource):
""" """
Xmpp resource. Xmpp resource.
@ -180,7 +213,9 @@ class Xmpp(nagiosplugin.Resource):
state = nagiosplugin.Unknown state = nagiosplugin.Unknown
cause = None cause = None
socket = None socket = None
daysleft = None days_left = None
parser = None
content_handler = None
def __init__( def __init__(
self, host_address, port, ipv6, is_server, starttls, self, host_address, port, ipv6, is_server, starttls,
@ -192,8 +227,8 @@ class Xmpp(nagiosplugin.Resource):
self.is_server = is_server self.is_server = is_server
self.starttls = starttls self.starttls = starttls
self.servername = servername self.servername = servername
self.checkcerts = checkcerts self.check_certs = checkcerts
self.caroots = caroots self.ca_roots = caroots
self.make_parser() self.make_parser()
self.set_content_handler() self.set_content_handler()
@ -210,50 +245,25 @@ class Xmpp(nagiosplugin.Resource):
Set the XMPP SAX content handler. Set the XMPP SAX content handler.
""" """
self.contenthandler = XmppResponseHandler( self.content_handler = XmppResponseHandler(
expect_starttls=self.starttls) expect_starttls=self.starttls)
self.parser.setContentHandler(self.contenthandler) self.parser.setContentHandler(self.content_handler)
def get_addrinfo(self): def get_addr_info(self):
""" """
Perform the DNS lookup and return a list of potential socket address Perform the DNS lookup and return a list of potential socket address
tuples as returned by :py:method:`socket.getaddrinfo`. tuples as returned by :py:method:`socket.getaddrinfo`.
""" """
if self.ipv6 is None: if self.ipv6 is None:
addrfamily = 0 addr_family = 0
elif self.ipv6 is True: elif self.ipv6 is True:
addrfamily = socket.AF_INET6 addr_family = socket.AF_INET6
else: else:
addrfamily = socket.AF_INET addr_family = socket.AF_INET
return socket.getaddrinfo( return socket.getaddrinfo(
self.address, self.port, addrfamily, socket.SOCK_STREAM, self.address, self.port, addr_family, socket.SOCK_STREAM,
socket.IPPROTO_TCP) socket.IPPROTO_TCP)
self.result = nagiosplugin.Critical
def open_socket(self, addrinfo):
"""
Open a client socket based on information in the addrinfo list of
tuples.
"""
for res in addrinfo:
af, socktype, proto, canonname, sa = res
try:
s = socket.socket(af, socktype, proto)
except socket.error:
s = None
continue
try:
s.connect(sa)
except socket.error:
s.close()
s = None
continue
break
if s is None:
raise XmppException("could not open socket")
return s
def handle_xmpp_stanza( def handle_xmpp_stanza(
self, message_str, timeout=0.1, expected_state=None self, message_str, timeout=0.1, expected_state=None
@ -283,37 +293,28 @@ class Xmpp(nagiosplugin.Resource):
chunks.append(data) chunks.append(data)
else: else:
break break
xmltext = b''.join(chunks).decode('utf-8') xml_text = b''.join(chunks).decode('utf-8')
_LOG.debug("read %s", xmltext) _LOG.debug("read %s", xml_text)
self.parser.feed(xmltext) self.parser.feed(xml_text)
if ( if (
expected_state is not None and expected_state is not None and
self.contenthandler.state != expected_state self.content_handler.state != expected_state
): ):
raise XmppException( raise XmppException(
"unexpected state %s" % self.contenthandler.state) "unexpected state %s" % self.content_handler.state)
def start_stream(self): def start_stream(self):
""" """
Start a XMPP conversation with the server. Start a XMPP conversation with the server.
""" """
if self.is_server: namespace = "jabber:server" if self.is_server else "jabber:client"
self.handle_xmpp_stanza((
"<?xml version='1.0' ?><stream:stream to='{servername}' " self.handle_xmpp_stanza(
"xmlns='jabber:server' " f"<stream:stream xmlns:stream='{NS_ETHERX_STREAMS}' xmlns='{namespace}' to='{self.servername}'"
"xmlns:stream='http://etherx.jabber.org/streams' " f" version='1.0'>",
"version='1.0'>" expected_state=XMPP_STATE_RECEIVED_FEATURES
).format(servername=self.servername), )
expected_state=XMPP_STATE_RECEIVED_FEATURES)
else:
self.handle_xmpp_stanza((
"<?xml version='1.0' ?><stream:stream to='{servername}' "
"xmlns='jabber:client' "
"xmlns:stream='http://etherx.jabber.org/streams' "
"version='1.0'>"
).format(servername=self.servername),
expected_state=XMPP_STATE_RECEIVED_FEATURES)
def setup_ssl_context(self): def setup_ssl_context(self):
""" """
@ -321,17 +322,16 @@ class Xmpp(nagiosplugin.Resource):
""" """
context = ssl.create_default_context() context = ssl.create_default_context()
context.options = ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 if not self.check_certs:
if not self.checkcerts:
context.check_hostname = False context.check_hostname = False
context.verify_mode = ssl.CERT_NONE context.verify_mode = ssl.CERT_NONE
else: else:
context.verify_mode = ssl.CERT_REQUIRED context.verify_mode = ssl.CERT_REQUIRED
if self.caroots: if self.ca_roots:
if os.path.isfile(self.caroots): if os.path.isfile(self.ca_roots):
kwargs = {'cafile': self.caroots} kwargs = {'cafile': self.ca_roots}
else: else:
kwargs = {'capath': self.caroots} kwargs = {'capath': self.ca_roots}
context.load_verify_locations(**kwargs) context.load_verify_locations(**kwargs)
else: else:
context.load_default_certs() context.load_default_certs()
@ -346,21 +346,22 @@ class Xmpp(nagiosplugin.Resource):
""" """
_LOG.debug("start initiate_tls()") _LOG.debug("start initiate_tls()")
self.handle_xmpp_stanza( self.handle_xmpp_stanza(
"<starttls xmlns='{xmlns}'/>".format(xmlns=NS_IETF_XMPP_TLS), f"<starttls xmlns='{NS_IETF_XMPP_TLS}'/>",
timeout=0.5,
expected_state=XMPP_STATE_PROCEED_STARTTLS) expected_state=XMPP_STATE_PROCEED_STARTTLS)
sslcontext = self.setup_ssl_context() sslcontext = self.setup_ssl_context()
try: try:
self.socket = sslcontext.wrap_socket( self.socket = sslcontext.wrap_socket(
self.socket, server_hostname=self.servername) self.socket, server_hostname=self.servername)
_LOG.info("TLS socket setup successful") _LOG.info("TLS socket setup successful")
except ssl.SSLError as ssle: except ssl.CertificateError as certificate_error:
raise XmppException("SSL error %s" % ssle.strerror) raise XmppException("Certificate error %s" % certificate_error)
except ssl.CertificateError as certerr: except ssl.SSLError as ssl_error:
raise XmppException("Certificate error %s" % certerr) raise XmppException("SSL error %s" % ssl_error.strerror)
self.starttls = False self.starttls = False
# reset infos retrieved previously as written in RFC 3920 sec. 5. # reset infos retrieved previously as written in RFC 3920 sec. 5.
self.parser.reset() self.parser.reset()
if self.checkcerts: if self.check_certs:
certinfo = self.socket.getpeercert() certinfo = self.socket.getpeercert()
_LOG.debug("got the following certificate info: %s", certinfo) _LOG.debug("got the following certificate info: %s", certinfo)
_LOG.info( _LOG.info(
@ -368,12 +369,12 @@ class Xmpp(nagiosplugin.Resource):
certinfo['notBefore'], certinfo['notAfter']) certinfo['notBefore'], certinfo['notAfter'])
enddate = ssl.cert_time_to_seconds(certinfo['notAfter']) enddate = ssl.cert_time_to_seconds(certinfo['notAfter'])
remaining = datetime.fromtimestamp(enddate) - datetime.now() remaining = datetime.fromtimestamp(enddate) - datetime.now()
self.daysleft = remaining.days self.days_left = remaining.days
# start new parsing # start new parsing
self.make_parser() self.make_parser()
self.set_content_handler() self.set_content_handler()
self.start_stream() self.start_stream()
if not self.contenthandler.is_valid_start(): if not self.content_handler.is_valid_start():
raise XmppException("no valid response to XMPP client request") raise XmppException("no valid response to XMPP client request")
_LOG.debug("end initiate_tls()") _LOG.debug("end initiate_tls()")
@ -384,9 +385,9 @@ class Xmpp(nagiosplugin.Resource):
""" """
_LOG.debug("start handle_xmpp()") _LOG.debug("start handle_xmpp()")
self.start_stream() self.start_stream()
if not self.contenthandler.is_valid_start(): if not self.content_handler.is_valid_start():
raise XmppException("no valid response to XMPP client request") raise XmppException("no valid response to XMPP client request")
if self.starttls is True or self.contenthandler.tlsrequired: if self.starttls is True or self.content_handler.tls_required:
self.initiate_tls() self.initiate_tls()
self.handle_xmpp_stanza("</stream:stream>") self.handle_xmpp_stanza("</stream:stream>")
_LOG.debug("end handle_xmpp()") _LOG.debug("end handle_xmpp()")
@ -400,8 +401,8 @@ class Xmpp(nagiosplugin.Resource):
start = datetime.now() start = datetime.now()
_LOG.debug("start probe() at %s", start) _LOG.debug("start probe() at %s", start)
try: try:
addrinfo = self.get_addrinfo() addrinfo = self.get_addr_info()
self.socket = self.open_socket(addrinfo) self.socket = open_socket(addrinfo)
try: try:
self.handle_xmpp() self.handle_xmpp()
finally: finally:
@ -422,7 +423,7 @@ class Xmpp(nagiosplugin.Resource):
_LOG.debug("end probe() at %s", end) _LOG.debug("end probe() at %s", end)
yield nagiosplugin.Metric( yield nagiosplugin.Metric(
'time', (end - start).total_seconds(), 's', min=0) 'time', (end - start).total_seconds(), 's', min=0)
yield nagiosplugin.Metric('daysleft', self.daysleft, 'd') yield nagiosplugin.Metric('daysleft', self.days_left, 'd')
class XmppContext(nagiosplugin.ScalarContext): class XmppContext(nagiosplugin.ScalarContext):
@ -461,7 +462,7 @@ class DaysValidContext(nagiosplugin.Context):
self.critdays = critdays self.critdays = critdays
def evaluate(self, metric, resource): def evaluate(self, metric, resource):
if resource.checkcerts and metric.value is not None: if resource.check_certs and metric.value is not None:
if self.critical.match(metric.value): if self.critical.match(metric.value):
return nagiosplugin.Result( return nagiosplugin.Result(
nagiosplugin.Critical, nagiosplugin.Critical,
@ -477,7 +478,7 @@ class DaysValidContext(nagiosplugin.Context):
return nagiosplugin.Result(nagiosplugin.Ok) return nagiosplugin.Result(nagiosplugin.Ok)
def performance(self, metric, resource): def performance(self, metric, resource):
if resource.checkcerts and metric.value is not None: if resource.check_certs and metric.value is not None:
return nagiosplugin.Performance('daysvalid', metric.value, '') return nagiosplugin.Performance('daysvalid', metric.value, '')
return None return None

View file

@ -7,6 +7,29 @@ target: Messaging
type: Plugin type: Plugin
license: gplv3 license: gplv3
releases: releases:
- name: 0.3.3
description: "fix CA certificate check"
files:
-
name: check_xmppng
url: "file:///check_xmppng"
description: "Check command"
checksum: fdf942cb5c778aaa395a0ed1eba6dcda
-
name: COPYING
url: "file:///COPYING"
description: "GPL 3.0 license text"
checksum: d32239bcb673463ab874e80d47fae504
-
name: README.md
url: "file:///README.md"
description: "documentation"
checksum: 701ad7a882406a1f552a118d471a0b45
-
name: changes.md
url: "file:///changes.md"
description: "change log"
checksum: 0e23c919b413a4214c323b1953909c14
- name: 0.3.2 - name: 0.3.2
description: "fix CA certificate check" description: "fix CA certificate check"
files: files: