-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Adrien Delle Cave
committed
Apr 13, 2020
1 parent
4efe45c
commit 67ef4b2
Showing
3 changed files
with
478 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
# -*- coding: utf-8 -*- | ||
# Copyright (C) 2018-2019 fjord-technologies | ||
# SPDX-License-Identifier: GPL-3.0-or-later | ||
"""covenant.modules.probes""" | ||
|
||
import gc | ||
import logging | ||
import time | ||
import uuid | ||
|
||
from dwho.classes.modules import DWhoModuleBase, MODULES | ||
from httpdis.httpdis import HttpReqError, HttpResponse | ||
from sonicprobe.libs import xys | ||
from sonicprobe.libs.moresynchro import RWLock | ||
|
||
from covenant.classes.plugins import CovenantEPTObject, EPTS_SYNC | ||
|
||
LOG = logging.getLogger('covenant.modules.probes') | ||
|
||
|
||
# pylint: disable=attribute-defined-outside-init | ||
class ProbesModule(DWhoModuleBase): | ||
MODULE_NAME = 'probes' | ||
|
||
LOCK = RWLock() | ||
|
||
def safe_init(self, options): | ||
self.results = {} | ||
self.lock_timeout = self.config['general']['lock_timeout'] | ||
|
||
def _set_result(self, obj): | ||
self.results[obj.get_uid()] = obj | ||
|
||
def _get_result(self, uid): | ||
r = {'error': None, | ||
'result': None} | ||
|
||
while True: | ||
if uid not in self.results: | ||
time.sleep(0.1) | ||
continue | ||
|
||
res = self.results.pop(uid) | ||
if res.has_error(): | ||
r['error'] = res.get_errors() | ||
LOG.error("failed on call: %r. (errors: %r)", res.get_uid(), r['error']) | ||
else: | ||
r['result'] = res.get_result() | ||
LOG.info("successful on call: %r", res.get_uid()) | ||
LOG.debug("result on call: %r", r['result']) | ||
|
||
return r | ||
|
||
def _push_epts_sync(self, endpoint, method, params, args = None): | ||
if endpoint not in EPTS_SYNC: | ||
raise HttpReqError(404, "unable to find endpoint: %r" % endpoint) | ||
elif EPTS_SYNC[endpoint].type != 'probe': | ||
raise HttpReqError(400, "invalid endpoint type, correct type: %r" % EPTS_SYNC[endpoint].type) | ||
|
||
ept_sync = EPTS_SYNC[endpoint] | ||
uid = "%s:%s" % (ept_sync.name, uuid.uuid4()) | ||
ept_sync.qput(CovenantEPTObject(ept_sync.name, | ||
uid, | ||
endpoint, | ||
method, | ||
params, | ||
args, | ||
self._set_result)) | ||
return uid | ||
|
||
|
||
PROBES_QSCHEMA = xys.load(""" | ||
endpoint: !!str | ||
target*: !!str | ||
""") | ||
|
||
def probes(self, request): | ||
params = request.query_params() | ||
|
||
if not isinstance(params, dict): | ||
raise HttpReqError(400, "invalid arguments type") | ||
|
||
if not xys.validate(params, self.PROBES_QSCHEMA): | ||
raise HttpReqError(415, "invalid arguments for command") | ||
|
||
if not self.LOCK.acquire_read(self.lock_timeout): | ||
raise HttpReqError(503, "unable to take LOCK for reading after %s seconds" % self.lock_timeout) | ||
|
||
try: | ||
uid = self._push_epts_sync(params['endpoint'], 'probes', params) | ||
res = self._get_result(uid) | ||
if res['error']: | ||
raise HttpReqError(500, "failed to get results. (errors: %r)" % res['error']) | ||
|
||
return HttpResponse(data = res['result']) | ||
except HttpReqError: | ||
raise | ||
except Exception as e: | ||
LOG.exception(e) | ||
finally: | ||
gc.collect() | ||
self.LOCK.release() | ||
|
||
|
||
if __name__ != "__main__": | ||
def _start(): | ||
MODULES.register(ProbesModule()) | ||
_start() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,243 @@ | ||
# -*- coding: utf-8 -*- | ||
# Copyright (C) 2020 fjord-technologies | ||
# SPDX-License-Identifier: GPL-3.0-or-later | ||
"""covenant.plugins.pssl""" | ||
|
||
import logging | ||
import socket | ||
import ssl | ||
|
||
from datetime import datetime | ||
|
||
from cryptography import x509 | ||
from cryptography.hazmat.backends import default_backend | ||
|
||
import six | ||
|
||
from sonicprobe.libs import network, urisup | ||
|
||
from covenant.classes.exceptions import CovenantConfigurationError | ||
from covenant.classes.plugins import CovenantPlugBase, CovenantTargetFailed, PLUGINS | ||
|
||
LOG = logging.getLogger('covenant.plugins.ssl') | ||
|
||
_ALLOWED_OPTIONS = ('ALL', | ||
'NO_SSLv2', | ||
'NO_SSLv3', | ||
'NO_TLSv1', | ||
'NO_TLSv1_1', | ||
'NO_TLSv1_2', | ||
'NO_TLSv1_3', | ||
'NO_COMPRESSION', | ||
'CIPHER_SERVER_PREFERENCE', | ||
'SINGLE_DH_USE', | ||
'SINGLE_ECDH_USE', | ||
'ENABLE_MIDDLEBOX_COMPAT') | ||
|
||
|
||
class CovenantSslPlugin(CovenantPlugBase): | ||
PLUGIN_NAME = 'ssl' | ||
|
||
@staticmethod | ||
def _dnsname_to_idn(name): | ||
for x in ('*.', '.'): | ||
if name.startswith(x): | ||
return "%s%s" % (x, network.encode_idn(name[len(x):], True)) | ||
|
||
return network.encode_idn(name, True) | ||
|
||
@staticmethod | ||
def _valid_hostname(valid_hosts, host): | ||
try: | ||
ssl.match_hostname(valid_hosts, host) | ||
except ssl.CertificateError: | ||
return False | ||
|
||
return True | ||
|
||
@staticmethod | ||
def _load_cert(cert_der, rs = None): | ||
if not isinstance(rs, dict): | ||
rs = {} | ||
|
||
cert = x509.load_der_x509_certificate(six.ensure_str(cert_der), default_backend()) | ||
|
||
rs['connect_success'] = True | ||
rs['cert_not_before'] = int(cert.not_valid_before.strftime('%s')) | ||
rs['cert_not_after'] = int(cert.not_valid_after.strftime('%s')) | ||
rs['cert_has_expired'] = cert.not_valid_after < datetime.utcnow() | ||
rs['cert_serial_no'] = cert.serial_number | ||
rs['cert_issuer_cn'] = cert.issuer.get_attributes_for_oid(x509.OID_COMMON_NAME)[0].value | ||
rs['cert_cn'] = cert.subject.get_attributes_for_oid(x509.OID_COMMON_NAME)[0].value | ||
rs['cert_emails'] = [] | ||
rs['cert_dns_names'] = [] | ||
rs['cert_ip_addresses'] = [] | ||
|
||
for x in cert.subject.get_attributes_for_oid(x509.OID_EMAIL_ADDRESS): | ||
rs['cert_emails'].append(six.ensure_str(x.value)) | ||
|
||
rs['cert_ou'] = [] | ||
for x in cert.subject.get_attributes_for_oid(x509.OID_ORGANIZATIONAL_UNIT_NAME): | ||
rs['cert_ou'].append(six.ensure_str(x.value)) | ||
|
||
return (cert, rs) | ||
|
||
@staticmethod | ||
def _load_context_options(context, options): | ||
if not isinstance(options, list): | ||
return | ||
|
||
for x in options: | ||
if x not in _ALLOWED_OPTIONS: | ||
LOG.warning("invalid ssl option: OP_%s", x) | ||
continue | ||
|
||
if not hasattr(ssl, "OP_%s" % x): | ||
LOG.warning("unknown ssl option: OP_%s", x) | ||
continue | ||
|
||
context.options |= getattr(ssl, "OP_%s" % x) | ||
|
||
@staticmethod | ||
def _connect(context, host, port, server_hostname, timeout): | ||
conn = context.wrap_socket( | ||
socket.socket(socket.AF_INET), | ||
server_hostname = server_hostname) | ||
|
||
conn.settimeout(timeout) | ||
conn.connect((host, int(port))) | ||
|
||
return conn | ||
|
||
def _subject_alt_name(self, cert, rs = None, valid_hosts = None): | ||
if not isinstance(rs, dict): | ||
rs = {} | ||
|
||
if not isinstance(valid_hosts, dict): | ||
valid_hosts = {} | ||
|
||
try: | ||
ext = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName) | ||
except Exception: | ||
return (rs, valid_hosts) | ||
|
||
for x in ext.value: | ||
if isinstance(x, x509.RFC822Name): | ||
rs['cert_emails'].append(six.ensure_str(x.value)) | ||
elif isinstance(x, x509.DNSName): | ||
rs['cert_dns_names'].append(six.ensure_str(x.value)) | ||
valid_hosts['subjectAltName'].append(('DNS', self._dnsname_to_idn(x.value))) | ||
elif isinstance(x, x509.IPAddress): | ||
rs['cert_ip_addresses'].append(six.ensure_str(x.value.compressed)) | ||
valid_hosts['subjectAltName'].append(('IP Address', six.ensure_str(x.value.compressed))) | ||
|
||
return (rs, valid_hosts) | ||
|
||
def _do_call(self, obj, targets = None, registry = None): # pylint: disable=unused-argument | ||
if not targets: | ||
targets = self.targets | ||
|
||
for target in targets: | ||
(data, conn) = (None, None) | ||
|
||
cfg = target.config | ||
common_name = cfg.get('common_name') | ||
cfg['timeout'] = cfg.get('timeout', 10) | ||
cfg['verify_peer'] = cfg.get('verify_peer', True) | ||
params = obj.get_params() | ||
|
||
if not params.get('target'): | ||
uri = cfg.get('uri') | ||
else: | ||
uri = params['target'] | ||
|
||
if not uri: | ||
raise CovenantConfigurationError("missing target or uri in configuration") | ||
|
||
uri_split = urisup.uri_help_split(uri) | ||
scheme = None | ||
|
||
if not isinstance(uri_split[1], tuple): | ||
host = uri_split[0] | ||
port = uri_split[2] | ||
elif uri_split[1]: | ||
scheme = uri_split[0] | ||
host, port = uri_split[1][2:4] | ||
else: | ||
raise CovenantConfigurationError("missing host and port in uri: %r" % uri) | ||
|
||
if not host: | ||
raise CovenantConfigurationError("missing or invalid host in uri: %r" % uri) | ||
|
||
if scheme and not port: | ||
try: | ||
port = socket.getservbyname(scheme) | ||
except socket.error: | ||
pass | ||
|
||
if not port: | ||
raise CovenantConfigurationError("missing or invalid port in uri: %r" % uri) | ||
|
||
data = {'connect_success': False, | ||
'cert_secure': False, | ||
"%s_success" % self.type: False} | ||
|
||
try: | ||
server_hostname = common_name or host | ||
|
||
context = ssl.create_default_context() | ||
context.check_hostname = False | ||
context.verify_mode = ssl.CERT_NONE | ||
|
||
self._load_context_options(context, cfg.get('options')) | ||
|
||
conn = self._connect(context, host, port, server_hostname, cfg['timeout']) | ||
|
||
data['cipher_info'] = conn.cipher()[0] | ||
data['version_info'] = conn.version() | ||
|
||
valid_hosts = {'subject': [], | ||
'subjectAltName': []} | ||
|
||
cert_der = conn.getpeercert(True) | ||
if cert_der: | ||
self._subject_alt_name(self._load_cert(cert_der, data)[0], data, valid_hosts) | ||
data['hostname_valid'] = self._valid_hostname(valid_hosts, server_hostname) | ||
|
||
if cfg['verify_peer']: | ||
if conn: | ||
conn.close() | ||
|
||
context.verify_mode = ssl.CERT_REQUIRED | ||
context.load_default_certs(ssl.Purpose.SERVER_AUTH) | ||
conn = self._connect(context, host, port, server_hostname, cfg['timeout']) | ||
except ssl.SSLError as e: | ||
LOG.warning("ssl error on target: %r. exception: %r", | ||
target.name, | ||
e) | ||
except Exception as e: | ||
data = CovenantTargetFailed(e) | ||
LOG.exception("error on target: %r. exception: %r", | ||
target.name, | ||
e) | ||
else: | ||
if data.get('connect_success'): | ||
if not cfg['verify_peer']: | ||
data["%s_success" % self.type] = True | ||
elif not data.get('cert_has_expired') \ | ||
and data.get('hostname_valid'): | ||
data['cert_secure'] = True | ||
data["%s_success" % self.type] = True | ||
finally: | ||
if conn: | ||
conn.close() | ||
|
||
target(data) | ||
|
||
return self.generate_latest(registry) | ||
|
||
|
||
if __name__ != "__main__": | ||
def _start(): | ||
PLUGINS.register(CovenantSslPlugin) | ||
_start() |
Oops, something went wrong.