Beispiel #1
0
def get_things_done():
    """
    Here is where the service part is setup and action is done.
    """
    responders = yield start_responders()

    store = MemoryStore()

    # We first validate the directory.
    account_key = _get_account_key()
    try:
        client = yield Client.from_url(
            reactor,
            URL.fromText(acme_url.decode('utf-8')),
            key=JWKRSA(key=account_key),
            alg=RS256,
        )
    except Exception as error:
        print('\n\nFailed to connect to ACME directory. %s' % (error, ))
        yield reactor.stop()
        defer.returnValue(None)

    service = AcmeIssuingService(
        email='[email protected],[email protected]',
        cert_store=store,
        client=client,
        clock=reactor,
        responders=responders,
        panic=on_panic,
    )

    # Start the service and wait for it to start.
    yield service.start()

    # Wait for the existing certificate from the storage to be available.
    yield service.when_certs_valid()

    # Request a SAN ... if passed via command line.
    yield service.issue_cert(','.join(requested_domains))

    yield service.stopService()

    print('That was all the example.')
Beispiel #2
0
class MarathonAcme(object):
    log = Logger()

    def __init__(self,
                 marathon_client,
                 group,
                 cert_store,
                 mlb_client,
                 txacme_client_creator,
                 reactor,
                 email=None):
        """
        Create the marathon-acme service.

        :param marathon_client: The Marathon API client.
        :param group: The name of the marathon-lb group.
        :param cert_store: The ``ICertificateStore`` instance to use.
        :param mlb_clinet: The marathon-lb API client.
        :param txacme_client_creator: Callable to create the txacme client.
        :param reactor: The reactor to use.
        :param email: The ACME registration email.
        """
        self.marathon_client = marathon_client
        self.group = group
        self.reactor = reactor

        responder = HTTP01Responder()
        self.server = MarathonAcmeServer(responder.resource)

        mlb_cert_store = MlbCertificateStore(cert_store, mlb_client)
        self.txacme_service = AcmeIssuingService(mlb_cert_store,
                                                 txacme_client_creator,
                                                 reactor, [responder], email)

        self._server_listening = None

    def run(self, endpoint_description):
        self.log.info('Starting marathon-acme...')

        # Start the server
        d = self.server.listen(self.reactor, endpoint_description)

        def on_server_listening(listening_port):
            self._server_listening = listening_port

            # Start the txacme service and wait for the initial check
            self.txacme_service.startService()
            return self.txacme_service.when_certs_valid()

        d.addCallback(on_server_listening)

        # Then listen for events...
        d.addCallback(lambda _: self.listen_events())

        # If anything goes wrong or listening for events returns, stop
        d.addBoth(self._stop)

        return d

    def _stop(self, result):
        if isinstance(result, Failure):
            self.log.failure('Unhandle error during operation', result)
        self.log.warn('Stopping marathon-acme...')

        # If the server failed to start we have nothing to cancel yet
        if self._server_listening is not None:
            return gatherResults([
                self._server_listening.stopListening(),
                self.txacme_service.stopService()
            ],
                                 consumeErrors=True)

    def listen_events(self, reconnects=0):
        """
        Start listening for events from Marathon, running a sync when we first
        successfully subscribe and triggering a sync on API request events.
        """
        self.log.info('Listening for events from Marathon...')
        self._attached = False

        def on_finished(result, reconnects):
            # If the callback fires then the HTTP request to the event stream
            # went fine, but the persistent connection for the SSE stream was
            # dropped. Just reconnect for now- if we can't actually connect
            # then the errback will fire rather.
            self.log.warn(
                'Connection lost listening for events, '
                'reconnecting... ({reconnects} so far)',
                reconnects=reconnects)
            reconnects += 1
            return self.listen_events(reconnects)

        def log_failure(failure):
            self.log.failure('Failed to listen for events', failure)
            return failure

        return self.marathon_client.get_events({
            'event_stream_attached':
            self._sync_on_event_stream_attached,
            'api_post_event':
            self._sync_on_api_post_event
        }).addCallbacks(on_finished, log_failure, callbackArgs=[reconnects])

    def _sync_on_event_stream_attached(self, event):
        if self._attached:
            self.log.debug(
                'event_stream_attached event received (timestamp: '
                '"{timestamp}", remoteAddress: "{remoteAddress}"), but '
                'already attached',
                timestamp=event['timestamp'],
                remoteAddress=event['remoteAddress'])
            return

        self._attached = True
        self.log.info(
            'event_stream_attached event received (timestamp: "{timestamp}", '
            'remoteAddress: "{remoteAddress}"), running initial sync...',
            timestamp=event['timestamp'],
            remoteAddress=event['remoteAddress'])
        return self.sync()

    def _sync_on_api_post_event(self, event):
        self.log.info(
            'api_post_event event received (timestamp: "{timestamp}", uri: '
            '"{uri}"), triggering a sync...',
            timestamp=event['timestamp'],
            uri=event['uri'])
        return self.sync()

    def sync(self):
        """
        Fetch the list of apps from Marathon, find the domains that require
        certificates, and issue certificates for any domains that don't already
        have a certificate.
        """
        self.log.info('Starting a sync...')

        def log_success(result):
            self.log.info('Sync completed successfully')
            return result

        def log_failure(failure):
            self.log.failure('Sync failed', failure, LogLevel.error)
            return failure

        return (self.marathon_client.get_apps().addCallback(
            self._apps_acme_domains).addCallback(
                self._filter_new_domains).addCallback(
                    self._issue_certs).addCallbacks(log_success, log_failure))

    def _apps_acme_domains(self, apps):
        domains = []
        for app in apps:
            domains.extend(self._app_acme_domains(app))

        self.log.debug('Found {len_domains} domains for apps: {domains}',
                       len_domains=len(domains),
                       domains=domains)

        return domains

    def _app_acme_domains(self, app):
        app_domains = []
        labels = app['labels']
        app_group = labels.get('HAPROXY_GROUP')

        # Prefer the 'portDefinitions' field added in Marathon 1.0.0 but fall
        # back to the deprecated 'ports' array if that's not present.
        if 'portDefinitions' in app:
            ports = app['portDefinitions']
        else:
            ports = app['ports']

        # Iterate through the ports, checking for corresponding labels
        for port_index, _ in enumerate(ports):
            # Get the port group label, defaulting to the app group label
            port_group = labels.get('HAPROXY_%d_GROUP' % (port_index, ),
                                    app_group)

            if port_group == self.group:
                domain_label = labels.get(
                    'MARATHON_ACME_%d_DOMAIN' % (port_index, ), '')
                port_domains = parse_domain_label(domain_label)

                if port_domains:
                    # TODO: Support SANs- for now just use the first domain
                    if len(port_domains) > 1:
                        self.log.warn(
                            'Multiple domains found for port {port} of app '
                            '{app}, only the first will be used',
                            port=port_index,
                            app=app['id'])

                    app_domains.append(port_domains[0])

        self.log.debug('Found {len_domains} domains for app {app}: {domains}',
                       len_domains=len(app_domains),
                       app=app['id'],
                       domains=app_domains)

        return app_domains

    def _filter_new_domains(self, marathon_domains):
        def filter_domains(stored_domains):
            return set(marathon_domains) - set(stored_domains.keys())

        d = self.txacme_service.cert_store.as_dict()
        d.addCallback(filter_domains)
        return d

    def _issue_certs(self, domains):
        if domains:
            self.log.info(
                'Issuing certificates for {len_domains} domains: {domains}',
                len_domains=len(domains),
                domains=domains)
        else:
            self.log.debug('No new domains to issue certificates for')
        return gatherResults([self._issue_cert(domain) for domain in domains])

    def _issue_cert(self, domain):
        """
        Issue a certificate for the given domain.
        """
        def errback(failure):
            # Don't fail on some of the errors we could get from the ACME
            # server, rather just log an error so that we can continue with
            # other domains.
            failure.trap(txacme_ServerError)
            acme_error = failure.value.message

            # FIXME: The acme error code stuff is a mess pre- the unreleased
            # 0.10 version. Update this to use the 'code' attribute when the
            # new acme library is released.
            acme_error_code = str(acme_error.typ).split(':')[-1]
            if acme_error_code in [
                    'rateLimited', 'serverInternal', 'connection',
                    'unknownHost'
            ]:
                # TODO: Fire off an error to Sentry or something?
                self.log.error(
                    'Error ({code}) issuing certificate for "{domain}": '
                    '{detail}',
                    code=acme_error_code,
                    domain=domain,
                    detail=acme_error.detail)
            else:
                # There are more error codes but if they happen then something
                # serious has gone wrong-- carry on error-ing.
                return failure

        d = self.txacme_service.issue_cert(domain)
        return d.addErrback(errback)
Beispiel #3
0
class AcmeHandler(object):
    def __init__(self, hs):
        self.hs = hs
        self.reactor = hs.get_reactor()
        self._acme_domain = hs.config.acme_domain

    @defer.inlineCallbacks
    def start_listening(self):

        # Configure logging for txacme, if you need to debug
        # from eliot import add_destinations
        # from eliot.twisted import TwistedDestination
        #
        # add_destinations(TwistedDestination())

        from txacme.challenges import HTTP01Responder
        from txacme.service import AcmeIssuingService
        from txacme.endpoint import load_or_create_client_key
        from txacme.client import Client
        from josepy.jwa import RS256

        self._store = ErsatzStore()
        responder = HTTP01Responder()

        self._issuer = AcmeIssuingService(
            cert_store=self._store,
            client_creator=(lambda: Client.from_url(
                reactor=self.reactor,
                url=URL.from_text(self.hs.config.acme_url),
                key=load_or_create_client_key(
                    FilePath(self.hs.config.config_dir_path)),
                alg=RS256,
            )),
            clock=self.reactor,
            responders=[responder],
        )

        well_known = Resource()
        well_known.putChild(b'acme-challenge', responder.resource)
        responder_resource = Resource()
        responder_resource.putChild(b'.well-known', well_known)
        responder_resource.putChild(b'check',
                                    static.Data(b'OK', b'text/plain'))

        srv = server.Site(responder_resource)

        bind_addresses = self.hs.config.acme_bind_addresses
        for host in bind_addresses:
            logger.info(
                "Listening for ACME requests on %s:%i",
                host,
                self.hs.config.acme_port,
            )
            try:
                self.reactor.listenTCP(
                    self.hs.config.acme_port,
                    srv,
                    interface=host,
                )
            except twisted.internet.error.CannotListenError as e:
                check_bind_error(e, host, bind_addresses)

        # Make sure we are registered to the ACME server. There's no public API
        # for this, it is usually triggered by startService, but since we don't
        # want it to control where we save the certificates, we have to reach in
        # and trigger the registration machinery ourselves.
        self._issuer._registered = False
        yield self._issuer._ensure_registered()

    @defer.inlineCallbacks
    def provision_certificate(self):

        logger.warning("Reprovisioning %s", self._acme_domain)

        try:
            yield self._issuer.issue_cert(self._acme_domain)
        except Exception:
            logger.exception("Fail!")
            raise
        logger.warning("Reprovisioned %s, saving.", self._acme_domain)
        cert_chain = self._store.certs[self._acme_domain]

        try:
            with open(self.hs.config.tls_private_key_file,
                      "wb") as private_key_file:
                for x in cert_chain:
                    if x.startswith(b"-----BEGIN RSA PRIVATE KEY-----"):
                        private_key_file.write(x)

            with open(self.hs.config.tls_certificate_file,
                      "wb") as certificate_file:
                for x in cert_chain:
                    if x.startswith(b"-----BEGIN CERTIFICATE-----"):
                        certificate_file.write(x)
        except Exception:
            logger.exception("Failed saving!")
            raise

        defer.returnValue(True)