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
## version 0.3.3 2023-08-04
* fix starttls behaviour with Python 3.11
## version 0.3.2 2021-03-07
* remove brokebn CA certificate statistics
* remove broken CA certificate statistics
## version 0.3.1 2019-06-23

View file

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

View file

@ -7,6 +7,29 @@ target: Messaging
type: Plugin
license: gplv3
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
description: "fix CA certificate check"
files: