mirror of
https://github.com/certbot/certbot.git
synced 2026-05-28 04:34:11 -04:00
Cleanup after #232
This commit is contained in:
parent
2484d2e192
commit
95fb2146c4
3 changed files with 92 additions and 87 deletions
5
docs/api/client/standalone_authenticator.rst
Normal file
5
docs/api/client/standalone_authenticator.rst
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
:mod:`letsencrypt.client.standalone_authenticator`
|
||||
--------------------------------------------------
|
||||
|
||||
.. automodule:: letsencrypt.client.standalone_authenticator
|
||||
:members:
|
||||
|
|
@ -1,12 +1,4 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""An authenticator that doesn't rely on any existing server program.
|
||||
|
||||
This authenticator creates its own ephemeral TCP listener on the specified
|
||||
port in order to respond to incoming DVSNI challenges from the certificate
|
||||
authority."""
|
||||
|
||||
"""Standalone authenticator."""
|
||||
import os
|
||||
import signal
|
||||
import socket
|
||||
|
|
@ -19,18 +11,21 @@ import OpenSSL.SSL
|
|||
import zope.component
|
||||
import zope.interface
|
||||
|
||||
from letsencrypt.client import challenge_util
|
||||
from letsencrypt.client import constants
|
||||
from letsencrypt.client import challenge_util
|
||||
from letsencrypt.client import interfaces
|
||||
|
||||
|
||||
class StandaloneAuthenticator(object):
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
"""The StandaloneAuthenticator class itself.
|
||||
"""Standalone authenticator.
|
||||
|
||||
This authenticator can be invoked by the Let's Encrypt client
|
||||
according to the IAuthenticator API interface. It creates a local
|
||||
TCP listener on a specified port and satisfies DVSNI challenges."""
|
||||
This authenticator creates its own ephemeral TCP listener on the
|
||||
specified port in order to respond to incoming DVSNI challenges from
|
||||
the certificate authority. Therefore, it does not rely on any
|
||||
existing server program.
|
||||
|
||||
"""
|
||||
zope.interface.implements(interfaces.IAuthenticator)
|
||||
|
||||
def __init__(self):
|
||||
|
|
@ -49,10 +44,12 @@ class StandaloneAuthenticator(object):
|
|||
This handler receives inter-process communication from the
|
||||
child process in the form of Unix signals.
|
||||
|
||||
:param int sig: Which signal the process received."""
|
||||
# subprocess → client READY : SIGIO
|
||||
# subprocess → client INUSE : SIGUSR1
|
||||
# subprocess → client CANTBIND: SIGUSR2
|
||||
:param int sig: Which signal the process received.
|
||||
|
||||
"""
|
||||
# subprocess to client READY: SIGIO
|
||||
# subprocess to client INUSE: SIGUSR1
|
||||
# subprocess to client CANTBIND: SIGUSR2
|
||||
if sig == signal.SIGIO:
|
||||
self.subproc_state = "ready"
|
||||
elif sig == signal.SIGUSR1:
|
||||
|
|
@ -69,8 +66,10 @@ class StandaloneAuthenticator(object):
|
|||
This handler receives inter-process communication from the parent
|
||||
process in the form of Unix signals.
|
||||
|
||||
:param int sig: Which signal the process received."""
|
||||
# client → subprocess CLEANUP : SIGINT
|
||||
:param int sig: Which signal the process received.
|
||||
|
||||
"""
|
||||
# client to subprocess CLEANUP : SIGINT
|
||||
if sig == signal.SIGINT:
|
||||
try:
|
||||
self.ssl_conn.shutdown()
|
||||
|
|
@ -91,6 +90,7 @@ class StandaloneAuthenticator(object):
|
|||
# reported here and none of them should impede us from
|
||||
# exiting as gracefully as possible.
|
||||
pass
|
||||
|
||||
os.kill(self.parent_pid, signal.SIGUSR1)
|
||||
sys.exit(0)
|
||||
|
||||
|
|
@ -101,18 +101,20 @@ class StandaloneAuthenticator(object):
|
|||
connection when an incoming connection provides an SNI name
|
||||
(in order to serve the appropriate certificate, if any).
|
||||
|
||||
:param OpenSSL.Connection connection: The TLS connection object
|
||||
on which the SNI extension was received."""
|
||||
:param connection: The TLS connection object on which the SNI
|
||||
extension was received.
|
||||
:type connection: :class:`OpenSSL.Connection`
|
||||
|
||||
"""
|
||||
sni_name = connection.get_servername()
|
||||
if sni_name in self.tasks:
|
||||
pem_cert = self.tasks[sni_name]
|
||||
else:
|
||||
# TODO: Should we really present a certificate if we get an
|
||||
# unexpected SNI name? Or should we just disconnect?
|
||||
# unexpected SNI name? Or should we just disconnect?
|
||||
pem_cert = self.tasks.values()[0]
|
||||
cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM,
|
||||
pem_cert)
|
||||
cert = OpenSSL.crypto.load_certificate(
|
||||
OpenSSL.crypto.FILETYPE_PEM, pem_cert)
|
||||
new_ctx = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_METHOD)
|
||||
new_ctx.set_verify(OpenSSL.SSL.VERIFY_NONE, lambda: False)
|
||||
new_ctx.use_certificate(cert)
|
||||
|
|
@ -122,32 +124,36 @@ class StandaloneAuthenticator(object):
|
|||
def do_parent_process(self, port, delay_amount=5):
|
||||
"""Perform the parent process side of the TCP listener task.
|
||||
|
||||
This should only be called by start_listener(). We will wait
|
||||
up to delay_amount seconds to hear from the child process via
|
||||
a signal.
|
||||
This should only be called by :meth:`start_listener`. We will
|
||||
wait up to delay_amount seconds to hear from the child process
|
||||
via a signal.
|
||||
|
||||
:param int port: Which TCP port to bind.
|
||||
:param float delay_amount: How long in seconds to wait for the
|
||||
subprocess to notify us whether it succeeded.
|
||||
subprocess to notify us whether it succeeded.
|
||||
|
||||
:returns: True or False according to whether we were notified
|
||||
that the child process succeeded or failed in binding the port."""
|
||||
:returns: ``True`` or ``False`` according to whether we were notified
|
||||
that the child process succeeded or failed in binding the port.
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
signal.signal(signal.SIGIO, self.client_signal_handler)
|
||||
signal.signal(signal.SIGUSR1, self.client_signal_handler)
|
||||
signal.signal(signal.SIGUSR2, self.client_signal_handler)
|
||||
|
||||
display = zope.component.getUtility(interfaces.IDisplay)
|
||||
|
||||
start_time = time.time()
|
||||
while time.time() < start_time + delay_amount:
|
||||
if self.subproc_state == "ready":
|
||||
return True
|
||||
if self.subproc_state == "inuse":
|
||||
elif self.subproc_state == "inuse":
|
||||
display.generic_notification(
|
||||
"Could not bind TCP port {0} because it is already in "
|
||||
"use it is already in use by another process on this "
|
||||
"system (such as a web server).".format(port))
|
||||
return False
|
||||
if self.subproc_state == "cantbind":
|
||||
elif self.subproc_state == "cantbind":
|
||||
display.generic_notification(
|
||||
"Could not bind TCP port {0} because you don't have "
|
||||
"the appropriate permissions (for example, you "
|
||||
|
|
@ -155,23 +161,28 @@ class StandaloneAuthenticator(object):
|
|||
"root).".format(port))
|
||||
return False
|
||||
time.sleep(0.1)
|
||||
|
||||
display.generic_notification(
|
||||
"Subprocess unexpectedly timed out while trying to bind TCP "
|
||||
"port {0}.".format(port))
|
||||
|
||||
return False
|
||||
|
||||
def do_child_process(self, port, key):
|
||||
"""Perform the child process side of the TCP listener task.
|
||||
|
||||
This should only be called by start_listener().
|
||||
This should only be called by :meth:`start_listener`.
|
||||
|
||||
Normally does not return; instead, the child process exits from
|
||||
within this function or from within the child process signal
|
||||
handler.
|
||||
|
||||
:param int port: Which TCP port to bind.
|
||||
:param le_util.Key key: The private key to use to respond to
|
||||
DVSNI challenge requests."""
|
||||
:param key: The private key to use to respond to DVSNI challenge
|
||||
requests.
|
||||
:type key: `letsencrypt.client.le_util.Key`
|
||||
|
||||
"""
|
||||
signal.signal(signal.SIGINT, self.subproc_signal_handler)
|
||||
self.sock = socket.socket()
|
||||
try:
|
||||
|
|
@ -217,14 +228,20 @@ class StandaloneAuthenticator(object):
|
|||
self.ssl_conn.close()
|
||||
|
||||
def start_listener(self, port, key):
|
||||
"""Create a child process which will start a TCP listener on the
|
||||
"""Start listener.
|
||||
|
||||
Create a child process which will start a TCP listener on the
|
||||
specified port to perform the specified DVSNI challenges.
|
||||
|
||||
:param int port: The TCP port to bind.
|
||||
:param le_util.Key key: The private key to use to respond to
|
||||
DVSNI challenge requests.
|
||||
:returns: True or False to indicate success or failure creating
|
||||
the subprocess.
|
||||
:param key: The private key to use to respond to DVSNI challenge
|
||||
requests.
|
||||
:type key: :class:`letsencrypt.client.le_util.Key`
|
||||
|
||||
:returns: ``True`` or ``False`` to indicate success or failure creating
|
||||
the subprocess.
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
fork_result = os.fork()
|
||||
Crypto.Random.atfork()
|
||||
|
|
@ -243,33 +260,21 @@ class StandaloneAuthenticator(object):
|
|||
|
||||
# IAuthenticator method implementations follow
|
||||
|
||||
def get_chall_pref(self, unused_domain):
|
||||
# pylint: disable=no-self-use
|
||||
"""IAuthenticator interface method get_chall_pref.
|
||||
|
||||
Return a list of challenge types that this authenticator
|
||||
can perform for this domain. In the case of the
|
||||
StandaloneAuthenticator, the only challenge type that can ever
|
||||
be performed is dvsni.
|
||||
|
||||
:returns: A list containing only 'dvsni'."""
|
||||
def get_chall_pref(self, unused_domain): # pylint: disable=no-self-use
|
||||
"""Get challenge preferences."""
|
||||
return ["dvsni"]
|
||||
|
||||
def perform(self, chall_list):
|
||||
"""IAuthenticator interface method perform.
|
||||
"""Perform the challege.
|
||||
|
||||
Attempt to perform the
|
||||
specified challenges, returning the status of each. For the
|
||||
StandaloneAuthenticator, because there is no convenient way to add
|
||||
additional requests, this should only be invoked once; subsequent
|
||||
invocations are an error. To perform validations for multiple
|
||||
independent sets of domains, a separate StandaloneAuthenticator
|
||||
should be instantiated.
|
||||
.. warning::
|
||||
For the StandaloneAuthenticator, because there is no convenient
|
||||
way to add additional requests, this should only be invoked
|
||||
once; subsequent invocations are an error. To perform
|
||||
validations for multiple independent sets of domains, a separate
|
||||
StandaloneAuthenticator should be instantiated.
|
||||
|
||||
:param list chall_list: A list of the the challenge objects to
|
||||
be attempted by this authenticator.
|
||||
:returns: A list in the same order containing, in each position,
|
||||
the successfully configured challenge, False, or None."""
|
||||
"""
|
||||
if self.child_pid or self.tasks:
|
||||
# We should not be willing to continue with perform
|
||||
# if there were existing pending challenges.
|
||||
|
|
@ -305,17 +310,14 @@ class StandaloneAuthenticator(object):
|
|||
return results_if_failure
|
||||
|
||||
def cleanup(self, chall_list):
|
||||
"""IAuthenticator interface method cleanup.
|
||||
"""Clean up.
|
||||
|
||||
Remove each of the specified challenges from the list of
|
||||
challenges that still need to be performed. (In the case of
|
||||
the StandaloneAuthenticator, if some challenges are removed
|
||||
from the list, the authenticator socket will still respond to
|
||||
those challenges.) Once all challenges have been removed from
|
||||
the list, the listener is deactivated and stops listening.
|
||||
If some challenges are removed from the list, the authenticator
|
||||
socket will still respond to those challenges. Once all
|
||||
challenges have been removed from the list, the listener is
|
||||
deactivated and stops listening.
|
||||
|
||||
:param list chall_list: A list of the the challenge objects to
|
||||
be deactivated."""
|
||||
"""
|
||||
# Remove this from pending tasks list
|
||||
for chall in chall_list:
|
||||
assert isinstance(chall, challenge_util.DvsniChall)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
"""Tests for standalone_authenticator.py."""
|
||||
import mock
|
||||
import unittest
|
||||
|
||||
"""Tests for letsencrypt.client.standalone_authenticator."""
|
||||
import os
|
||||
import pkg_resources
|
||||
import signal
|
||||
import socket
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
import OpenSSL.crypto
|
||||
import OpenSSL.SSL
|
||||
|
||||
|
|
@ -20,7 +17,7 @@ from letsencrypt.client import le_util
|
|||
# after one iteration, based on.
|
||||
# http://igorsobreira.com/2013/03/17/testing-infinite-loops.html
|
||||
|
||||
class SocketAcceptOnlyNTimes(object):
|
||||
class _SocketAcceptOnlyNTimes(object):
|
||||
# pylint: disable=too-few-public-methods
|
||||
"""
|
||||
Callable that will raise `CallableExhausted`
|
||||
|
|
@ -39,6 +36,7 @@ class SocketAcceptOnlyNTimes(object):
|
|||
# Modified here for a single use as socket.accept()
|
||||
return (mock.MagicMock(), "ignored")
|
||||
|
||||
|
||||
class CallableExhausted(Exception):
|
||||
# pylint: disable=too-few-public-methods
|
||||
"""Exception raised when a method is called more than the
|
||||
|
|
@ -65,7 +63,7 @@ class SNICallbackTest(unittest.TestCase):
|
|||
self.authenticator = StandaloneAuthenticator()
|
||||
name, r_b64 = "example.com", le_util.jose_b64encode("x" * 32)
|
||||
test_key = pkg_resources.resource_string(
|
||||
__name__, 'testdata/rsa256_key.pem')
|
||||
__name__, "testdata/rsa256_key.pem")
|
||||
nonce, key = "abcdef", le_util.Key("foo", test_key)
|
||||
self.cert = challenge_util.dvsni_gen_cert(name, r_b64, nonce, key)[0]
|
||||
private_key = OpenSSL.crypto.load_privatekey(
|
||||
|
|
@ -189,7 +187,7 @@ class PerformTest(unittest.TestCase):
|
|||
def test_can_perform(self):
|
||||
"""What happens if start_listener() returns True."""
|
||||
test_key = pkg_resources.resource_string(
|
||||
__name__, 'testdata/rsa256_key.pem')
|
||||
__name__, "testdata/rsa256_key.pem")
|
||||
key = le_util.Key("something", test_key)
|
||||
chall1 = challenge_util.DvsniChall(
|
||||
"foo.example.com", "whee", "foononce", key)
|
||||
|
|
@ -216,7 +214,7 @@ class PerformTest(unittest.TestCase):
|
|||
def test_cannot_perform(self):
|
||||
"""What happens if start_listener() returns False."""
|
||||
test_key = pkg_resources.resource_string(
|
||||
__name__, 'testdata/rsa256_key.pem')
|
||||
__name__, "testdata/rsa256_key.pem")
|
||||
key = le_util.Key("something", test_key)
|
||||
chall1 = challenge_util.DvsniChall(
|
||||
"foo.example.com", "whee", "foononce", key)
|
||||
|
|
@ -347,7 +345,7 @@ class DoChildProcessTest(unittest.TestCase):
|
|||
self.authenticator = StandaloneAuthenticator()
|
||||
name, r_b64 = "example.com", le_util.jose_b64encode("x" * 32)
|
||||
test_key = pkg_resources.resource_string(
|
||||
__name__, 'testdata/rsa256_key.pem')
|
||||
__name__, "testdata/rsa256_key.pem")
|
||||
nonce, key = "abcdef", le_util.Key("foo", test_key)
|
||||
self.key = key
|
||||
self.cert = challenge_util.dvsni_gen_cert(name, r_b64, nonce, key)[0]
|
||||
|
|
@ -412,10 +410,10 @@ class DoChildProcessTest(unittest.TestCase):
|
|||
"OpenSSL.SSL.Connection")
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator.socket.socket")
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator.os.kill")
|
||||
def test_do_child_process_success(self, mock_kill, mock_socket,
|
||||
mock_connection):
|
||||
def test_do_child_process_success(
|
||||
self, mock_kill, mock_socket, mock_connection):
|
||||
sample_socket = mock.MagicMock()
|
||||
sample_socket.accept.side_effect = SocketAcceptOnlyNTimes(2)
|
||||
sample_socket.accept.side_effect = _SocketAcceptOnlyNTimes(2)
|
||||
mock_socket.return_value = sample_socket
|
||||
mock_connection.return_value = mock.MagicMock()
|
||||
self.assertRaises(
|
||||
|
|
@ -457,5 +455,5 @@ class CleanupTest(unittest.TestCase):
|
|||
self.assertRaises(ValueError, self.authenticator.cleanup, [chall])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
Loading…
Reference in a new issue