diff --git a/changes.md b/changes.md index f327db8..5804cf7 100644 --- a/changes.md +++ b/changes.md @@ -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 diff --git a/check_xmppng b/check_xmppng index 0d08786..150540e 100755 --- a/check_xmppng +++ b/check_xmppng @@ -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(( - "" - ).format(servername=self.servername), - expected_state=XMPP_STATE_RECEIVED_FEATURES) - else: - self.handle_xmpp_stanza(( - "" - ).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"", + 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( - "".format(xmlns=NS_IETF_XMPP_TLS), + f"", + 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("") _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 diff --git a/icingaexchange.yml b/icingaexchange.yml index 048a9c9..514fe16 100644 --- a/icingaexchange.yml +++ b/icingaexchange.yml @@ -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: