certbot/letsencrypt/plugins/standalone.py

265 lines
9.7 KiB
Python
Raw Normal View History

2015-09-26 12:47:29 -04:00
"""Standalone Authenticator."""
2015-10-04 14:53:36 -04:00
import argparse
2015-09-26 12:47:29 -04:00
import collections
import logging
import socket
import threading
import OpenSSL
2015-10-04 13:54:58 -04:00
import six
2015-09-26 12:47:29 -04:00
import zope.interface
from acme import challenges
from acme import standalone as acme_standalone
from letsencrypt import errors
from letsencrypt import interfaces
from letsencrypt.plugins import common
from letsencrypt.plugins import util
logger = logging.getLogger(__name__)
class ServerManager(object):
2015-10-04 13:21:13 -04:00
"""Standalone servers manager.
Manager for `ACMEServer` and `ACMETLSServer` instances.
2015-09-26 12:47:29 -04:00
`certs` and `http_01_resources` correspond to
2015-10-04 13:21:13 -04:00
`acme.crypto_util.SSLSocket.certs` and
`acme.crypto_util.SSLSocket.http_01_resources` respectively. All
2015-10-04 13:21:13 -04:00
created servers share the same certificates and resources, so if
2015-10-28 17:27:04 -04:00
you're running both TLS and non-TLS instances, HTTP01 handlers
2015-10-04 13:21:13 -04:00
will serve the same URLs!
"""
_Instance = collections.namedtuple("_Instance", "server thread")
def __init__(self, certs, http_01_resources):
self._instances = {}
2015-09-26 12:47:29 -04:00
self.certs = certs
self.http_01_resources = http_01_resources
2015-09-26 12:47:29 -04:00
def run(self, port, challenge_type):
2015-10-04 13:21:13 -04:00
"""Run ACME server on specified ``port``.
This method is idempotent, i.e. all calls with the same pair of
``(port, challenge_type)`` will reuse the same server.
2015-10-04 13:21:13 -04:00
:param int port: Port to run the server on.
:param challenge_type: Subclass of `acme.challenges.Challenge`,
2015-11-01 07:22:19 -05:00
either `acme.challenge.HTTP01` or `acme.challenges.TLSSNI01`.
2015-10-04 13:21:13 -04:00
:returns: Server instance.
:rtype: ACMEServerMixin
2015-10-04 13:21:13 -04:00
"""
2015-11-01 07:22:19 -05:00
assert challenge_type in (challenges.TLSSNI01, challenges.HTTP01)
if port in self._instances:
return self._instances[port].server
2015-09-26 12:47:29 -04:00
2015-10-08 17:10:12 -04:00
address = ("", port)
2015-09-26 12:47:29 -04:00
try:
2015-11-01 07:22:19 -05:00
if challenge_type is challenges.TLSSNI01:
2015-11-07 09:15:04 -05:00
server = acme_standalone.TLSSNI01Server(address, self.certs)
2015-10-28 17:27:04 -04:00
else: # challenges.HTTP01
server = acme_standalone.HTTP01Server(
address, self.http_01_resources)
2015-09-26 12:47:29 -04:00
except socket.error as error:
raise errors.StandaloneBindError(error, port)
thread = threading.Thread(
# pylint: disable=no-member
target=server.serve_forever)
2015-09-26 12:47:29 -04:00
thread.start()
2015-10-04 13:21:13 -04:00
# if port == 0, then random free port on OS is taken
# pylint: disable=no-member
2015-10-20 16:44:18 -04:00
real_port = server.socket.getsockname()[1]
self._instances[real_port] = self._Instance(server, thread)
return server
2015-09-26 12:47:29 -04:00
def stop(self, port):
2015-10-04 13:21:13 -04:00
"""Stop ACME server running on the specified ``port``.
:param int port:
"""
instance = self._instances[port]
logger.debug("Stopping server at %s:%d...",
2015-10-20 16:44:18 -04:00
*instance.server.socket.getsockname()[:2])
instance.server.shutdown()
2016-02-08 20:52:12 -05:00
# Not calling server_close causes problems when renewing multiple
# certs with `letsencrypt renew` using TLSSNI01 and PyOpenSSL 0.13
instance.server.server_close()
instance.thread.join()
del self._instances[port]
2015-10-04 13:21:13 -04:00
def running(self):
"""Return all running instances.
Once the server is stopped using `stop`, it will not be
returned.
:returns: Mapping from ``port`` to ``server``.
2015-10-04 13:21:13 -04:00
:rtype: tuple
2015-09-26 12:47:29 -04:00
2015-10-04 13:21:13 -04:00
"""
return dict((port, instance.server) for port, instance
in six.iteritems(self._instances))
2015-09-26 12:47:29 -04:00
SUPPORTED_CHALLENGES = [challenges.TLSSNI01, challenges.HTTP01]
2015-10-04 14:53:36 -04:00
def supported_challenges_validator(data):
2015-10-04 16:24:53 -04:00
"""Supported challenges validator for the `argparse`.
It should be passed as `type` argument to `add_argument`.
"""
2015-10-04 14:53:36 -04:00
challs = data.split(",")
unrecognized = [name for name in challs
if name not in challenges.Challenge.TYPES]
if unrecognized:
raise argparse.ArgumentTypeError(
"Unrecognized challenges: {0}".format(", ".join(unrecognized)))
choices = set(chall.typ for chall in SUPPORTED_CHALLENGES)
if not set(challs).issubset(choices):
raise argparse.ArgumentTypeError(
"Plugin does not support the following (valid) "
2015-10-04 16:24:53 -04:00
"challenges: {0}".format(", ".join(set(challs) - choices)))
2015-10-04 14:53:36 -04:00
return data
@zope.interface.implementer(interfaces.IAuthenticator)
@zope.interface.provider(interfaces.IPluginFactory)
2015-09-26 12:47:29 -04:00
class Authenticator(common.Plugin):
"""Standalone Authenticator.
This authenticator creates its own ephemeral TCP listener on the
2015-11-01 07:22:19 -05:00
necessary port in order to respond to incoming tls-sni-01 and http-01
2015-09-26 12:47:29 -04:00
challenges from the certificate authority. Therefore, it does not
rely on any existing server program.
"""
2015-10-28 22:13:41 -04:00
description = "Automatically use a temporary webserver"
2015-09-26 12:47:29 -04:00
def __init__(self, *args, **kwargs):
super(Authenticator, self).__init__(*args, **kwargs)
2015-11-01 07:22:19 -05:00
# one self-signed key for all tls-sni-01 certificates
2015-09-26 12:47:29 -04:00
self.key = OpenSSL.crypto.PKey()
2015-12-16 22:46:56 -05:00
self.key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048)
2015-09-26 12:47:29 -04:00
self.served = collections.defaultdict(set)
# Stuff below is shared across threads (i.e. servers read
# values, main thread writes). Due to the nature of CPython's
2015-09-26 12:47:29 -04:00
# GIL, the operations are safe, c.f.
# https://docs.python.org/2/faq/library.html#what-kinds-of-global-value-mutation-are-thread-safe
self.certs = {}
self.http_01_resources = set()
2015-09-26 12:47:29 -04:00
self.servers = ServerManager(self.certs, self.http_01_resources)
2015-09-26 12:47:29 -04:00
2015-10-04 14:53:36 -04:00
@classmethod
def add_parser_arguments(cls, add):
2015-12-03 01:50:32 -05:00
add("supported-challenges",
help="Supported challenges. Preferred in the order they are listed.",
2015-12-03 01:50:32 -05:00
type=supported_challenges_validator,
default=",".join(chall.typ for chall in SUPPORTED_CHALLENGES))
2015-10-04 14:53:36 -04:00
@property
def supported_challenges(self):
2015-10-04 15:27:47 -04:00
"""Challenges supported by this plugin."""
return [challenges.Challenge.TYPES[name] for name in
self.conf("supported-challenges").split(",")]
2015-10-04 14:53:36 -04:00
@property
def _necessary_ports(self):
necessary_ports = set()
2015-10-28 17:27:04 -04:00
if challenges.HTTP01 in self.supported_challenges:
necessary_ports.add(self.config.http01_port)
2015-11-01 07:22:19 -05:00
if challenges.TLSSNI01 in self.supported_challenges:
2015-11-07 09:21:58 -05:00
necessary_ports.add(self.config.tls_sni_01_port)
return necessary_ports
2015-09-26 12:47:29 -04:00
def more_info(self): # pylint: disable=missing-docstring
2015-10-28 13:49:05 -04:00
return("This authenticator creates its own ephemeral TCP listener "
2015-11-01 07:22:19 -05:00
"on the necessary port in order to respond to incoming "
"tls-sni-01 and http-01 challenges from the certificate "
"authority. Therefore, it does not rely on any existing "
"server program.")
2015-09-26 12:47:29 -04:00
def prepare(self): # pylint: disable=missing-docstring
pass
2015-09-26 12:47:29 -04:00
def get_chall_pref(self, domain):
# pylint: disable=unused-argument,missing-docstring
return self.supported_challenges
2015-09-26 12:47:29 -04:00
def perform(self, achalls): # pylint: disable=missing-docstring
renewer = self.config.verb == "renew"
if any(util.already_listening(port, renewer) for port in self._necessary_ports):
raise errors.MisconfigurationError(
"At least one of the (possibly) required ports is "
"already taken.")
2015-09-26 12:47:29 -04:00
try:
return self.perform2(achalls)
except errors.StandaloneBindError as error:
display = zope.component.getUtility(interfaces.IDisplay)
if error.socket_error.errno == socket.errno.EACCES:
display.notification(
"Could not bind TCP port {0} because you don't have "
"the appropriate permissions (for example, you "
"aren't running this program as "
"root).".format(error.port))
elif error.socket_error.errno == socket.errno.EADDRINUSE:
display.notification(
"Could not bind TCP port {0} because it is already in "
"use by another process on this system (such as a web "
"server). Please stop the program in question and then "
"try again.".format(error.port))
else:
raise # XXX: How to handle unknown errors in binding?
def perform2(self, achalls):
"""Perform achallenges without IDisplay interaction."""
responses = []
for achall in achalls:
2015-10-28 17:27:04 -04:00
if isinstance(achall.chall, challenges.HTTP01):
server = self.servers.run(
self.config.http01_port, challenges.HTTP01)
2015-10-28 17:27:04 -04:00
response, validation = achall.response_and_validation()
self.http_01_resources.add(
2015-10-28 17:27:04 -04:00
acme_standalone.HTTP01RequestHandler.HTTP01Resource(
2015-09-26 12:47:29 -04:00
chall=achall.chall, response=response,
validation=validation))
2015-11-01 07:22:19 -05:00
else: # tls-sni-01
server = self.servers.run(
2015-11-07 09:21:58 -05:00
self.config.tls_sni_01_port, challenges.TLSSNI01)
2015-11-01 07:22:19 -05:00
response, (cert, _) = achall.response_and_validation(
cert_key=self.key)
self.certs[response.z_domain] = (self.key, cert)
2015-09-26 12:47:29 -04:00
self.served[server].add(achall)
responses.append(response)
return responses
def cleanup(self, achalls): # pylint: disable=missing-docstring
# reduce self.served and close servers if none challenges are served
for server, server_achalls in self.served.items():
for achall in achalls:
if achall in server_achalls:
server_achalls.remove(achall)
for port, server in six.iteritems(self.servers.running()):
2015-09-26 12:47:29 -04:00
if not self.served[server]:
self.servers.stop(port)