diff --git a/.gitattributes b/.gitattributes index 5eee84cce..0f8bbbfb5 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,7 +1,16 @@ -* text=auto eol=lf +#Default, normalize CRLF into LF in non-binary files +# Files identified as binary by Git are not changed +* crlf=auto # special files +*.sh crlf=input +*.py crlf=input + *.bat text eol=crlf + +*.der binary +*.gz binary *.jpeg binary *.jpg binary *.png binary +*.gz binary diff --git a/acme/.pylintrc b/acme/.pylintrc index d0d150631..06bb2a01f 100644 --- a/acme/.pylintrc +++ b/acme/.pylintrc @@ -21,12 +21,6 @@ persistent=yes # usually to register additional checkers. load-plugins=linter_plugin -# DEPRECATED -include-ids=no - -# DEPRECATED -symbols=no - # Use multiple processes to speed up Pylint. jobs=1 diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 280bc8308..c436cc631 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -500,7 +500,7 @@ class DNS(_TokenChallenge): """ return DNSResponse(validation=self.gen_validation( - self, account_key, **kwargs)) + account_key, **kwargs)) def validation_domain_name(self, name): """Domain name for TXT validation record. diff --git a/acme/acme/client_test.py b/acme/acme/client_test.py index 33e80aab7..a526a0984 100644 --- a/acme/acme/client_test.py +++ b/acme/acme/client_test.py @@ -572,7 +572,7 @@ class ClientNetworkTest(unittest.TestCase): sess = mock.MagicMock() self.net.session = sess del self.net - sess.close.assert_called_once() + sess.close.assert_called_once_with() @mock.patch('acme.client.requests') def test_requests_error_passthrough(self, mock_requests): diff --git a/acme/setup.py b/acme/setup.py index d38864dc1..ed133e128 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.7.0.dev0' +version = '0.9.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-apache/certbot_apache/augeas_configurator.py b/certbot-apache/certbot_apache/augeas_configurator.py index 12753541c..6999120d6 100644 --- a/certbot-apache/certbot_apache/augeas_configurator.py +++ b/certbot-apache/certbot_apache/augeas_configurator.py @@ -1,7 +1,6 @@ """Class of Augeas Configurators.""" import logging -import augeas from certbot import errors from certbot import reverter @@ -29,12 +28,9 @@ class AugeasConfigurator(common.Plugin): def __init__(self, *args, **kwargs): super(AugeasConfigurator, self).__init__(*args, **kwargs) - self.aug = augeas.Augeas( - # specify a directory to load our preferred lens from - loadpath=constants.AUGEAS_LENS_DIR, - # Do not save backup (we do it ourselves), do not load - # anything by default - flags=(augeas.Augeas.NONE | augeas.Augeas.NO_MODL_AUTOLOAD)) + # Placeholder for augeas + self.aug = None + self.save_notes = "" # See if any temporary changes need to be recovered @@ -42,6 +38,16 @@ class AugeasConfigurator(common.Plugin): # because this will change the underlying configuration and potential # vhosts self.reverter = reverter.Reverter(self.config) + + def init_augeas(self): + """ Initialize the actual Augeas instance """ + import augeas + self.aug = augeas.Augeas( + # specify a directory to load our preferred lens from + loadpath=constants.AUGEAS_LENS_DIR, + # Do not save backup (we do it ourselves), do not load + # anything by default + flags=(augeas.Augeas.NONE | augeas.Augeas.NO_MODL_AUTOLOAD)) self.recovery_routine() def check_parsing_errors(self, lens): diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 1e02ae7b3..9caa4a764 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -15,7 +15,7 @@ from acme import challenges from certbot import errors from certbot import interfaces -from certbot import le_util +from certbot import util from certbot.plugins import common @@ -106,8 +106,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): add("handle-sites", default=constants.os_constant("handle_sites"), help="Let installer handle enabling sites for you." + "(Only Ubuntu/Debian currently)") - le_util.add_deprecated_argument(add, argument_name="ctl", nargs=1) - le_util.add_deprecated_argument( + util.add_deprecated_argument(add, argument_name="ctl", nargs=1) + util.add_deprecated_argument( add, argument_name="init-script", nargs=1) def __init__(self, *args, **kwargs): @@ -150,8 +150,14 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): :raises .errors.PluginError: If there is any other error """ + # Perform the actual Augeas initialization to be able to react + try: + self.init_augeas() + except ImportError: + raise errors.NoInstallationError("Problem in Augeas installation") + # Verify Apache is installed - if not le_util.exe_exists(constants.os_constant("restart_cmd")[0]): + if not util.exe_exists(constants.os_constant("restart_cmd")[0]): raise errors.NoInstallationError # Make sure configuration is valid @@ -1521,14 +1527,14 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Generate reversal command. # Try to be safe here... check that we can probably reverse before # applying enmod command - if not le_util.exe_exists(self.conf("dismod")): + if not util.exe_exists(self.conf("dismod")): raise errors.MisconfigurationError( "Unable to find a2dismod, please make sure a2enmod and " "a2dismod are configured correctly for certbot.") self.reverter.register_undo_command( temp, [self.conf("dismod"), mod_name]) - le_util.run_script([self.conf("enmod"), mod_name]) + util.run_script([self.conf("enmod"), mod_name]) def restart(self): """Runs a config test and reloads the Apache server. @@ -1547,7 +1553,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ try: - le_util.run_script(constants.os_constant("restart_cmd")) + util.run_script(constants.os_constant("restart_cmd")) except errors.SubprocessError as err: raise errors.MisconfigurationError(str(err)) @@ -1558,7 +1564,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ try: - le_util.run_script(constants.os_constant("conftest_cmd")) + util.run_script(constants.os_constant("conftest_cmd")) except errors.SubprocessError as err: raise errors.MisconfigurationError(str(err)) @@ -1574,8 +1580,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ try: - stdout, _ = le_util.run_script( - constants.os_constant("version_cmd")) + stdout, _ = util.run_script(constants.os_constant("version_cmd")) except errors.SubprocessError: raise errors.PluginError( "Unable to run %s -v" % diff --git a/certbot-apache/certbot_apache/constants.py b/certbot-apache/certbot_apache/constants.py index f3226572c..9252814c4 100644 --- a/certbot-apache/certbot_apache/constants.py +++ b/certbot-apache/certbot_apache/constants.py @@ -1,6 +1,6 @@ """Apache plugin constants.""" import pkg_resources -from certbot import le_util +from certbot import util CLI_DEFAULTS_DEBIAN = dict( @@ -78,6 +78,9 @@ CLI_DEFAULTS = { "centos linux": CLI_DEFAULTS_CENTOS, "fedora": CLI_DEFAULTS_CENTOS, "red hat enterprise linux server": CLI_DEFAULTS_CENTOS, + "rhel": CLI_DEFAULTS_CENTOS, + "amazon": CLI_DEFAULTS_CENTOS, + "gentoo": CLI_DEFAULTS_GENTOO, "gentoo base system": CLI_DEFAULTS_GENTOO, "darwin": CLI_DEFAULTS_DARWIN, } @@ -116,7 +119,7 @@ def os_constant(key): :param key: name of cli constant :return: value of constant for active os """ - os_info = le_util.get_os_info() + os_info = util.get_os_info() try: constants = CLI_DEFAULTS[os_info[0].lower()] except KeyError: diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index d7a5fd75a..e5c09fd1d 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -49,14 +49,24 @@ class MultipleVhostsTest(util.ApacheTest): shutil.rmtree(self.config_dir) shutil.rmtree(self.work_dir) - @mock.patch("certbot_apache.configurator.le_util.exe_exists") + @mock.patch("certbot_apache.configurator.util.exe_exists") def test_prepare_no_install(self, mock_exe_exists): mock_exe_exists.return_value = False self.assertRaises( errors.NoInstallationError, self.config.prepare) + @mock.patch("certbot_apache.augeas_configurator.AugeasConfigurator.init_augeas") + def test_prepare_no_augeas(self, mock_init_augeas): + """ Test augeas initialization ImportError """ + def side_effect_error(): + """ Side effect error for the test """ + raise ImportError + mock_init_augeas.side_effect = side_effect_error + self.assertRaises( + errors.NoInstallationError, self.config.prepare) + @mock.patch("certbot_apache.parser.ApacheParser") - @mock.patch("certbot_apache.configurator.le_util.exe_exists") + @mock.patch("certbot_apache.configurator.util.exe_exists") def test_prepare_version(self, mock_exe_exists, _): mock_exe_exists.return_value = True self.config.version = None @@ -67,7 +77,7 @@ class MultipleVhostsTest(util.ApacheTest): errors.NotSupportedError, self.config.prepare) @mock.patch("certbot_apache.parser.ApacheParser") - @mock.patch("certbot_apache.configurator.le_util.exe_exists") + @mock.patch("certbot_apache.configurator.util.exe_exists") def test_prepare_old_aug(self, mock_exe_exists, _): mock_exe_exists.return_value = True self.config.config_test = mock.Mock() @@ -268,8 +278,8 @@ class MultipleVhostsTest(util.ApacheTest): self.config.is_site_enabled, "irrelevant") - @mock.patch("certbot.le_util.run_script") - @mock.patch("certbot.le_util.exe_exists") + @mock.patch("certbot.util.run_script") + @mock.patch("certbot.util.exe_exists") @mock.patch("certbot_apache.parser.subprocess.Popen") def test_enable_mod(self, mock_popen, mock_exe_exists, mock_run_script): mock_popen().communicate.return_value = ("Define: DUMP_RUN_CFG", "") @@ -287,7 +297,7 @@ class MultipleVhostsTest(util.ApacheTest): self.assertRaises( errors.NotSupportedError, self.config.enable_mod, "ssl") - @mock.patch("certbot.le_util.exe_exists") + @mock.patch("certbot.util.exe_exists") def test_enable_mod_no_disable(self, mock_exe_exists): mock_exe_exists.return_value = False self.assertRaises( @@ -695,7 +705,7 @@ class MultipleVhostsTest(util.ApacheTest): self.config.cleanup([achall1, achall2]) self.assertTrue(mock_restart.called) - @mock.patch("certbot.le_util.run_script") + @mock.patch("certbot.util.run_script") def test_get_version(self, mock_script): mock_script.return_value = ( "Server Version: Apache/2.4.2 (Debian)", "") @@ -717,21 +727,21 @@ class MultipleVhostsTest(util.ApacheTest): mock_script.side_effect = errors.SubprocessError("Can't find program") self.assertRaises(errors.PluginError, self.config.get_version) - @mock.patch("certbot_apache.configurator.le_util.run_script") + @mock.patch("certbot_apache.configurator.util.run_script") def test_restart(self, _): self.config.restart() - @mock.patch("certbot_apache.configurator.le_util.run_script") + @mock.patch("certbot_apache.configurator.util.run_script") def test_restart_bad_process(self, mock_run_script): mock_run_script.side_effect = [None, errors.SubprocessError] self.assertRaises(errors.MisconfigurationError, self.config.restart) - @mock.patch("certbot.le_util.run_script") + @mock.patch("certbot.util.run_script") def test_config_test(self, _): self.config.config_test() - @mock.patch("certbot.le_util.run_script") + @mock.patch("certbot.util.run_script") def test_config_test_bad_process(self, mock_run_script): mock_run_script.side_effect = errors.SubprocessError @@ -771,7 +781,7 @@ class MultipleVhostsTest(util.ApacheTest): @mock.patch("certbot_apache.configurator.ApacheConfigurator._get_http_vhost") @mock.patch("certbot_apache.display_ops.select_vhost") - @mock.patch("certbot.le_util.exe_exists") + @mock.patch("certbot.util.exe_exists") def test_enhance_unknown_vhost(self, mock_exe, mock_sel_vhost, mock_get): self.config.parser.modules.add("rewrite_module") mock_exe.return_value = True @@ -792,8 +802,8 @@ class MultipleVhostsTest(util.ApacheTest): errors.PluginError, self.config.enhance, "certbot.demo", "unknown_enhancement") - @mock.patch("certbot.le_util.run_script") - @mock.patch("certbot.le_util.exe_exists") + @mock.patch("certbot.util.run_script") + @mock.patch("certbot.util.exe_exists") def test_ocsp_stapling(self, mock_exe, mock_run_script): self.config.parser.update_runtime_variables = mock.Mock() self.config.parser.modules.add("mod_ssl.c") @@ -821,7 +831,7 @@ class MultipleVhostsTest(util.ApacheTest): self.assertEqual(len(stapling_cache_aug_path), 1) - @mock.patch("certbot.le_util.exe_exists") + @mock.patch("certbot.util.exe_exists") def test_ocsp_stapling_twice(self, mock_exe): self.config.parser.update_runtime_variables = mock.Mock() self.config.parser.modules.add("mod_ssl.c") @@ -848,7 +858,7 @@ class MultipleVhostsTest(util.ApacheTest): self.assertEqual(len(stapling_cache_aug_path), 1) - @mock.patch("certbot.le_util.exe_exists") + @mock.patch("certbot.util.exe_exists") def test_ocsp_unsupported_apache_version(self, mock_exe): mock_exe.return_value = True self.config.parser.update_runtime_variables = mock.Mock() @@ -871,8 +881,8 @@ class MultipleVhostsTest(util.ApacheTest): http_vh = self.config._get_http_vhost(ssl_vh) self.assertTrue(http_vh.ssl == False) - @mock.patch("certbot.le_util.run_script") - @mock.patch("certbot.le_util.exe_exists") + @mock.patch("certbot.util.run_script") + @mock.patch("certbot.util.exe_exists") def test_http_header_hsts(self, mock_exe, _): self.config.parser.update_runtime_variables = mock.Mock() self.config.parser.modules.add("mod_ssl.c") @@ -909,8 +919,8 @@ class MultipleVhostsTest(util.ApacheTest): self.config.enhance, "encryption-example.demo", "ensure-http-header", "Strict-Transport-Security") - @mock.patch("certbot.le_util.run_script") - @mock.patch("certbot.le_util.exe_exists") + @mock.patch("certbot.util.run_script") + @mock.patch("certbot.util.exe_exists") def test_http_header_uir(self, mock_exe, _): self.config.parser.update_runtime_variables = mock.Mock() self.config.parser.modules.add("mod_ssl.c") @@ -947,8 +957,8 @@ class MultipleVhostsTest(util.ApacheTest): self.config.enhance, "encryption-example.demo", "ensure-http-header", "Upgrade-Insecure-Requests") - @mock.patch("certbot.le_util.run_script") - @mock.patch("certbot.le_util.exe_exists") + @mock.patch("certbot.util.run_script") + @mock.patch("certbot.util.exe_exists") def test_redirect_well_formed_http(self, mock_exe, _): self.config.parser.update_runtime_variables = mock.Mock() mock_exe.return_value = True @@ -991,8 +1001,8 @@ class MultipleVhostsTest(util.ApacheTest): # pylint: disable=protected-access self.assertTrue(self.config._is_rewrite_engine_on(self.vh_truth[3])) - @mock.patch("certbot.le_util.run_script") - @mock.patch("certbot.le_util.exe_exists") + @mock.patch("certbot.util.run_script") + @mock.patch("certbot.util.exe_exists") def test_redirect_with_existing_rewrite(self, mock_exe, _): self.config.parser.update_runtime_variables = mock.Mock() mock_exe.return_value = True diff --git a/certbot-apache/certbot_apache/tests/constants_test.py b/certbot-apache/certbot_apache/tests/constants_test.py index d970c96be..c040030df 100644 --- a/certbot-apache/certbot_apache/tests/constants_test.py +++ b/certbot-apache/certbot_apache/tests/constants_test.py @@ -8,19 +8,19 @@ from certbot_apache import constants class ConstantsTest(unittest.TestCase): - @mock.patch("certbot.le_util.get_os_info") + @mock.patch("certbot.util.get_os_info") def test_get_debian_value(self, os_info): os_info.return_value = ('Debian', '', '') self.assertEqual(constants.os_constant("vhost_root"), "/etc/apache2/sites-available") - @mock.patch("certbot.le_util.get_os_info") + @mock.patch("certbot.util.get_os_info") def test_get_centos_value(self, os_info): os_info.return_value = ('CentOS Linux', '', '') self.assertEqual(constants.os_constant("vhost_root"), "/etc/httpd/conf.d") - @mock.patch("certbot.le_util.get_os_info") + @mock.patch("certbot.util.get_os_info") def test_get_default_value(self, os_info): os_info.return_value = ('Nonexistent Linux', '', '') self.assertEqual(constants.os_constant("vhost_root"), diff --git a/certbot-apache/certbot_apache/tests/tls_sni_01_test.py b/certbot-apache/certbot_apache/tests/tls_sni_01_test.py index aa6a2a09c..5e369e3db 100644 --- a/certbot-apache/certbot_apache/tests/tls_sni_01_test.py +++ b/certbot-apache/certbot_apache/tests/tls_sni_01_test.py @@ -38,8 +38,8 @@ class TlsSniPerformTest(util.ApacheTest): resp = self.sni.perform() self.assertEqual(len(resp), 0) - @mock.patch("certbot.le_util.exe_exists") - @mock.patch("certbot.le_util.run_script") + @mock.patch("certbot.util.exe_exists") + @mock.patch("certbot.util.run_script") def test_perform1(self, _, mock_exists): mock_register = mock.Mock() self.sni.configurator.reverter.register_undo_command = mock_register diff --git a/certbot-apache/certbot_apache/tests/util.py b/certbot-apache/certbot_apache/tests/util.py index 8935ee908..050876687 100644 --- a/certbot-apache/certbot_apache/tests/util.py +++ b/certbot-apache/certbot_apache/tests/util.py @@ -95,8 +95,8 @@ def get_apache_configurator( in_progress_dir=os.path.join(backups, "IN_PROGRESS"), work_dir=work_dir) - with mock.patch("certbot_apache.configurator.le_util.run_script"): - with mock.patch("certbot_apache.configurator.le_util." + with mock.patch("certbot_apache.configurator.util.run_script"): + with mock.patch("certbot_apache.configurator.util." "exe_exists") as mock_exe_exists: mock_exe_exists.return_value = True with mock.patch("certbot_apache.parser.ApacheParser." diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py index 56c6a451d..e3dbe4563 100644 --- a/certbot-apache/setup.py +++ b/certbot-apache/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.7.0.dev0' +version = '0.9.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-auto b/certbot-auto index 8c6e6c486..80f39cf59 100755 --- a/certbot-auto +++ b/certbot-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.6.0" +LE_AUTO_VERSION="0.8.1" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -38,17 +38,6 @@ Help for certbot itself cannot be provided until it is installed. All arguments are accepted and forwarded to the Certbot client when run." -while getopts ":hnv" arg; do - case $arg in - h) - HELP=1;; - n) - ASSUME_YES=1;; - v) - VERBOSE=1;; - esac -done - for arg in "$@" ; do case "$arg" in --debug) @@ -65,9 +54,26 @@ for arg in "$@" ; do ASSUME_YES=1;; --verbose) VERBOSE=1;; + -[!-]*) + while getopts ":hnv" short_arg $arg; do + case "$short_arg" in + h) + HELP=1;; + n) + ASSUME_YES=1;; + v) + VERBOSE=1;; + esac + done;; esac done +if [ $BASENAME = "letsencrypt-auto" ]; then + # letsencrypt-auto does not respect --help or --yes for backwards compatibility + ASSUME_YES=1 + HELP=0 +fi + # certbot-auto needs root access to bootstrap OS dependencies, and # certbot itself needs root access for almost all modes of operation # The "normal" case is that sudo is used for the steps that need root, but @@ -107,12 +113,6 @@ else SUDO= fi -if [ $BASENAME = "letsencrypt-auto" ]; then - # letsencrypt-auto does not respect --help or --yes for backwards compatibility - ASSUME_YES=1 - HELP=0 -fi - ExperimentalBootstrap() { # Arguments: Platform name, bootstrap function name if [ "$DEBUG" = 1 ]; then @@ -172,7 +172,7 @@ BootstrapDebCommon() { # distro version (#346) virtualenv= - if apt-cache show virtualenv > /dev/null 2>&1; then + if apt-cache show virtualenv > /dev/null 2>&1 && ! apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then virtualenv="virtualenv" fi @@ -425,7 +425,8 @@ BootstrapMac() { $pkgcmd augeas $pkgcmd dialog - if [ "$(which python)" = "/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python" ]; then + if [ "$(which python)" = "/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python" \ + -o "$(which python)" = "/usr/bin/python" ]; then # We want to avoid using the system Python because it requires root to use pip. # python.org, MacPorts or HomeBrew Python installations should all be OK. echo "Installing python..." @@ -435,7 +436,8 @@ BootstrapMac() { # Workaround for _dlopen not finding augeas on OS X if [ "$pkgman" = "port" ] && ! [ -e "/usr/local/lib/libaugeas.dylib" ] && [ -e "/opt/local/lib/libaugeas.dylib" ]; then echo "Applying augeas workaround" - $SUDO ln -s /opt/local/lib/libaugeas.dylib /usr/local/lib + $SUDO mkdir -p /usr/local/lib/ + $SUDO ln -s /opt/local/lib/libaugeas.dylib /usr/local/lib/ fi if ! hash pip 2>/dev/null; then @@ -451,12 +453,44 @@ BootstrapMac() { fi } +BootstrapSmartOS() { + pkgin update + pkgin -y install 'gcc49' 'py27-augeas' 'py27-virtualenv' +} + +BootstrapMageiaCommon() { + if ! $SUDO urpmi --force \ + python \ + libpython-devel \ + python-virtualenv + then + echo "Could not install Python dependencies. Aborting bootstrap!" + exit 1 + fi + + if ! $SUDO urpmi --force \ + git \ + gcc \ + cdialog \ + python-augeas \ + libopenssl-devel \ + libffi-devel \ + rootcerts + then + echo "Could not install additional dependencies. Aborting bootstrap!" + exit 1 + fi +} + # Install required OS packages: Bootstrap() { if [ -f /etc/debian_version ]; then echo "Bootstrapping dependencies for Debian-based OSes..." BootstrapDebCommon + elif [ -f /etc/mageia-release ] ; then + # Mageia has both /etc/mageia-release and /etc/redhat-release + ExperimentalBootstrap "Mageia" BootstrapMageiaCommon elif [ -f /etc/redhat-release ]; then echo "Bootstrapping dependencies for RedHat-based OSes..." BootstrapRpmCommon @@ -469,7 +503,7 @@ Bootstrap() { BootstrapArchCommon else echo "Please use pacman to install letsencrypt packages:" - echo "# pacman -S letsencrypt letsencrypt-apache" + echo "# pacman -S certbot certbot-apache" echo echo "If you would like to use the virtualenv way, please run the script again with the" echo "--debug flag." @@ -483,14 +517,17 @@ Bootstrap() { ExperimentalBootstrap "FreeBSD" BootstrapFreeBsd elif uname | grep -iq Darwin ; then ExperimentalBootstrap "Mac OS X" BootstrapMac - elif grep -iq "Amazon Linux" /etc/issue ; then + elif [ -f /etc/issue ] && grep -iq "Amazon Linux" /etc/issue ; then ExperimentalBootstrap "Amazon Linux" BootstrapRpmCommon + elif [ -f /etc/product ] && grep -q "Joyent Instance" /etc/product ; then + ExperimentalBootstrap "Joyent SmartOS Zone" BootstrapSmartOS else echo "Sorry, I don't know how to bootstrap Certbot on your operating system!" echo echo "You will need to bootstrap, configure virtualenv, and run pip install manually." echo "Please see https://letsencrypt.readthedocs.org/en/latest/contributing.html#prerequisites" echo "for more info." + exit 1 fi } @@ -523,6 +560,7 @@ if [ "$1" = "--le-auto-phase2" ]; then echo "Installing Python packages..." TEMP_DIR=$(TempDir) + trap 'rm -rf "$TEMP_DIR"' EXIT # There is no $ interpolation due to quotes on starting heredoc delimiter. # ------------------------------------------------------------------------- cat << "UNLIKELY_EOF" > "$TEMP_DIR/letsencrypt-auto-requirements.txt" @@ -703,24 +741,21 @@ zope.interface==4.1.3 \ mock==1.0.1 \ --hash=sha256:b839dd2d9c117c701430c149956918a423a9863b48b09c90e30a6013e7d2f44f \ --hash=sha256:8f83080daa249d036cbccfb8ae5cc6ff007b88d6d937521371afabe7b19badbc +letsencrypt==0.7.0 \ + --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ + --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.6.0 \ - --hash=sha256:cbe4e7a340a19725a8740ed86e30abdbe18fc22c4c6022b7a8e56642d502bcc3 \ - --hash=sha256:ec4e6009dfbd629b58473eb06bbebfd9fb2a79fc8831c149e9205bc38a98ecc6 -certbot==0.6.0 \ - --hash=sha256:a893632d228864b0a751db9f3fdd93439ed34b988ea21b64fb0f0fa2ceded6a2 \ - --hash=sha256:80b0b7dc5afeec2816ef638a61e7c628d73cd72666eebf4984be426d1c2b492d -certbot-apache==0.6.0 \ - --hash=sha256:0ab077f0913b81ed5c1b141c3a7c4c0228ef3738d8d61a93db794d9a80718d43 \ - --hash=sha256:1cfbe751209079a803758f472200816fac559f2a36fdd582d25e3ba5601423a1 -letsencrypt==0.6.0 \ - --hash=sha256:93196c7dcd57272a753e525d145c5a9987c8968c22ec954bcf83dcc9d2499a76 \ - --hash=sha256:a16d6c395f1bf5fd61a28ef83dc78f42dbecbad9d00be6236f2ad8915645c154 -letsencrypt-apache==0.6.0 \ - --hash=sha256:02fadc52a0796e53978c508beec9c53e1fc047660240832b9bde5d53ab3a1379 \ - --hash=sha256:1c5522d94d7750bdb9bfa6201d2c263e914f662c9d0079e673167233cf4364f1 +acme==0.8.1 \ + --hash=sha256:ccd7883772efbf933f91713b8241455993834f3620c8fbd459d9ed5e50bbaaca \ + --hash=sha256:d3ea4acf280bf6253ad7d641cb0970f230a19805acfed809e7a8ddcf62157d9f +certbot==0.8.1 \ + --hash=sha256:89805d9f70249ae859ec4d7a99c00b4bb7083ca90cd12d4d202b76dfc284f7c5 \ + --hash=sha256:6ca8df3d310ced6687d38aac17c0fb8c1b2ec7a3bea156a254e4cc2a1c132771 +certbot-apache==0.8.1 \ + --hash=sha256:c9e3fdc15e65589c2e39eb0e6b1f61f0c0a1db3c17b00bb337f0ff636cc61cb3 \ + --hash=sha256:0faf2879884d3b7a58b071902fba37d4b8b58a50e2c3b8ac262c0a74134045ed UNLIKELY_EOF # ------------------------------------------------------------------------- @@ -880,7 +915,6 @@ UNLIKELY_EOF PIP_OUT=`"$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1` PIP_STATUS=$? set -e - rm -rf "$TEMP_DIR" if [ "$PIP_STATUS" != 0 ]; then # Report error. (Otherwise, be quiet.) echo "Had a problem while installing Python packages:" @@ -890,14 +924,16 @@ UNLIKELY_EOF fi echo "Installation succeeded." fi - echo "Requesting root privileges to run certbot..." + if [ -n "$SUDO" ]; then + # SUDO is su wrapper or sudo + echo "Requesting root privileges to run certbot..." + echo " $VENV_BIN/letsencrypt" "$@" + fi if [ -z "$SUDO_ENV" ] ; then # SUDO is su wrapper / noop - echo " " $SUDO "$VENV_BIN/letsencrypt" "$@" $SUDO "$VENV_BIN/letsencrypt" "$@" else # sudo - echo " " $SUDO "$SUDO_ENV" "$VENV_BIN/letsencrypt" "$@" $SUDO "$SUDO_ENV" "$VENV_BIN/letsencrypt" "$@" fi @@ -923,8 +959,8 @@ else fi if [ "$NO_SELF_UPGRADE" != 1 ]; then - echo "Checking for new version..." TEMP_DIR=$(TempDir) + trap 'rm -rf "$TEMP_DIR"' EXIT # --------------------------------------------------------------------------- cat << "UNLIKELY_EOF" > "$TEMP_DIR/fetch.py" """Do downloading and JSON parsing without additional dependencies. :: @@ -997,7 +1033,7 @@ def latest_stable_version(get): """Return the latest stable release of letsencrypt.""" metadata = loads(get( environ.get('LE_AUTO_JSON_URL', - 'https://pypi.python.org/pypi/letsencrypt/json'))) + 'https://pypi.python.org/pypi/certbot/json'))) # metadata['info']['version'] actually returns the latest of any kind of # release release, contrary to https://wiki.python.org/moin/PyPIJSON. # The regex is a sufficient regex for picking out prereleases for most @@ -1016,7 +1052,7 @@ def verified_new_le_auto(get, tag, temp_dir): """ le_auto_dir = environ.get( 'LE_AUTO_DIR_TEMPLATE', - 'https://raw.githubusercontent.com/letsencrypt/letsencrypt/%s/' + 'https://raw.githubusercontent.com/certbot/certbot/%s/' 'letsencrypt-auto-source/') % tag write(get(le_auto_dir + 'letsencrypt-auto'), temp_dir, 'letsencrypt-auto') write(get(le_auto_dir + 'letsencrypt-auto.sig'), temp_dir, 'letsencrypt-auto.sig') @@ -1079,8 +1115,6 @@ UNLIKELY_EOF # filesystems is non-atomic, doing `rm dest, cp src dest, rm src`, but the # cp is unlikely to fail (esp. under sudo) if the rm doesn't. $SUDO mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0" - # TODO: Clean up temp dir safely, even if it has quotes in its path. - rm -rf "$TEMP_DIR" fi # A newer version is available. fi # Self-upgrading is allowed. diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/a2dismod.sh b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/a2dismod.sh deleted file mode 100755 index ca96e216f..000000000 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/a2dismod.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -# An extremely simplified version of `a2enmod` for disabling modules in the -# httpd docker image. First argument is the server_root and the second is the -# module to be disabled. - -apache_confdir=$1 -module=$2 - -sed -i "/.*"$module".*/d" "$apache_confdir/test.conf" -enabled_conf="$apache_confdir/mods-enabled/"$module".conf" -if [ -e "$enabled_conf" ] -then - rm $enabled_conf -fi diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/a2enmod.sh b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/a2enmod.sh deleted file mode 100755 index 4da9288a2..000000000 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/a2enmod.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash -# An extremely simplified version of `a2enmod` for enabling modules in the -# httpd docker image. First argument is the Apache ServerRoot which should be -# an absolute path. The second is the module to be enabled, such as `ssl`. - -confdir=$1 -module=$2 - -echo "LoadModule ${module}_module " \ - "/usr/local/apache2/modules/mod_${module}.so" >> "${confdir}/test.conf" -availbase="/mods-available/${module}.conf" -availconf=$confdir$availbase -enabldir="$confdir/mods-enabled" -enablconf="$enabldir/${module}.conf" -if [ -e $availconf -a -d $enabldir -a ! -e $enablconf ] -then - ln -s "..$availbase" $enablconf -fi diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/apache24.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/apache24.py deleted file mode 100644 index 927c329ef..000000000 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/apache24.py +++ /dev/null @@ -1,63 +0,0 @@ -"""Proxies ApacheConfigurator for Apache 2.4 tests""" - -import zope.interface - -from certbot_compatibility_test import errors -from certbot_compatibility_test import interfaces -from certbot_compatibility_test.configurators.apache import common as apache_common - - -# The docker image doesn't actually have the watchdog module, but unless the -# config uses mod_heartbeat or mod_heartmonitor (which aren't installed and -# therefore the config won't be loaded), I believe this isn't a problem -# http://httpd.apache.org/docs/2.4/mod/mod_watchdog.html -STATIC_MODULES = set(["core", "so", "http", "mpm_event", "watchdog"]) - - -SHARED_MODULES = { - "log_config", "logio", "version", "unixd", "access_compat", "actions", - "alias", "allowmethods", "auth_basic", "auth_digest", "auth_form", - "authn_anon", "authn_core", "authn_dbd", "authn_dbm", "authn_file", - "authn_socache", "authnz_ldap", "authz_core", "authz_dbd", "authz_dbm", - "authz_groupfile", "authz_host", "authz_owner", "authz_user", "autoindex", - "buffer", "cache", "cache_disk", "cache_socache", "cgid", "dav", "dav_fs", - "dbd", "deflate", "dir", "dumpio", "env", "expires", "ext_filter", - "file_cache", "filter", "headers", "include", "info", "lbmethod_bybusyness", - "lbmethod_byrequests", "lbmethod_bytraffic", "lbmethod_heartbeat", "ldap", - "log_debug", "macro", "mime", "negotiation", "proxy", "proxy_ajp", - "proxy_balancer", "proxy_connect", "proxy_express", "proxy_fcgi", - "proxy_ftp", "proxy_http", "proxy_scgi", "proxy_wstunnel", "ratelimit", - "remoteip", "reqtimeout", "request", "rewrite", "sed", "session", - "session_cookie", "session_crypto", "session_dbd", "setenvif", - "slotmem_shm", "socache_dbm", "socache_memcache", "socache_shmcb", - "speling", "ssl", "status", "substitute", "unique_id", "userdir", - "vhost_alias"} - - -@zope.interface.implementer(interfaces.IConfiguratorProxy) -class Proxy(apache_common.Proxy): - """Wraps the ApacheConfigurator for Apache 2.4 tests""" - - def __init__(self, args): - """Initializes the plugin with the given command line args""" - super(Proxy, self).__init__(args) - # Running init isn't ideal, but the Docker container needs to survive - # Apache restarts - self.start_docker("bradmw/apache2.4", "init") - - def preprocess_config(self, server_root): - """Prepares the configuration for use in the Docker""" - super(Proxy, self).preprocess_config(server_root) - if self.version[1] != 4: - raise errors.Error("Apache version not 2.4") - - with open(self.test_conf, "a") as f: - for module in self.modules: - if module not in STATIC_MODULES: - if module in SHARED_MODULES: - f.write( - "LoadModule {0}_module /usr/local/apache2/modules/" - "mod_{0}.so\n".format(module)) - else: - raise errors.Error( - "Unsupported module {0}".format(module)) diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py index f57e0512d..ed3d9d67a 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py @@ -1,6 +1,7 @@ """Provides a common base for Apache proxies""" import re import os +import shutil import subprocess import mock @@ -9,6 +10,7 @@ import zope.interface from certbot import configuration from certbot import errors as le_errors from certbot_apache import configurator +from certbot_apache import constants from certbot_compatibility_test import errors from certbot_compatibility_test import interfaces from certbot_compatibility_test import util @@ -29,58 +31,14 @@ class Proxy(configurators_common.Proxy): super(Proxy, self).__init__(args) self.le_config.apache_le_vhost_ext = "-le-ssl.conf" - self._setup_mock() - self.modules = self.server_root = self.test_conf = self.version = None self._apache_configurator = self._all_names = self._test_names = None - - def _setup_mock(self): - """Replaces specific modules with mock.MagicMock""" - mock_subprocess = mock.MagicMock() - mock_subprocess.check_call = self.check_call - mock_subprocess.Popen = self.popen - - mock.patch( - "certbot_apache.configurator.subprocess", - mock_subprocess).start() - mock.patch( - "certbot_apache.parser.subprocess", - mock_subprocess).start() - mock.patch( - "certbot.le_util.subprocess", - mock_subprocess).start() - mock.patch( - "certbot_apache.configurator.le_util.exe_exists", - _is_apache_command).start() - patch = mock.patch( "certbot_apache.configurator.display_ops.select_vhost") mock_display = patch.start() mock_display.side_effect = le_errors.PluginError( "Unable to determine vhost") - def check_call(self, command, *args, **kwargs): - """If command is an Apache command, command is executed in the - running docker image. Otherwise, subprocess.check_call is used. - - """ - if _is_apache_command(command): - command = _modify_command(command) - return super(Proxy, self).check_call(command, *args, **kwargs) - else: - return subprocess.check_call(command, *args, **kwargs) - - def popen(self, command, *args, **kwargs): - """If command is an Apache command, command is executed in the - running docker image. Otherwise, subprocess.Popen is used. - - """ - if _is_apache_command(command): - command = _modify_command(command) - return super(Proxy, self).popen(command, *args, **kwargs) - else: - return subprocess.Popen(command, *args, **kwargs) - def __getattr__(self, name): """Wraps the Apache Configurator methods""" method = getattr(self._apache_configurator, name, None) @@ -91,29 +49,20 @@ class Proxy(configurators_common.Proxy): def load_config(self): """Loads the next configuration for the plugin to test""" - if hasattr(self.le_config, "apache_init_script"): - try: - self.check_call([self.le_config.apache_init_script, "stop"]) - except errors.Error: - raise errors.Error( - "Failed to stop previous apache config from running") config = super(Proxy, self).load_config() - self.modules = _get_modules(config) - self.version = _get_version(config) self._all_names, self._test_names = _get_names(config) server_root = _get_server_root(config) - with open(os.path.join(config, "config_file")) as f: - config_file = os.path.join(server_root, f.readline().rstrip()) - self.test_conf = _create_test_conf(server_root, config_file) + # with open(os.path.join(config, "config_file")) as f: + # config_file = os.path.join(server_root, f.readline().rstrip()) + shutil.rmtree("/etc/apache2") + shutil.copytree(server_root, "/etc/apache2", symlinks=True) - self.preprocess_config(server_root) - self._prepare_configurator(server_root, config_file) + self._prepare_configurator() try: - self.check_call("apachectl -d {0} -f {1} -k start".format( - server_root, config_file)) + subprocess.check_call("apachectl -k start".split()) except errors.Error: raise errors.Error( "Apache failed to load {0} before tests started".format( @@ -121,34 +70,13 @@ class Proxy(configurators_common.Proxy): return config - def preprocess_config(self, server_root): - # pylint: disable=anomalous-backslash-in-string, no-self-use - """Prepares the configuration for use in the Docker""" - - find = subprocess.Popen( - ["find", server_root, "-type", "f"], - stdout=subprocess.PIPE) - subprocess.check_call([ - "xargs", "sed", "-e", "s/DocumentRoot.*/DocumentRoot " - "\/usr\/local\/apache2\/htdocs/I", - "-e", "s/SSLPassPhraseDialog.*/SSLPassPhraseDialog builtin/I", - "-e", "s/TypesConfig.*/TypesConfig " - "\/usr\/local\/apache2\/conf\/mime.types/I", - "-e", "s/LoadModule/#LoadModule/I", - "-e", "s/SSLCertificateFile.*/SSLCertificateFile " - "\/usr\/local\/apache2\/conf\/empty_cert.pem/I", - "-e", "s/SSLCertificateKeyFile.*/SSLCertificateKeyFile " - "\/usr\/local\/apache2\/conf\/rsa1024_key2.pem/I", - "-i"], stdin=find.stdout) - - def _prepare_configurator(self, server_root, config_file): + def _prepare_configurator(self): """Prepares the Apache plugin for testing""" - self.le_config.apache_server_root = server_root - self.le_config.apache_ctl = "apachectl -d {0} -f {1}".format( - server_root, config_file) - self.le_config.apache_enmod = "a2enmod.sh {0}".format(server_root) - self.le_config.apache_dismod = "a2dismod.sh {0}".format(server_root) - self.le_config.apache_init_script = self.le_config.apache_ctl + " -k" + for k in constants.CLI_DEFAULTS_DEBIAN.keys(): + setattr(self.le_config, "apache_" + k, constants.os_constant(k)) + + # An alias + self.le_config.apache_handle_modules = self.le_config.apache_handle_mods self._apache_configurator = configurator.ApacheConfigurator( config=configuration.NamespaceConfig(self.le_config), @@ -183,39 +111,6 @@ class Proxy(configurators_common.Proxy): domain, cert_path, key_path, chain_path, fullchain_path) -def _is_apache_command(command): - """Returns true if command is an Apache command""" - if isinstance(command, list): - command = command[0] - - for apache_command in APACHE_COMMANDS: - if command.startswith(apache_command): - return True - - return False - - -def _modify_command(command): - """Modifies command so configtest works inside the docker image""" - if isinstance(command, list): - for i in xrange(len(command)): - if command[i] == "configtest": - command[i] = "-t" - else: - command = command.replace("configtest", "-t") - - return command - - -def _create_test_conf(server_root, apache_config): - """Creates a test config file and adds it to the Apache config""" - test_conf = os.path.join(server_root, "test.conf") - open(test_conf, "w").close() - subprocess.check_call( - ["sed", "-i", "1iInclude test.conf", apache_config]) - return test_conf - - def _get_server_root(config): """Returns the server root directory in config""" subdirs = [ @@ -223,7 +118,7 @@ def _get_server_root(config): if os.path.isdir(os.path.join(config, name))] if len(subdirs) != 1: - errors.Error("Malformed configuration directiory {0}".format(config)) + errors.Error("Malformed configuration directory {0}".format(config)) return os.path.join(config, subdirs[0].rstrip()) @@ -251,34 +146,3 @@ def _get_names(config): words[1].find(".") != -1): all_names.add(words[1]) return all_names, non_ip_names - - -def _get_modules(config): - """Returns the list of modules found in module_list""" - modules = [] - with open(os.path.join(config, "modules")) as f: - for line in f: - # Modules list is indented, everything else is headers/footers - if line[0].isspace(): - words = line.split() - # Modules redundantly end in "_module" which we can discard - modules.append(words[0][:-7]) - - return modules - - -def _get_version(config): - """Return version of Apache Server. - - Version is returned as tuple. (ie. 2.4.7 = (2, 4, 7)). Code taken from - the Apache plugin. - - """ - with open(os.path.join(config, "version")) as f: - # Should be on first line of input - matches = APACHE_VERSION_REGEX.findall(f.readline()) - - if len(matches) != 1: - raise errors.Error("Unable to find Apache version") - - return tuple([int(i) for i in matches[0].split(".")]) diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/common.py index 4657883a3..03128cc86 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/common.py @@ -4,10 +4,7 @@ import os import shutil import tempfile -import docker - from certbot import constants -from certbot_compatibility_test import errors from certbot_compatibility_test import util @@ -18,20 +15,9 @@ class Proxy(object): # pylint: disable=too-many-instance-attributes """A common base for compatibility test configurators""" - _NOT_ADDED_ARGS = True - @classmethod def add_parser_arguments(cls, parser): """Adds command line arguments needed by the plugin""" - if Proxy._NOT_ADDED_ARGS: - group = parser.add_argument_group("docker") - group.add_argument( - "--docker-url", default="unix://var/run/docker.sock", - help="URL of the docker server") - group.add_argument( - "--no-remove", action="store_true", - help="do not delete container on program exit") - Proxy._NOT_ADDED_ARGS = False def __init__(self, args): """Initializes the plugin with the given command line args""" @@ -43,10 +29,8 @@ class Proxy(object): for config in os.listdir(config_dir)] self.args = args - self._docker_client = docker.Client( - base_url=self.args.docker_url, version="auto") - self.http_port, self.https_port = util.get_two_free_ports() - self._container_id = None + self.http_port = 80 + self.https_port = 443 def has_more_configs(self): """Returns true if there are more configs to test""" @@ -54,9 +38,6 @@ class Proxy(object): def cleanup_from_tests(self): """Performs any necessary cleanup from running plugin tests""" - self._docker_client.stop(self._container_id, 0) - if not self.args.no_remove: - self._docker_client.remove_container(self._container_id) def load_config(self): """Returns the next config directory to be tested""" @@ -65,67 +46,6 @@ class Proxy(object): os.makedirs(backup) return self._configs.pop() - def start_docker(self, image_name, command): - """Creates and runs a Docker container with the specified image""" - logger.warning("Pulling Docker image. This may take a minute.") - for line in self._docker_client.pull(image_name, stream=True): - logger.debug(line) - - host_config = docker.utils.create_host_config( - binds={self._temp_dir: {"bind": self._temp_dir, "mode": "rw"}}, - port_bindings={ - 80: ("127.0.0.1", self.http_port), - 443: ("127.0.0.1", self.https_port)},) - container = self._docker_client.create_container( - image_name, command, ports=[80, 443], volumes=self._temp_dir, - host_config=host_config) - if container["Warnings"]: - logger.warning(container["Warnings"]) - self._container_id = container["Id"] - self._docker_client.start(self._container_id) - - def check_call(self, command, *args, **kwargs): - # pylint: disable=unused-argument - """Simulates a call to check_call but executes the command in the - running docker image - - """ - if self.popen(command).returncode: - raise errors.Error( - "{0} exited with a nonzero value".format(command)) - - def popen(self, command, *args, **kwargs): - # pylint: disable=unused-argument - """Simulates a call to Popen but executes the command in the - running docker image - - """ - class SimplePopen(object): - # pylint: disable=too-few-public-methods - """Simplified Popen object""" - def __init__(self, returncode, output): - self.returncode = returncode - self._stdout = output - self._stderr = output - - def communicate(self): - """Returns stdout and stderr""" - return self._stdout, self._stderr - - if isinstance(command, list): - command = " ".join(command) - - returncode, output = self.execute_in_docker(command) - return SimplePopen(returncode, output) - - def execute_in_docker(self, command): - """Executes command inside the running docker image""" - logger.debug("Executing '%s'", command) - exec_id = self._docker_client.exec_create(self._container_id, command) - output = self._docker_client.exec_start(exec_id) - returncode = self._docker_client.exec_inspect(exec_id)["ExitCode"] - return returncode, output - def copy_certs_and_keys(self, cert_path, key_path, chain_path=None): """Copies certs and keys into the temporary directory""" cert_and_key_dir = os.path.join(self._temp_dir, "certs_and_keys") diff --git a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py index 6823dfdab..38abffb18 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py +++ b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py @@ -7,6 +7,7 @@ import os import shutil import tempfile import time +import sys import OpenSSL @@ -21,17 +22,17 @@ from certbot_compatibility_test import errors from certbot_compatibility_test import util from certbot_compatibility_test import validator -from certbot_compatibility_test.configurators.apache import apache24 +from certbot_compatibility_test.configurators.apache import common DESCRIPTION = """ -Tests Certbot plugins against different server configuratons. It is -assumed that Docker is already installed. If no test types is specified, all +Tests Certbot plugins against different server configurations. It is +assumed that Docker is already installed. If no test type is specified, all tests that the plugin supports are performed. """ -PLUGINS = {"apache": apache24.Proxy} +PLUGINS = {"apache": common.Proxy} logger = logging.getLogger(__name__) @@ -61,8 +62,8 @@ def test_authenticator(plugin, config, temp_dir): "Plugin failed to complete %s for %s in %s", type(achalls[i]), achalls[i].domain, config) success = False - elif isinstance(responses[i], challenges.TLSSNI01): - verify = functools.partial(responses[i].simple_verify, achalls[i], + elif isinstance(responses[i], challenges.TLSSNI01Response): + verify = functools.partial(responses[i].simple_verify, achalls[i].chall, achalls[i].domain, util.JWK.public_key(), host="127.0.0.1", @@ -142,7 +143,8 @@ def test_deploy_cert(plugin, temp_dir, domains): for domain in domains: try: - plugin.deploy_cert(domain, cert_path, util.KEY_PATH) + plugin.deploy_cert(domain, cert_path, util.KEY_PATH, cert_path) + plugin.save() # Needed by the Apache plugin except le_errors.Error as error: logger.error("Plugin failed to deploy ceritificate for %s:", domain) logger.exception(error) @@ -177,6 +179,7 @@ def test_enhancements(plugin, domains): for domain in domains: try: plugin.enhance(domain, "redirect") + plugin.save() # Needed by the Apache plugin except le_errors.PluginError as error: # Don't immediately fail because a redirect may already be enabled logger.warning("Plugin failed to enable redirect for %s:", domain) @@ -341,7 +344,7 @@ def main(): temp_dir = tempfile.mkdtemp() plugin = PLUGINS[args.plugin](args) try: - plugin.execute_in_docker("mkdir -p /var/log/apache2") + overall_success = True while plugin.has_more_configs(): success = True @@ -360,10 +363,18 @@ def main(): if success: logger.info("All tests on %s succeeded", config) else: + overall_success = False logger.error("Tests on %s failed", config) finally: plugin.cleanup_from_tests() + if overall_success: + logger.warn("All compatibility tests succeeded") + sys.exit(0) + else: + logger.warn("One or more compatibility tests failed") + sys.exit(1) + if __name__ == "__main__": main() diff --git a/certbot-compatibility-test/certbot_compatibility_test/testdata/configs.tar.gz b/certbot-compatibility-test/certbot_compatibility_test/testdata/configs.tar.gz index 7323acf74..9b819d0c7 100644 Binary files a/certbot-compatibility-test/certbot_compatibility_test/testdata/configs.tar.gz and b/certbot-compatibility-test/certbot_compatibility_test/testdata/configs.tar.gz differ diff --git a/certbot-compatibility-test/certbot_compatibility_test/util.py b/certbot-compatibility-test/certbot_compatibility_test/util.py index cbce4fb56..af951aa6a 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/util.py +++ b/certbot-compatibility-test/certbot_compatibility_test/util.py @@ -1,11 +1,9 @@ """Utility functions for Certbot plugin tests.""" import argparse import copy -import contextlib import os import re import shutil -import socket import tarfile from acme import jose @@ -52,13 +50,3 @@ def extract_configs(configs, parent_dir): raise errors.Error("Unknown configurations file type") return config_dir - - -def get_two_free_ports(): - """Returns two free ports to use for the tests""" - with contextlib.closing(socket.socket()) as sock1: - with contextlib.closing(socket.socket()) as sock2: - sock1.bind(("", 0)) - sock2.bind(("", 0)) - - return sock1.getsockname()[1], sock2.getsockname()[1] diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index 8f9452c5a..fb56be65f 100644 --- a/certbot-compatibility-test/setup.py +++ b/certbot-compatibility-test/setup.py @@ -4,12 +4,11 @@ from setuptools import setup from setuptools import find_packages -version = '0.7.0.dev0' +version = '0.9.0.dev0' install_requires = [ - 'certbot=={0}'.format(version), - 'certbot-apache=={0}'.format(version), - 'docker-py', + 'certbot', + 'certbot-apache', 'requests', 'zope.interface', ] diff --git a/certbot-nginx/certbot_nginx/configurator.py b/certbot-nginx/certbot_nginx/configurator.py index e402d5c79..30928e56c 100644 --- a/certbot-nginx/certbot_nginx/configurator.py +++ b/certbot-nginx/certbot_nginx/configurator.py @@ -17,7 +17,7 @@ from certbot import constants as core_constants from certbot import crypto_util from certbot import errors from certbot import interfaces -from certbot import le_util +from certbot import util from certbot import reverter from certbot.plugins import common @@ -111,7 +111,7 @@ class NginxConfigurator(common.Plugin): :raises .errors.MisconfigurationError: If Nginx is misconfigured """ # Verify Nginx is installed - if not le_util.exe_exists(self.conf('ctl')): + if not util.exe_exists(self.conf('ctl')): raise errors.NoInstallationError # Make sure configuration is valid @@ -318,7 +318,7 @@ class NginxConfigurator(common.Plugin): cert = acme_crypto_util.gen_ss_cert(key, domains=[socket.gethostname()]) cert_pem = OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, cert) - cert_file, cert_path = le_util.unique_file(os.path.join(tmp_dir, "cert.pem")) + cert_file, cert_path = util.unique_file(os.path.join(tmp_dir, "cert.pem")) with cert_file: cert_file.write(cert_pem) return cert_path, le_key.file @@ -426,7 +426,7 @@ class NginxConfigurator(common.Plugin): """ try: - le_util.run_script([self.conf('ctl'), "-c", self.nginx_conf, "-t"]) + util.run_script([self.conf('ctl'), "-c", self.nginx_conf, "-t"]) except errors.SubprocessError as err: raise errors.MisconfigurationError(str(err)) @@ -439,11 +439,11 @@ class NginxConfigurator(common.Plugin): """ uid = os.geteuid() - le_util.make_or_verify_dir( + util.make_or_verify_dir( self.config.work_dir, core_constants.CONFIG_DIRS_MODE, uid) - le_util.make_or_verify_dir( + util.make_or_verify_dir( self.config.backup_dir, core_constants.CONFIG_DIRS_MODE, uid) - le_util.make_or_verify_dir( + util.make_or_verify_dir( self.config.config_dir, core_constants.CONFIG_DIRS_MODE, uid) def get_version(self): diff --git a/certbot-nginx/certbot_nginx/tests/configurator_test.py b/certbot-nginx/certbot_nginx/tests/configurator_test.py index b36802939..30f287249 100644 --- a/certbot-nginx/certbot_nginx/tests/configurator_test.py +++ b/certbot-nginx/certbot_nginx/tests/configurator_test.py @@ -30,7 +30,7 @@ class NginxConfiguratorTest(util.NginxTest): shutil.rmtree(self.config_dir) shutil.rmtree(self.work_dir) - @mock.patch("certbot_nginx.configurator.le_util.exe_exists") + @mock.patch("certbot_nginx.configurator.util.exe_exists") def test_prepare_no_install(self, mock_exe_exists): mock_exe_exists.return_value = False self.assertRaises( @@ -40,7 +40,7 @@ class NginxConfiguratorTest(util.NginxTest): self.assertEquals((1, 6, 2), self.config.version) self.assertEquals(5, len(self.config.parser.parsed)) - @mock.patch("certbot_nginx.configurator.le_util.exe_exists") + @mock.patch("certbot_nginx.configurator.util.exe_exists") @mock.patch("certbot_nginx.configurator.subprocess.Popen") def test_prepare_initializes_version(self, mock_popen, mock_exe_exists): mock_popen().communicate.return_value = ( @@ -362,11 +362,11 @@ class NginxConfiguratorTest(util.NginxTest): mock_popen.side_effect = OSError("Can't find program") self.assertRaises(errors.MisconfigurationError, self.config.restart) - @mock.patch("certbot.le_util.run_script") + @mock.patch("certbot.util.run_script") def test_config_test(self, _): self.config.config_test() - @mock.patch("certbot.le_util.run_script") + @mock.patch("certbot.util.run_script") def test_config_test_bad_process(self, mock_run_script): mock_run_script.side_effect = errors.SubprocessError self.assertRaises(errors.MisconfigurationError, self.config.config_test) diff --git a/certbot-nginx/certbot_nginx/tests/util.py b/certbot-nginx/certbot_nginx/tests/util.py index 3c4731700..ddacd041b 100644 --- a/certbot-nginx/certbot_nginx/tests/util.py +++ b/certbot-nginx/certbot_nginx/tests/util.py @@ -51,7 +51,7 @@ def get_nginx_configurator( with mock.patch("certbot_nginx.configurator.NginxConfigurator." "config_test"): - with mock.patch("certbot_nginx.configurator.le_util." + with mock.patch("certbot_nginx.configurator.util." "exe_exists") as mock_exe_exists: mock_exe_exists.return_value = True config = configurator.NginxConfigurator( diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index a74b93093..62c705b4c 100644 --- a/certbot-nginx/setup.py +++ b/certbot-nginx/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.7.0.dev0' +version = '0.9.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot/__init__.py b/certbot/__init__.py index 85f370e7a..34358a5d9 100644 --- a/certbot/__init__.py +++ b/certbot/__init__.py @@ -1,4 +1,4 @@ """Certbot client.""" # version number like 1.2.3a0, must have at least 2 parts, like 1.2 -__version__ = '0.7.0.dev0' +__version__ = '0.9.0.dev0' diff --git a/certbot/account.py b/certbot/account.py index cc50a6ea6..798f8664e 100644 --- a/certbot/account.py +++ b/certbot/account.py @@ -16,7 +16,7 @@ from acme import messages from certbot import errors from certbot import interfaces -from certbot import le_util +from certbot import util logger = logging.getLogger(__name__) @@ -130,7 +130,7 @@ class AccountFileStorage(interfaces.AccountStorage): """ def __init__(self, config): self.config = config - le_util.make_or_verify_dir(config.accounts_dir, 0o700, os.geteuid(), + util.make_or_verify_dir(config.accounts_dir, 0o700, os.geteuid(), self.config.strict_permissions) def _account_dir_path(self, account_id): @@ -186,16 +186,29 @@ class AccountFileStorage(interfaces.AccountStorage): return acc def save(self, account): + self._save(account, regr_only=False) + + def save_regr(self, account): + """Save the registration resource. + + :param Account account: account whose regr should be saved + + """ + self._save(account, regr_only=True) + + def _save(self, account, regr_only): account_dir_path = self._account_dir_path(account.id) - le_util.make_or_verify_dir(account_dir_path, 0o700, os.geteuid(), - self.config.strict_permissions) + util.make_or_verify_dir(account_dir_path, 0o700, os.geteuid(), + self.config.strict_permissions) try: with open(self._regr_path(account_dir_path), "w") as regr_file: regr_file.write(account.regr.json_dumps()) - with le_util.safe_open(self._key_path(account_dir_path), - "w", chmod=0o400) as key_file: - key_file.write(account.key.json_dumps()) - with open(self._metadata_path(account_dir_path), "w") as metadata_file: - metadata_file.write(account.meta.json_dumps()) + if not regr_only: + with util.safe_open(self._key_path(account_dir_path), + "w", chmod=0o400) as key_file: + key_file.write(account.key.json_dumps()) + with open(self._metadata_path( + account_dir_path), "w") as metadata_file: + metadata_file.write(account.meta.json_dumps()) except IOError as error: raise errors.AccountStorageError(error) diff --git a/certbot/cli.py b/certbot/cli.py index 3bd565496..ba1f23708 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -1,6 +1,7 @@ """Certbot command line argument & config processing.""" from __future__ import print_function import argparse +import copy import glob import logging import logging.handlers @@ -17,7 +18,7 @@ from certbot import crypto_util from certbot import errors from certbot import hooks from certbot import interfaces -from certbot import le_util +from certbot import util from certbot.plugins import disco as plugins_disco import certbot.plugins.selection as plugin_selection @@ -61,6 +62,7 @@ cert. Major SUBCOMMANDS are: install Install a previously obtained cert in a server renew Renew previously obtained certs that are near expiry revoke Revoke a previously obtained certificate + register Perform tasks related to registering with the CA rollback Rollback server configuration changes made during install config_changes Show changes made to server config during installation plugins Display information about installed plugins @@ -86,7 +88,8 @@ More detailed help: the available topics are: all, automation, paths, security, testing, or any of the subcommands or - plugins (certonly, install, nginx, apache, standalone, webroot, etc) + plugins (certonly, install, register, nginx, apache, standalone, webroot, + etc.) """ @@ -209,6 +212,35 @@ def set_by_cli(var): set_by_cli.detector = None +def has_default_value(option, value): + """Does option have the default value? + + If the default value of option is not known, False is returned. + + :param str option: configuration variable being considered + :param value: value of the configuration variable named option + + :returns: True if option has the default value, otherwise, False + :rtype: bool + + """ + return (option in helpful_parser.defaults and + helpful_parser.defaults[option] == value) + + +def option_was_set(option, value): + """Was option set by the user or does it differ from the default? + + :param str option: configuration variable being considered + :param value: value of the configuration variable named option + + :returns: True if the option was set, otherwise, False + :rtype: bool + + """ + return set_by_cli(option) or not has_default_value(option, value) + + def argparse_type(variable): "Return our argparse type function for a config variable (default: str)" # pylint: disable=protected-access @@ -285,8 +317,9 @@ class HelpfulArgumentParser(object): self.VERBS = {"auth": main.obtain_cert, "certonly": main.obtain_cert, "config_changes": main.config_changes, "run": main.run, "install": main.install, "plugins": main.plugins_cmd, - "renew": main.renew, "revoke": main.revoke, - "rollback": main.rollback, "everything": main.run} + "register": main.register, "renew": main.renew, + "revoke": main.revoke, "rollback": main.rollback, + "everything": main.run} # List of topics for which additional help can be provided HELP_TOPICS = ["all", "security", "paths", "automation", "testing"] + list(self.VERBS) @@ -317,6 +350,7 @@ class HelpfulArgumentParser(object): sys.exit(0) self.visible_topics = self.determine_help_topics(self.help_arg) self.groups = {} # elements are added by .add_group() + self.defaults = {} # elements are added by .parse_args() def parse_args(self): """Parses command line arguments and returns the result. @@ -332,9 +366,12 @@ class HelpfulArgumentParser(object): if self.detect_defaults: return parsed_args + self.defaults = dict((key, copy.deepcopy(self.parser.get_default(key))) + for key in vars(parsed_args)) + # Do any post-parsing homework here - if self.verb == "renew": + if self.verb == "renew" and not parsed_args.dialog_mode: parsed_args.noninteractive_mode = True if parsed_args.staging or parsed_args.dry_run: @@ -343,7 +380,21 @@ class HelpfulArgumentParser(object): if parsed_args.csr: self.handle_csr(parsed_args) - hooks.validate_hooks(parsed_args) + if parsed_args.must_staple: + parsed_args.staple = True + + # Avoid conflicting args + conficting_args = ["quiet", "noninteractive_mode", "text_mode"] + if parsed_args.dialog_mode: + for arg in conficting_args: + if getattr(parsed_args, arg): + raise errors.Error( + ("Conflicting values for displayer." + " {0} conflicts with dialog_mode").format(arg) + ) + + if parsed_args.validate_hooks: + hooks.validate_hooks(parsed_args) return parsed_args @@ -502,7 +553,7 @@ class HelpfulArgumentParser(object): :param int nargs: Number of arguments the option takes. """ - le_util.add_deprecated_argument( + util.add_deprecated_argument( self.parser.add_argument, argument_name, num_args) def add_group(self, topic, **kwargs): @@ -557,7 +608,7 @@ class HelpfulArgumentParser(object): return dict([(t, t == chosen_topic) for t in self.help_topics]) -def prepare_and_parse_args(plugins, args, detect_defaults=False): +def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: disable=too-many-statements """Returns parsed command line arguments. :param .PluginsRegistry plugins: available plugins @@ -567,6 +618,9 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): :rtype: argparse.Namespace """ + + # pylint: disable=too-many-statements + helpful = HelpfulArgumentParser(args, plugins, detect_defaults) # --help is automatically provided by argparse @@ -584,6 +638,9 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): help="Run without ever asking for user input. This may require " "additional command line flags; the client will try to explain " "which ones are required if it finds one missing") + helpful.add( + None, "--dialog", dest="dialog_mode", action="store_true", + help="Run using dialog") helpful.add( None, "--dry-run", action="store_true", dest="dry_run", help="Perform a test run of the client, obtaining test (invalid) certs" @@ -606,6 +663,11 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): "certificates. Updates to the Subscriber Agreement will still " "affect you, and will be effective 14 days after posting an " "update to the web site.") + helpful.add( + "register", "--update-registration", action="store_true", + help="With the register verb, indicates that details associated " + "with an existing registration, such as the e-mail address, " + "should be updated, rather than registering a new account.") helpful.add(None, "-m", "--email", help=config_help("email")) # positional arg shadows --domains, instead of appending, and # --domains is useful, because it can be stored in config @@ -773,6 +835,14 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): "For this command, the shell variable $RENEWED_LINEAGE will point to the" "config live subdirectory containing the new certs and keys; the shell variable " "$RENEWED_DOMAINS will contain a space-delimited list of renewed cert domains") + helpful.add( + "renew", "--disable-hook-validation", + action='store_false', dest='validate_hooks', default=True, + help="Ordinarily the commands specified for --pre-hook/--post-hook/--renew-hook" + " will be checked for validity, to see if the programs being run are in the $PATH," + " so that mistakes can be caught early, even when the hooks aren't being run just yet." + " The validation is rather simplistic and fails if you use more advanced" + " shell constructs, so you can use this switch to disable it.") helpful.add_deprecated_argument("--agree-dev-preview", 0) @@ -935,7 +1005,7 @@ def add_domains(args_or_config, domains): """ validated_domains = [] for domain in domains.split(","): - domain = le_util.enforce_domain_sanity(domain.strip()) + domain = util.enforce_domain_sanity(domain.strip()) validated_domains.append(domain) if domain not in args_or_config.domains: args_or_config.domains.append(domain) diff --git a/certbot/client.py b/certbot/client.py index ba31f8760..1dec6a1a9 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -21,7 +21,7 @@ from certbot import crypto_util from certbot import errors from certbot import error_handler from certbot import interfaces -from certbot import le_util +from certbot import util from certbot import reverter from certbot import storage from certbot import cli @@ -53,7 +53,7 @@ def _determine_user_agent(config): if config.user_agent is None: ua = "CertbotACMEClient/{0} ({1}) Authenticator/{2} Installer/{3}" - ua = ua.format(certbot.__version__, " ".join(le_util.get_os_info()), + ua = ua.format(certbot.__version__, util.get_os_info_ua(), config.authenticator, config.installer) else: ua = config.user_agent @@ -150,7 +150,7 @@ def perform_registration(acme, config): return acme.register(messages.NewRegistration.from_data(email=config.email)) except messages.Error as e: if e.typ == "urn:acme:error:invalidEmail": - config.namespace.email = display_ops.get_email(more=True, invalid=True) + config.namespace.email = display_ops.get_email(invalid=True) return perform_registration(acme, config) else: raise @@ -198,7 +198,7 @@ class Client(object): consistent with identifiers present in the `csr`. :param list domains: Domain names. - :param .le_util.CSR csr: DER-encoded Certificate Signing + :param .util.CSR csr: DER-encoded Certificate Signing Request. The key used to generate this CSR can be different than `authkey`. :param list authzr: List of @@ -237,8 +237,8 @@ class Client(object): :returns: `.CertificateResource`, certificate chain (as returned by `.fetch_chain`), and newly generated private key - (`.le_util.Key`) and DER-encoded Certificate Signing Request - (`.le_util.CSR`). + (`.util.Key`) and DER-encoded Certificate Signing Request + (`.util.CSR`). :rtype: tuple """ @@ -312,7 +312,7 @@ class Client(object): """ for path in cert_path, chain_path, fullchain_path: - le_util.make_or_verify_dir( + util.make_or_verify_dir( os.path.dirname(path), 0o755, os.geteuid(), self.config.strict_permissions) @@ -504,9 +504,9 @@ def validate_key_csr(privkey, csr=None): If csr is left as None, only the key will be validated. :param privkey: Key associated with CSR - :type privkey: :class:`certbot.le_util.Key` + :type privkey: :class:`certbot.util.Key` - :param .le_util.CSR csr: CSR + :param .util.CSR csr: CSR :raises .errors.Error: when validation fails @@ -523,7 +523,7 @@ def validate_key_csr(privkey, csr=None): if csr.form == "der": csr_obj = OpenSSL.crypto.load_certificate_request( OpenSSL.crypto.FILETYPE_ASN1, csr.data) - csr = le_util.CSR(csr.file, OpenSSL.crypto.dump_certificate( + csr = util.CSR(csr.file, OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, csr_obj), "pem") # If CSR is provided, it must be readable and valid. @@ -586,10 +586,10 @@ def _open_pem_file(cli_arg_path, pem_path): """ if cli.set_by_cli(cli_arg_path): - return le_util.safe_open(pem_path, chmod=0o644),\ + return util.safe_open(pem_path, chmod=0o644),\ os.path.abspath(pem_path) else: - uniq = le_util.unique_file(pem_path, 0o644) + uniq = util.unique_file(pem_path, 0o644) return uniq[0], os.path.abspath(uniq[1]) def _save_chain(chain_pem, chain_file): diff --git a/certbot/colored_logging.py b/certbot/colored_logging.py index d42fb5966..93bf3a55a 100644 --- a/certbot/colored_logging.py +++ b/certbot/colored_logging.py @@ -2,7 +2,7 @@ import logging import sys -from certbot import le_util +from certbot import util class StreamHandler(logging.StreamHandler): @@ -40,6 +40,6 @@ class StreamHandler(logging.StreamHandler): if sys.version_info < (2, 7) else super(StreamHandler, self).format(record)) if self.colored and record.levelno >= self.red_level: - return ''.join((le_util.ANSI_SGR_RED, out, le_util.ANSI_SGR_RESET)) + return ''.join((util.ANSI_SGR_RED, out, util.ANSI_SGR_RESET)) else: return out diff --git a/certbot/configuration.py b/certbot/configuration.py index 172b35bfe..712135b8d 100644 --- a/certbot/configuration.py +++ b/certbot/configuration.py @@ -8,7 +8,7 @@ import zope.interface from certbot import constants from certbot import errors from certbot import interfaces -from certbot import le_util +from certbot import util @zope.interface.implementer(interfaces.IConfig) @@ -132,4 +132,4 @@ def check_config_sanity(config): if config.namespace.domains is not None: for domain in config.namespace.domains: # This may be redundant, but let's be paranoid - le_util.enforce_domain_sanity(domain) + util.enforce_domain_sanity(domain) diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py index 68e07e059..1e831dd8f 100644 --- a/certbot/crypto_util.py +++ b/certbot/crypto_util.py @@ -17,7 +17,7 @@ from acme import jose from certbot import errors from certbot import interfaces -from certbot import le_util +from certbot import util logger = logging.getLogger(__name__) @@ -37,7 +37,7 @@ def init_save_key(key_size, key_dir, keyname="key-certbot.pem"): :param str keyname: Filename of key :returns: Key - :rtype: :class:`certbot.le_util.Key` + :rtype: :class:`certbot.util.Key` :raises ValueError: If unable to generate the key given key_size. @@ -50,30 +50,29 @@ def init_save_key(key_size, key_dir, keyname="key-certbot.pem"): config = zope.component.getUtility(interfaces.IConfig) # Save file - le_util.make_or_verify_dir(key_dir, 0o700, os.geteuid(), - config.strict_permissions) - key_f, key_path = le_util.unique_file( - os.path.join(key_dir, keyname), 0o600) + util.make_or_verify_dir(key_dir, 0o700, os.geteuid(), + config.strict_permissions) + key_f, key_path = util.unique_file(os.path.join(key_dir, keyname), 0o600) with key_f: key_f.write(key_pem) logger.info("Generating key (%d bits): %s", key_size, key_path) - return le_util.Key(key_path, key_pem) + return util.Key(key_path, key_pem) def init_save_csr(privkey, names, path, csrname="csr-certbot.pem"): """Initialize a CSR with the given private key. :param privkey: Key to include in the CSR - :type privkey: :class:`certbot.le_util.Key` + :type privkey: :class:`certbot.util.Key` :param set names: `str` names to include in the CSR :param str path: Certificate save directory. :returns: CSR - :rtype: :class:`certbot.le_util.CSR` + :rtype: :class:`certbot.util.CSR` """ config = zope.component.getUtility(interfaces.IConfig) @@ -82,16 +81,16 @@ def init_save_csr(privkey, names, path, csrname="csr-certbot.pem"): must_staple=config.must_staple) # Save CSR - le_util.make_or_verify_dir(path, 0o755, os.geteuid(), + util.make_or_verify_dir(path, 0o755, os.geteuid(), config.strict_permissions) - csr_f, csr_filename = le_util.unique_file( + csr_f, csr_filename = util.unique_file( os.path.join(path, csrname), 0o644) csr_f.write(csr_pem) csr_f.close() logger.info("Creating CSR: %s", csr_filename) - return le_util.CSR(csr_filename, csr_der, "der") + return util.CSR(csr_filename, csr_der, "der") # Lower level functions @@ -187,7 +186,7 @@ def import_csr_file(csrfile, data): :param str data: contents of the CSR file :returns: (`OpenSSL.crypto.FILETYPE_PEM` or `OpenSSL.crypto.FILETYPE_ASN1`, - le_util.CSR object representing the CSR, + util.CSR object representing the CSR, list of domains requested in the CSR) :rtype: tuple @@ -200,7 +199,7 @@ def import_csr_file(csrfile, data): logger.debug("CSR parse error (form=%s, typ=%s):", form, typ) logger.debug(traceback.format_exc()) continue - return typ, le_util.CSR(file=csrfile, data=data, form=form), domains + return typ, util.CSR(file=csrfile, data=data, form=form), domains raise errors.Error("Failed to parse CSR file: {0}".format(csrfile)) @@ -297,6 +296,32 @@ def get_sans_from_csr(csr, typ=OpenSSL.crypto.FILETYPE_PEM): csr, OpenSSL.crypto.load_certificate_request, typ) +def _get_names_from_cert_or_req(cert_or_req, load_func, typ): + loaded_cert_or_req = _load_cert_or_req(cert_or_req, load_func, typ) + common_name = loaded_cert_or_req.get_subject().CN + # pylint: disable=protected-access + sans = acme_crypto_util._pyopenssl_cert_or_req_san(loaded_cert_or_req) + + if common_name is None: + return sans + else: + return [common_name] + [d for d in sans if d != common_name] + + +def get_names_from_cert(csr, typ=OpenSSL.crypto.FILETYPE_PEM): + """Get a list of domains from a cert, including the CN if it is set. + + :param str cert: Certificate (encoded). + :param typ: `OpenSSL.crypto.FILETYPE_PEM` or `OpenSSL.crypto.FILETYPE_ASN1` + + :returns: A list of domain names. + :rtype: list + + """ + return _get_names_from_cert_or_req( + csr, OpenSSL.crypto.load_certificate, typ) + + def get_names_from_csr(csr, typ=OpenSSL.crypto.FILETYPE_PEM): """Get a list of domains from a CSR, including the CN if it is set. @@ -307,13 +332,8 @@ def get_names_from_csr(csr, typ=OpenSSL.crypto.FILETYPE_PEM): :rtype: list """ - loaded_csr = _load_cert_or_req( + return _get_names_from_cert_or_req( csr, OpenSSL.crypto.load_certificate_request, typ) - # Use a set to avoid duplication with CN and Subject Alt Names - domains = set(d for d in (loaded_csr.get_subject().CN,) if d is not None) - # pylint: disable=protected-access - domains.update(acme_crypto_util._pyopenssl_cert_or_req_san(loaded_csr)) - return list(domains) def dump_pyopenssl_chain(chain, filetype=OpenSSL.crypto.FILETYPE_PEM): diff --git a/certbot/display/ops.py b/certbot/display/ops.py index 6752bf0c1..c7e566256 100644 --- a/certbot/display/ops.py +++ b/certbot/display/ops.py @@ -6,7 +6,7 @@ import zope.component from certbot import errors from certbot import interfaces -from certbot import le_util +from certbot import util from certbot.display import util as display_util logger = logging.getLogger(__name__) @@ -15,41 +15,56 @@ logger = logging.getLogger(__name__) z_util = zope.component.getUtility -def get_email(more=False, invalid=False): +def get_email(invalid=False, optional=True): """Prompt for valid email address. - :param bool more: explain why the email is strongly advisable, but how to - skip it - :param bool invalid: true if the user just typed something, but it wasn't - a valid-looking email + :param bool invalid: True if an invalid address was provided by the user + :param bool optional: True if the user can use + --register-unsafely-without-email to avoid providing an e-mail - :returns: Email or ``None`` if cancelled by user. + :returns: e-mail address :rtype: str - """ - msg = "Enter email address (used for urgent notices and lost key recovery)" - if invalid: - msg = "There seem to be problems with that address. " + msg - if more: - msg += ('\n\nIf you really want to skip this, you can run the client with ' - '--register-unsafely-without-email but make sure you backup your ' - 'account key from /etc/letsencrypt/accounts\n\n') - try: - code, email = zope.component.getUtility(interfaces.IDisplay).input(msg) - except errors.MissingCommandlineFlag: - msg = ("You should register before running non-interactively, or provide --agree-tos" - " and --email flags") - raise errors.MissingCommandlineFlag(msg) + :raises errors.Error: if the user cancels - if code == display_util.OK: - if le_util.safe_email(email): - return email + """ + invalid_prefix = "There seem to be problems with that address. " + msg = "Enter email address (used for urgent notices and lost key recovery)" + unsafe_suggestion = ("\n\nIf you really want to skip this, you can run " + "the client with --register-unsafely-without-email " + "but make sure you then backup your account key from " + "/etc/letsencrypt/accounts\n\n") + if optional: + if invalid: + msg += unsafe_suggestion else: - # TODO catch the server's ACME invalid email address error, and - # make a similar call when that happens - return get_email(more=True, invalid=(email != "")) + suggest_unsafe = True else: - return None + suggest_unsafe = False + + while True: + try: + code, email = z_util(interfaces.IDisplay).input( + invalid_prefix + msg if invalid else msg) + except errors.MissingCommandlineFlag: + msg = ("You should register before running non-interactively, " + "or provide --agree-tos and --email flags") + raise errors.MissingCommandlineFlag(msg) + + if code != display_util.OK: + if optional: + raise errors.Error( + "An e-mail address or " + "--register-unsafely-without-email must be provided.") + else: + raise errors.Error("An e-mail address must be provided.") + elif util.safe_email(email): + return email + elif suggest_unsafe: + msg += unsafe_suggestion + suggest_unsafe = False # add this message at most once + + invalid = bool(email) def choose_account(accounts): @@ -119,7 +134,7 @@ def get_valid_domains(domains): valid_domains = [] for domain in domains: try: - valid_domains.append(le_util.enforce_domain_sanity(domain)) + valid_domains.append(util.enforce_domain_sanity(domain)) except errors.ConfigurationError: continue return valid_domains @@ -163,7 +178,7 @@ def _choose_names_manually(): for i, domain in enumerate(domain_list): try: - domain_list[i] = le_util.enforce_domain_sanity(domain) + domain_list[i] = util.enforce_domain_sanity(domain) except errors.ConfigurationError as e: invalid_domains[domain] = e.message diff --git a/certbot/display/util.py b/certbot/display/util.py index 8de607534..b4004997f 100644 --- a/certbot/display/util.py +++ b/certbot/display/util.py @@ -41,10 +41,15 @@ def _wrap_lines(msg): """ lines = msg.splitlines() fixed_l = [] - for line in lines: - fixed_l.append(textwrap.fill(line, 80)) - return os.linesep.join(fixed_l) + for line in lines: + fixed_l.append(textwrap.fill( + line, + 80, + break_long_words=False, + break_on_hyphens=False)) + + return os.linesep.join(fixed_l) @zope.interface.implementer(interfaces.IDisplay) class NcursesDisplay(object): @@ -265,7 +270,11 @@ class FileDisplay(object): """ ans = raw_input( - textwrap.fill("%s (Enter 'c' to cancel): " % message, 80)) + textwrap.fill( + "%s (Enter 'c' to cancel): " % message, + 80, + break_long_words=False, + break_on_hyphens=False)) if ans == "c" or ans == "C": return CANCEL, "-1" @@ -402,7 +411,11 @@ class FileDisplay(object): # Write out the menu choices for i, desc in enumerate(choices, 1): self.outfile.write( - textwrap.fill("{num}: {desc}".format(num=i, desc=desc), 80)) + textwrap.fill( + "{num}: {desc}".format(num=i, desc=desc), + 80, + break_long_words=False, + break_on_hyphens=False)) # Keep this outside of the textwrap self.outfile.write(os.linesep) diff --git a/certbot/interfaces.py b/certbot/interfaces.py index 8e8666e70..37835462e 100644 --- a/certbot/interfaces.py +++ b/certbot/interfaces.py @@ -201,9 +201,9 @@ class IConfig(zope.interface.Interface): "Email used for registration and recovery contact.") rsa_key_size = zope.interface.Attribute("Size of the RSA key.") must_staple = zope.interface.Attribute( - "Whether to request the OCSP Must Staple certificate extension. " - "Additional setup may be required after issuance. This does not " - "currently autoconfigure web servers for OCSP stapling. ") + "Adds the OCSP Must Staple extension to the certificate. " + "Autoconfigures OCSP Stapling for supported setups " + "(Apache version >= 2.3.3 ).") config_dir = zope.interface.Attribute("Configuration directory.") work_dir = zope.interface.Attribute("Working directory.") diff --git a/certbot/main.py b/certbot/main.py index 9c8849cfe..3575b862e 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -2,6 +2,7 @@ from __future__ import print_function import atexit import dialog +import errno import functools import logging.handlers import os @@ -25,7 +26,7 @@ from certbot import constants from certbot import errors from certbot import hooks from certbot import interfaces -from certbot import le_util +from certbot import util from certbot import log from certbot import reporter from certbot import renewal @@ -230,7 +231,7 @@ def _find_duplicative_certs(config, domains): cli_config = configuration.RenewerConfiguration(config) configs_dir = cli_config.renewal_configs_dir # Verify the directory is there - le_util.make_or_verify_dir(configs_dir, mode=0o755, uid=os.geteuid()) + util.make_or_verify_dir(configs_dir, mode=0o755, uid=os.geteuid()) for renewal_file in renewal.renewal_conf_files(cli_config): try: @@ -293,7 +294,7 @@ def _report_new_cert(config, cert_path, fullchain_path): msg = ('Congratulations! Your certificate {0} been saved at {1}.' ' Your cert will expire on {2}. To obtain a new or tweaked version of this ' 'certificate in the future, simply run {3} again{4}. ' - 'To non-interactively renew *all* of your ceriticates, run "{3} renew"' + 'To non-interactively renew *all* of your certificates, run "{3} renew"' .format(and_chain, path, expiry, cli.cli_command, verbswitch)) reporter_util.add_message(msg, reporter_util.MEDIUM_PRIORITY) @@ -367,6 +368,48 @@ def _init_le_client(config, authenticator, installer): return client.Client(config, acc, authenticator, installer, acme=acme) +def register(config, unused_plugins): + """Create or modify accounts on the server.""" + + # Portion of _determine_account logic to see whether accounts already + # exist or not. + account_storage = account.AccountFileStorage(config) + accounts = account_storage.find_all() + + # registering a new account + if not config.update_registration: + if len(accounts) > 0: + # TODO: add a flag to register a duplicate account (this will + # also require extending _determine_account's behavior + # or else extracting the registration code from there) + return ("There is an existing account; registration of a " + "duplicate account with this command is currently " + "unsupported.") + # _determine_account will register an account + _determine_account(config) + return + + # --update-registration + if len(accounts) == 0: + return "Could not find an existing account to update." + if config.email is None: + if config.register_unsafely_without_email: + return ("--register-unsafely-without-email provided, however, a " + "new e-mail address must\ncurrently be provided when " + "updating a registration.") + config.namespace.email = display_ops.get_email(optional=False) + + acc, acme = _determine_account(config) + acme_client = client.Client(config, acc, None, None, acme=acme) + # We rely on an exception to interrupt this process if it didn't work. + acc.regr = acme_client.acme.update_registration(acc.regr.update( + body=acc.regr.body.update(contact=('mailto:' + config.email,)))) + account_storage.save_regr(acc) + reporter_util = zope.component.getUtility(interfaces.IReporter) + msg = "Your e-mail address was updated to {0}.".format(config.email) + reporter_util.add_message(msg, reporter_util.MEDIUM_PRIORITY) + + def install(config, plugins): """Install a previously obtained cert in a server.""" # XXX: Update for renewer/RenewableCert @@ -547,8 +590,16 @@ def renew(config, unused_plugins): def setup_log_file_handler(config, logfile, fmt): """Setup file debug logging.""" log_file_path = os.path.join(config.logs_dir, logfile) - handler = logging.handlers.RotatingFileHandler( - log_file_path, maxBytes=2 ** 20, backupCount=10) + try: + handler = logging.handlers.RotatingFileHandler( + log_file_path, maxBytes=2 ** 20, backupCount=10) + except IOError as e: + if e.errno == errno.EACCES: + msg = ("Access denied writing to {0}. To run as non-root, set " + + "--logs-dir, --config-dir, --work-dir to writable paths.") + raise errors.Error(msg.format(log_file_path)) + else: + raise # rotate on each invocation, rollover only possible when maxBytes # is nonzero and backupCount is nonzero, so we set maxBytes as big # as possible not to overrun in single CLI invocation (1MB). @@ -660,12 +711,12 @@ def main(cli_args=sys.argv[1:]): # Setup logging ASAP, otherwise "No handlers could be found for # logger ..." TODO: this should be done before plugins discovery for directory in config.config_dir, config.work_dir: - le_util.make_or_verify_dir( + util.make_or_verify_dir( directory, constants.CONFIG_DIRS_MODE, os.geteuid(), "--strict-permissions" in cli_args) # TODO: logs might contain sensitive data such as contents of the # private key! #525 - le_util.make_or_verify_dir( + util.make_or_verify_dir( config.logs_dir, 0o700, os.geteuid(), "--strict-permissions" in cli_args) setup_logging(config, _cli_log_handler, logfile='letsencrypt.log') cli.possible_deprecation_warning(config) diff --git a/certbot/plugins/common.py b/certbot/plugins/common.py index 757bf19d8..007105c7b 100644 --- a/certbot/plugins/common.py +++ b/certbot/plugins/common.py @@ -12,7 +12,7 @@ from acme.jose import util as jose_util from certbot import constants from certbot import interfaces -from certbot import le_util +from certbot import util def option_namespace(name): @@ -255,7 +255,7 @@ class TLSSNI01(object): # Write out challenge cert and key with open(cert_path, "wb") as cert_chall_fd: cert_chall_fd.write(cert_pem) - with le_util.safe_open(key_path, 'wb', chmod=0o400) as key_file: + with util.safe_open(key_path, 'wb', chmod=0o400) as key_file: key_file.write(key_pem) return response diff --git a/certbot/plugins/common_test.py b/certbot/plugins/common_test.py index 0dd1cd522..f3ea714c4 100644 --- a/certbot/plugins/common_test.py +++ b/certbot/plugins/common_test.py @@ -193,7 +193,7 @@ class TLSSNI01Test(unittest.TestCase): with mock.patch("certbot.plugins.common.open", mock_open, create=True): - with mock.patch("certbot.plugins.common.le_util.safe_open", + with mock.patch("certbot.plugins.common.util.safe_open", mock_safe_open): # pylint: disable=protected-access self.assertEqual(response, self.sni._setup_challenge_cert( diff --git a/certbot/renewal.py b/certbot/renewal.py index b5b982972..95f64b94d 100644 --- a/certbot/renewal.py +++ b/certbot/renewal.py @@ -18,7 +18,7 @@ from certbot import constants from certbot import crypto_util from certbot import errors from certbot import interfaces -from certbot import le_util +from certbot import util from certbot import hooks from certbot import storage from certbot.plugins import disco as plugins_disco @@ -60,7 +60,8 @@ def _reconstitute(config, full_path): try: renewal_candidate = storage.RenewableCert( full_path, configuration.RenewerConfiguration(config)) - except (errors.CertStorageError, IOError): + except (errors.CertStorageError, IOError) as exc: + logger.warning(exc) logger.warning("Renewal configuration file %s is broken. Skipping.", full_path) logger.debug("Traceback was:\n%s", traceback.format_exc()) return None @@ -86,7 +87,7 @@ def _reconstitute(config, full_path): return None try: - config.domains = [le_util.enforce_domain_sanity(d) + config.domains = [util.enforce_domain_sanity(d) for d in renewal_candidate.names()] except errors.ConfigurationError as error: logger.warning("Renewal configuration file %s references a cert " diff --git a/certbot/reporter.py b/certbot/reporter.py index d509cb0b8..e07011aee 100644 --- a/certbot/reporter.py +++ b/certbot/reporter.py @@ -11,11 +11,16 @@ from six.moves import queue # pylint: disable=import-error import zope.interface from certbot import interfaces -from certbot import le_util +from certbot import util logger = logging.getLogger(__name__) +# Store the pid of the process that first imported this module so that +# atexit_print_messages side-effects such as error reporting can be limited to +# this process and not any fork()'d children. +INITIAL_PID = os.getpid() + @zope.interface.implementer(interfaces.IReporter) class Reporter(object): @@ -55,12 +60,14 @@ class Reporter(object): self.messages.put(self._msg_type(priority, msg, on_crash)) logger.info("Reporting to user: %s", msg) - def atexit_print_messages(self, pid=os.getpid()): + def atexit_print_messages(self, pid=None): """Function to be registered with atexit to print messages. :param int pid: Process ID """ + if pid is None: + pid = INITIAL_PID # This ensures that messages are only printed from the process that # created the Reporter. if pid == os.getpid(): @@ -79,13 +86,18 @@ class Reporter(object): bold_on = sys.stdout.isatty() if not self.config.quiet: if bold_on: - print(le_util.ANSI_SGR_BOLD) + print(util.ANSI_SGR_BOLD) print('IMPORTANT NOTES:') first_wrapper = textwrap.TextWrapper( - initial_indent=' - ', subsequent_indent=(' ' * 3)) + initial_indent=' - ', + subsequent_indent=(' ' * 3), + break_long_words=False, + break_on_hyphens=False) next_wrapper = textwrap.TextWrapper( initial_indent=first_wrapper.subsequent_indent, - subsequent_indent=first_wrapper.subsequent_indent) + subsequent_indent=first_wrapper.subsequent_indent, + break_long_words=False, + break_on_hyphens=False) while not self.messages.empty(): msg = self.messages.get() if self.config.quiet: @@ -96,7 +108,7 @@ class Reporter(object): if no_exception or msg.on_crash: if bold_on and msg.priority > self.HIGH_PRIORITY: if not self.config.quiet: - sys.stdout.write(le_util.ANSI_SGR_RESET) + sys.stdout.write(util.ANSI_SGR_RESET) bold_on = False lines = msg.text.splitlines() print(first_wrapper.fill(lines[0])) @@ -104,4 +116,4 @@ class Reporter(object): print("\n".join( next_wrapper.fill(line) for line in lines[1:])) if bold_on and not self.config.quiet: - sys.stdout.write(le_util.ANSI_SGR_RESET) + sys.stdout.write(util.ANSI_SGR_RESET) diff --git a/certbot/reverter.py b/certbot/reverter.py index 16ee5d8a4..f8140d60d 100644 --- a/certbot/reverter.py +++ b/certbot/reverter.py @@ -13,7 +13,7 @@ import zope.component from certbot import constants from certbot import errors from certbot import interfaces -from certbot import le_util +from certbot import util from certbot.display import util as display_util @@ -33,7 +33,7 @@ class Reverter(object): def __init__(self, config): self.config = config - le_util.make_or_verify_dir( + util.make_or_verify_dir( config.backup_dir, constants.CONFIG_DIRS_MODE, os.geteuid(), self.config.strict_permissions) @@ -185,7 +185,7 @@ class Reverter(object): :raises .ReverterError: if unable to add checkpoint """ - le_util.make_or_verify_dir( + util.make_or_verify_dir( cp_dir, constants.CONFIG_DIRS_MODE, os.geteuid(), self.config.strict_permissions) @@ -281,7 +281,7 @@ class Reverter(object): csvreader = csv.reader(csvfile) for command in reversed(list(csvreader)): try: - le_util.run_script(command) + util.run_script(command) except errors.SubprocessError: logger.error( "Unable to run undo command: %s", " ".join(command)) @@ -397,7 +397,7 @@ class Reverter(object): else: cp_dir = self.config.in_progress_dir - le_util.make_or_verify_dir( + util.make_or_verify_dir( cp_dir, constants.CONFIG_DIRS_MODE, os.geteuid(), self.config.strict_permissions) diff --git a/certbot/storage.py b/certbot/storage.py index 6c13eb844..82fdbfd54 100644 --- a/certbot/storage.py +++ b/certbot/storage.py @@ -7,18 +7,20 @@ import re import configobj import parsedatetime import pytz +import six import certbot +from certbot import cli from certbot import constants from certbot import crypto_util from certbot import errors from certbot import error_handler -from certbot import le_util +from certbot import util logger = logging.getLogger(__name__) ALL_FOUR = ("cert", "privkey", "chain", "fullchain") -CURRENT_VERSION = le_util.get_strict_version(certbot.__version__) +CURRENT_VERSION = util.get_strict_version(certbot.__version__) def config_with_defaults(config=None): @@ -158,36 +160,13 @@ def relevant_values(all_values): :param dict all_values: The original values. :returns: A new dictionary containing items that can be used in renewal. - :rtype dict:""" + :rtype dict: - from certbot import cli - - def _is_cli_default(option, value): - # Look through the CLI parser defaults and see if this option is - # both present and equal to the specified value. If not, return - # False. - # pylint: disable=protected-access - for x in cli.helpful_parser.parser._actions: - if x.dest == option: - if x.default == value: - return True - else: - break - return False - - values = dict() - for option, value in all_values.iteritems(): - # Try to find reasons to store this item in the - # renewal config. It can be stored if it is relevant and - # (it is set_by_cli() or flag_default() is different - # from the value or flag_default() doesn't exist). - if _relevant(option): - if (cli.set_by_cli(option) - or not _is_cli_default(option, value)): -# or option not in constants.CLI_DEFAULTS -# or constants.CLI_DEFAULTS[option] != value): - values[option] = value - return values + """ + return dict( + (option, value) + for option, value in six.iteritems(all_values) + if _relevant(option) and cli.option_was_set(option, value)) class RenewableCert(object): # pylint: disable=too-many-instance-attributes @@ -264,7 +243,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes conf_version = self.configuration.get("version") if (conf_version is not None and - le_util.get_strict_version(conf_version) > CURRENT_VERSION): + util.get_strict_version(conf_version) > CURRENT_VERSION): logger.warning( "Attempting to parse the version %s renewal configuration " "file found at %s with version %s of Certbot. This might not " @@ -616,7 +595,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes if target is None: raise errors.CertStorageError("could not find cert file") with open(target) as f: - return crypto_util.get_sans_from_cert(f.read()) + return crypto_util.get_names_from_cert(f.read()) def autodeployment_is_enabled(self): """Is automatic deployment enabled for this cert? @@ -769,7 +748,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes if not os.path.exists(i): os.makedirs(i, 0o700) logger.debug("Creating directory %s.", i) - config_file, config_filename = le_util.unique_lineage_name( + config_file, config_filename = util.unique_lineage_name( cli_config.renewal_configs_dir, lineagename) if not config_filename.endswith(".conf"): raise errors.CertStorageError( diff --git a/certbot/tests/account_test.py b/certbot/tests/account_test.py index a96e57507..4cd2bfebf 100644 --- a/certbot/tests/account_test.py +++ b/certbot/tests/account_test.py @@ -137,6 +137,16 @@ class AccountFileStorageTest(unittest.TestCase): # restore self.assertEqual(self.acc, self.storage.load(self.acc.id)) + def test_save_regr(self): + self.storage.save_regr(self.acc) + account_path = os.path.join(self.config.accounts_dir, self.acc.id) + self.assertTrue(os.path.exists(account_path)) + self.assertTrue(os.path.exists(os.path.join( + account_path, "regr.json"))) + for file_name in "meta.json", "private_key.json": + self.assertFalse(os.path.exists( + os.path.join(account_path, file_name))) + def test_find_all(self): self.storage.save(self.acc) self.assertEqual([self.acc], self.storage.find_all()) diff --git a/certbot/tests/auth_handler_test.py b/certbot/tests/auth_handler_test.py index 3facd4f7c..eccc36418 100644 --- a/certbot/tests/auth_handler_test.py +++ b/certbot/tests/auth_handler_test.py @@ -11,7 +11,7 @@ from acme import messages from certbot import achallenges from certbot import errors -from certbot import le_util +from certbot import util from certbot.tests import acme_util @@ -69,7 +69,7 @@ class GetAuthorizationsTest(unittest.TestCase): self.mock_auth.perform.side_effect = gen_auth_resp - self.mock_account = mock.Mock(key=le_util.Key("file_path", "PEM")) + self.mock_account = mock.Mock(key=util.Key("file_path", "PEM")) self.mock_net = mock.MagicMock(spec=acme_client.Client) self.handler = AuthHandler( diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 06aba4ab3..9a2009e43 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -22,7 +22,7 @@ from certbot import configuration from certbot import constants from certbot import crypto_util from certbot import errors -from certbot import le_util +from certbot import util from certbot import main from certbot import renewal from certbot import storage @@ -149,6 +149,14 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods args.extend(['--email', 'io@io.is']) self._cli_missing_flag(args, "--agree-tos") + @mock.patch('certbot.main.renew') + def test_gui(self, renew): + args = ['renew', '--dialog'] + # --text conflicts with --dialog + self.standard_args.remove('--text') + self._call(args) + self.assertFalse(renew.call_args[0][0].noninteractive_mode) + @mock.patch('certbot.main.client.acme_client.Client') @mock.patch('certbot.main._determine_account') @mock.patch('certbot.main.client.Client.obtain_and_enroll_certificate') @@ -163,13 +171,13 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods with mock.patch('certbot.main.client.acme_client.ClientNetwork') as acme_net: self._call_no_clientmock(args) - os_ver = " ".join(le_util.get_os_info()) + os_ver = util.get_os_info_ua() ua = acme_net.call_args[1]["user_agent"] self.assertTrue(os_ver in ua) import platform plat = platform.platform() if "linux" in plat.lower(): - self.assertTrue(platform.linux_distribution()[0] in ua) + self.assertTrue(util.get_os_info_ua() in ua) with mock.patch('certbot.main.client.acme_client.ClientNetwork') as acme_net: ua = "bandersnatch" @@ -201,7 +209,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods '--key-path', 'key', '--chain-path', 'chain']) self.assertEqual(mock_pick_installer.call_count, 1) - @mock.patch('certbot.le_util.exe_exists') + @mock.patch('certbot.util.exe_exists') def test_configurator_selection(self, mock_exe_exists): mock_exe_exists.return_value = True real_plugins = disco.PluginsRegistry.find_all() @@ -422,6 +430,13 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods for arg in conflicting_args: self.assertTrue(arg in error.message) + def test_must_staple_flag(self): + parse = self._get_argument_parser() + short_args = ['--must-staple'] + namespace = parse(short_args) + self.assertTrue(namespace.must_staple) + self.assertTrue(namespace.staple) + def test_staging_flag(self): parse = self._get_argument_parser() short_args = ['--staging'] @@ -432,6 +447,19 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods short_args += '--server example.com'.split() self._check_server_conflict_message(short_args, '--staging') + def test_option_was_set(self): + key_size_option = 'rsa_key_size' + key_size_value = cli.flag_default(key_size_option) + self._get_argument_parser()( + '--rsa-key-size {0}'.format(key_size_value).split()) + + self.assertTrue(cli.option_was_set(key_size_option, key_size_value)) + self.assertTrue(cli.option_was_set('no_verify_ssl', True)) + + config_dir_option = 'config_dir' + self.assertFalse(cli.option_was_set( + config_dir_option, cli.flag_default(config_dir_option))) + def _assert_dry_run_flag_worked(self, namespace, existing_account): self.assertTrue(namespace.dry_run) self.assertTrue(namespace.break_my_certs) @@ -636,6 +664,18 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods out = stdout.getvalue() self.assertEqual("", out) + def test_renew_hook_validation(self): + self._make_test_renewal_conf('sample-renewal.conf') + args = ["renew", "--dry-run", "--post-hook=no-such-command"] + self._test_renewal_common(True, [], args=args, should_renew=False, + error_expected=True) + + def test_renew_no_hook_validation(self): + self._make_test_renewal_conf('sample-renewal.conf') + args = ["renew", "--dry-run", "--post-hook=no-such-command", + "--disable-hook-validation"] + self._test_renewal_common(True, [], args=args, should_renew=True, + error_expected=False) @mock.patch("certbot.cli.set_by_cli") def test_ancient_webroot_renewal_conf(self, mock_set_by_cli): @@ -909,6 +949,78 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self._call(['-c', test_util.vector_path('cli.ini')]) self.assertTrue(mocked_run.called) + def test_register(self): + with mock.patch('certbot.main.client') as mocked_client: + acc = mock.MagicMock() + acc.id = "imaginary_account" + mocked_client.register.return_value = (acc, "worked") + self._call_no_clientmock(["register", "--email", "user@example.org"]) + # TODO: It would be more correct to explicitly check that + # _determine_account() gets called in the above case, + # but coverage statistics should also show that it did. + with mock.patch('certbot.main.account') as mocked_account: + mocked_storage = mock.MagicMock() + mocked_account.AccountFileStorage.return_value = mocked_storage + mocked_storage.find_all.return_value = ["an account"] + x = self._call_no_clientmock(["register", "--email", "user@example.org"]) + self.assertTrue("There is an existing account" in x[0]) + + def test_update_registration_no_existing_accounts(self): + # with mock.patch('certbot.main.client') as mocked_client: + with mock.patch('certbot.main.account') as mocked_account: + mocked_storage = mock.MagicMock() + mocked_account.AccountFileStorage.return_value = mocked_storage + mocked_storage.find_all.return_value = [] + x = self._call_no_clientmock( + ["register", "--update-registration", "--email", + "user@example.org"]) + self.assertTrue("Could not find an existing account" in x[0]) + + def test_update_registration_unsafely(self): + # This test will become obsolete when register --update-registration + # supports removing an e-mail address from the account + with mock.patch('certbot.main.account') as mocked_account: + mocked_storage = mock.MagicMock() + mocked_account.AccountFileStorage.return_value = mocked_storage + mocked_storage.find_all.return_value = ["an account"] + x = self._call_no_clientmock( + "register --update-registration " + "--register-unsafely-without-email".split()) + self.assertTrue("--register-unsafely-without-email" in x[0]) + + @mock.patch('certbot.main.display_ops.get_email') + @mock.patch('certbot.main.zope.component.getUtility') + def test_update_registration_with_email(self, mock_utility, mock_email): + email = "user@example.com" + mock_email.return_value = email + with mock.patch('certbot.main.client') as mocked_client: + with mock.patch('certbot.main.account') as mocked_account: + with mock.patch('certbot.main._determine_account') as mocked_det: + with mock.patch('certbot.main.client') as mocked_client: + mocked_storage = mock.MagicMock() + mocked_account.AccountFileStorage.return_value = mocked_storage + mocked_storage.find_all.return_value = ["an account"] + mocked_det.return_value = (mock.MagicMock(), "foo") + acme_client = mock.MagicMock() + mocked_client.Client.return_value = acme_client + x = self._call_no_clientmock( + ["register", "--update-registration"]) + # When registration change succeeds, the return value + # of register() is None + self.assertTrue(x[0] is None) + # and we got supposedly did update the registration from + # the server + self.assertTrue( + acme_client.acme.update_registration.called) + # and we saved the updated registration on disk + self.assertTrue(mocked_storage.save_regr.called) + self.assertTrue( + email in mock_utility().add_message.call_args[0][0]) + + def test_conflicting_args(self): + args = ['renew', '--dialog', '--text'] + self.assertRaises(errors.Error, self._call, args) + class DetermineAccountTest(unittest.TestCase): """Tests for certbot.cli._determine_account.""" @@ -985,7 +1097,7 @@ class DuplicativeCertsTest(storage_test.BaseRenewableCertTest): def tearDown(self): shutil.rmtree(self.tempdir) - @mock.patch('certbot.le_util.make_or_verify_dir') + @mock.patch('certbot.util.make_or_verify_dir') def test_find_duplicative_names(self, unused_makedir): from certbot.main import _find_duplicative_certs test_cert = test_util.load_vector('cert-san.pem') diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index 8490efd9f..9156277a9 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -11,7 +11,7 @@ from acme import jose from certbot import account from certbot import errors -from certbot import le_util +from certbot import util from certbot.tests import test_util @@ -137,7 +137,7 @@ class ClientTest(unittest.TestCase): @mock.patch("certbot.client.logger") def test_obtain_certificate_from_csr(self, mock_logger): self._mock_obtain_certificate() - test_csr = le_util.CSR(form="der", file=None, data=CSR_SAN) + test_csr = util.CSR(form="der", file=None, data=CSR_SAN) auth_handler = self.client.auth_handler authzr = auth_handler.get_authorizations(self.eg_domains, False) @@ -172,7 +172,7 @@ class ClientTest(unittest.TestCase): def test_obtain_certificate(self, mock_crypto_util): self._mock_obtain_certificate() - csr = le_util.CSR(form="der", file=None, data=CSR_SAN) + csr = util.CSR(form="der", file=None, data=CSR_SAN) mock_crypto_util.init_save_csr.return_value = csr mock_crypto_util.init_save_key.return_value = mock.sentinel.key domains = ["example.com", "www.example.com"] diff --git a/certbot/tests/colored_logging_test.py b/certbot/tests/colored_logging_test.py index 91c6b8c08..0a7929561 100644 --- a/certbot/tests/colored_logging_test.py +++ b/certbot/tests/colored_logging_test.py @@ -4,7 +4,7 @@ import unittest import six -from certbot import le_util +from certbot import util class StreamHandlerTest(unittest.TestCase): @@ -32,9 +32,9 @@ class StreamHandlerTest(unittest.TestCase): self.logger.debug(msg) self.assertEqual(self.stream.getvalue(), - '{0}{1}{2}\n'.format(le_util.ANSI_SGR_RED, + '{0}{1}{2}\n'.format(util.ANSI_SGR_RED, msg, - le_util.ANSI_SGR_RESET)) + util.ANSI_SGR_RESET)) if __name__ == "__main__": diff --git a/certbot/tests/crypto_util_test.py b/certbot/tests/crypto_util_test.py index eeea0f4ab..5a592bbb1 100644 --- a/certbot/tests/crypto_util_test.py +++ b/certbot/tests/crypto_util_test.py @@ -10,7 +10,7 @@ import zope.component from certbot import errors from certbot import interfaces -from certbot import le_util +from certbot import util from certbot.tests import test_util @@ -63,7 +63,7 @@ class InitSaveCSRTest(unittest.TestCase): shutil.rmtree(self.csr_dir) @mock.patch('certbot.crypto_util.make_csr') - @mock.patch('certbot.crypto_util.le_util.make_or_verify_dir') + @mock.patch('certbot.crypto_util.util.make_or_verify_dir') def test_it(self, unused_mock_verify, mock_csr): from certbot.crypto_util import init_save_csr @@ -174,9 +174,9 @@ class ImportCSRFileTest(unittest.TestCase): self.assertEqual( (OpenSSL.crypto.FILETYPE_ASN1, - le_util.CSR(file=csrfile, - data=data, - form="der"), + util.CSR(file=csrfile, + data=data, + form="der"), ["example.com"],), self._call(csrfile, data)) @@ -186,9 +186,9 @@ class ImportCSRFileTest(unittest.TestCase): self.assertEqual( (OpenSSL.crypto.FILETYPE_PEM, - le_util.CSR(file=csrfile, - data=data, - form="pem"), + util.CSR(file=csrfile, + data=data, + form="pem"), ["example.com"],), self._call(csrfile, data)) @@ -273,6 +273,32 @@ class GetSANsFromCSRTest(unittest.TestCase): [], self._call(test_util.load_vector('csr-nosans.pem'))) +class GetNamesFromCertTest(unittest.TestCase): + """Tests for certbot.crypto_util.get_names_from_cert.""" + + @classmethod + def _call(cls, *args, **kwargs): + from certbot.crypto_util import get_names_from_cert + return get_names_from_cert(*args, **kwargs) + + def test_single(self): + self.assertEqual( + ['example.com'], + self._call(test_util.load_vector('cert.pem'))) + + def test_san(self): + self.assertEqual( + ['example.com', 'www.example.com'], + self._call(test_util.load_vector('cert-san.pem'))) + + def test_common_name_sans_order(self): + # Tests that the common name comes first + # followed by the SANS in alphabetical order + self.assertEqual( + ['example.com'] + ['{0}.example.com'.format(c) for c in 'abcd'], + self._call(test_util.load_vector('cert-5sans.pem'))) + + class GetNamesFromCSRTest(unittest.TestCase): """Tests for certbot.crypto_util.get_names_from_csr.""" @classmethod diff --git a/certbot/tests/display/ops_test.py b/certbot/tests/display/ops_test.py index 05cb6b12d..3aff37d86 100644 --- a/certbot/tests/display/ops_test.py +++ b/certbot/tests/display/ops_test.py @@ -12,6 +12,7 @@ from acme import jose from acme import messages from certbot import account +from certbot import errors from certbot import interfaces from certbot.display import util as display_util @@ -37,41 +38,39 @@ class GetEmailTest(unittest.TestCase): def test_cancel_none(self): self.input.return_value = (display_util.CANCEL, "foo@bar.baz") - self.assertTrue(self._call() is None) + self.assertRaises(errors.Error, self._call) + self.assertRaises(errors.Error, self._call, optional=False) def test_ok_safe(self): self.input.return_value = (display_util.OK, "foo@bar.baz") - with mock.patch("certbot.display.ops.le_util.safe_email") as mock_safe_email: + with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email: mock_safe_email.return_value = True self.assertTrue(self._call() is "foo@bar.baz") def test_ok_not_safe(self): self.input.return_value = (display_util.OK, "foo@bar.baz") - with mock.patch("certbot.display.ops.le_util.safe_email") as mock_safe_email: + with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email: mock_safe_email.side_effect = [False, True] self.assertTrue(self._call() is "foo@bar.baz") - def test_more_and_invalid_flags(self): - more_txt = "--register-unsafely-without-email" + def test_invalid_flag(self): invalid_txt = "There seem to be problems" - base_txt = "Enter email" self.input.return_value = (display_util.OK, "foo@bar.baz") - with mock.patch("certbot.display.ops.le_util.safe_email") as mock_safe_email: + with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email: mock_safe_email.return_value = True self._call() - msg = self.input.call_args[0][0] - self.assertTrue(more_txt not in msg) - self.assertTrue(invalid_txt not in msg) - self.assertTrue(base_txt in msg) - self._call(more=True) - msg = self.input.call_args[0][0] - self.assertTrue(more_txt in msg) - self.assertTrue(invalid_txt not in msg) - self._call(more=True, invalid=True) - msg = self.input.call_args[0][0] - self.assertTrue(more_txt in msg) - self.assertTrue(invalid_txt in msg) - self.assertTrue(base_txt in msg) + self.assertTrue(invalid_txt not in self.input.call_args[0][0]) + self._call(invalid=True) + self.assertTrue(invalid_txt in self.input.call_args[0][0]) + + def test_optional_flag(self): + self.input.return_value = (display_util.OK, "foo@bar.baz") + with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email: + mock_safe_email.side_effect = [False, True] + self._call(optional=False) + for call in self.input.call_args_list: + self.assertTrue( + "--register-unsafely-without-email" not in call[0][0]) class ChooseAccountTest(unittest.TestCase): diff --git a/certbot/tests/reverter_test.py b/certbot/tests/reverter_test.py index 85234b76a..58cc68dce 100644 --- a/certbot/tests/reverter_test.py +++ b/certbot/tests/reverter_test.py @@ -164,7 +164,7 @@ class ReverterCheckpointLocalTest(unittest.TestCase): errors.ReverterError, self.reverter.register_undo_command, True, ["command"]) - @mock.patch("certbot.le_util.run_script") + @mock.patch("certbot.util.run_script") def test_run_undo_commands(self, mock_run): mock_run.side_effect = ["", errors.SubprocessError] coms = [ diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index f19b7d89d..261500b98 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -11,6 +11,7 @@ import mock import pytz import certbot +from certbot import cli from certbot import configuration from certbot import errors from certbot.storage import ALL_FOUR @@ -84,18 +85,20 @@ class BaseRenewableCertTest(unittest.TestCase): def tearDown(self): shutil.rmtree(self.tempdir) + def _write_out_kind(self, kind, ver, value=None): + link = getattr(self.test_rc, kind) + if os.path.lexists(link): + os.unlink(link) + os.symlink(os.path.join(os.path.pardir, os.path.pardir, "archive", + "example.org", "{0}{1}.pem".format(kind, ver)), + link) + with open(link, "w") as f: + f.write(kind if value is None else value) + def _write_out_ex_kinds(self): for kind in ALL_FOUR: - where = getattr(self.test_rc, kind) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}12.pem".format(kind)), where) - with open(where, "w") as f: - f.write(kind) - os.unlink(where) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}11.pem".format(kind)), where) - with open(where, "w") as f: - f.write(kind) + self._write_out_kind(kind, 12) + self._write_out_kind(kind, 11) class RenewableCertTests(BaseRenewableCertTest): @@ -204,10 +207,7 @@ class RenewableCertTests(BaseRenewableCertTest): def test_current_target(self): # Relative path logic - os.symlink(os.path.join("..", "..", "archive", "example.org", - "cert17.pem"), self.test_rc.cert) - with open(self.test_rc.cert, "w") as f: - f.write("cert") + self._write_out_kind("cert", 17) self.assertTrue(os.path.samefile(self.test_rc.current_target("cert"), os.path.join(self.tempdir, "archive", "example.org", @@ -225,12 +225,8 @@ class RenewableCertTests(BaseRenewableCertTest): def test_current_version(self): for ver in (1, 5, 10, 20): - os.symlink(os.path.join("..", "..", "archive", "example.org", - "cert{0}.pem".format(ver)), - self.test_rc.cert) - with open(self.test_rc.cert, "w") as f: - f.write("cert") - os.unlink(self.test_rc.cert) + self._write_out_kind("cert", ver) + os.unlink(self.test_rc.cert) os.symlink(os.path.join("..", "..", "archive", "example.org", "cert10.pem"), self.test_rc.cert) self.assertEqual(self.test_rc.current_version("cert"), 10) @@ -241,61 +237,30 @@ class RenewableCertTests(BaseRenewableCertTest): def test_latest_and_next_versions(self): for ver in xrange(1, 6): for kind in ALL_FOUR: - where = getattr(self.test_rc, kind) - if os.path.islink(where): - os.unlink(where) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}{1}.pem".format(kind, ver)), where) - with open(where, "w") as f: - f.write(kind) + self._write_out_kind(kind, ver) self.assertEqual(self.test_rc.latest_common_version(), 5) self.assertEqual(self.test_rc.next_free_version(), 6) # Having one kind of file of a later version doesn't change the # result - os.unlink(self.test_rc.privkey) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "privkey7.pem"), self.test_rc.privkey) - with open(self.test_rc.privkey, "w") as f: - f.write("privkey") + self._write_out_kind("privkey", 7) self.assertEqual(self.test_rc.latest_common_version(), 5) # ... although it does change the next free version self.assertEqual(self.test_rc.next_free_version(), 8) # Nor does having three out of four change the result - os.unlink(self.test_rc.cert) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "cert7.pem"), self.test_rc.cert) - with open(self.test_rc.cert, "w") as f: - f.write("cert") - os.unlink(self.test_rc.fullchain) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "fullchain7.pem"), self.test_rc.fullchain) - with open(self.test_rc.fullchain, "w") as f: - f.write("fullchain") + self._write_out_kind("cert", 7) + self._write_out_kind("fullchain", 7) self.assertEqual(self.test_rc.latest_common_version(), 5) # If we have everything from a much later version, it does change # the result - ver = 17 for kind in ALL_FOUR: - where = getattr(self.test_rc, kind) - if os.path.islink(where): - os.unlink(where) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}{1}.pem".format(kind, ver)), where) - with open(where, "w") as f: - f.write(kind) + self._write_out_kind(kind, 17) self.assertEqual(self.test_rc.latest_common_version(), 17) self.assertEqual(self.test_rc.next_free_version(), 18) def test_update_link_to(self): for ver in xrange(1, 6): for kind in ALL_FOUR: - where = getattr(self.test_rc, kind) - if os.path.islink(where): - os.unlink(where) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}{1}.pem".format(kind, ver)), where) - with open(where, "w") as f: - f.write(kind) + self._write_out_kind(kind, ver) self.assertEqual(ver, self.test_rc.current_version(kind)) # pylint: disable=protected-access self.test_rc._update_link_to("cert", 3) @@ -312,10 +277,7 @@ class RenewableCertTests(BaseRenewableCertTest): "chain3000.pem") def test_version(self): - os.symlink(os.path.join("..", "..", "archive", "example.org", - "cert12.pem"), self.test_rc.cert) - with open(self.test_rc.cert, "w") as f: - f.write("cert") + self._write_out_kind("cert", 12) # TODO: We should probably test that the directory is still the # same, but it's tricky because we can get an absolute # path out when we put a relative path in. @@ -325,13 +287,7 @@ class RenewableCertTests(BaseRenewableCertTest): def test_update_all_links_to_success(self): for ver in xrange(1, 6): for kind in ALL_FOUR: - where = getattr(self.test_rc, kind) - if os.path.islink(where): - os.unlink(where) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}{1}.pem".format(kind, ver)), where) - with open(where, "w") as f: - f.write(kind) + self._write_out_kind(kind, ver) self.assertEqual(ver, self.test_rc.current_version(kind)) self.assertEqual(self.test_rc.latest_common_version(), 5) for ver in xrange(1, 6): @@ -376,13 +332,7 @@ class RenewableCertTests(BaseRenewableCertTest): def test_has_pending_deployment(self): for ver in xrange(1, 6): for kind in ALL_FOUR: - where = getattr(self.test_rc, kind) - if os.path.islink(where): - os.unlink(where) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}{1}.pem".format(kind, ver)), where) - with open(where, "w") as f: - f.write(kind) + self._write_out_kind(kind, ver) self.assertEqual(ver, self.test_rc.current_version(kind)) for ver in xrange(1, 6): self.test_rc.update_all_links_to(ver) @@ -395,24 +345,22 @@ class RenewableCertTests(BaseRenewableCertTest): def test_names(self): # Trying the current version - test_cert = test_util.load_vector("cert-san.pem") - os.symlink(os.path.join("..", "..", "archive", "example.org", - "cert12.pem"), self.test_rc.cert) - with open(self.test_rc.cert, "w") as f: - f.write(test_cert) + self._write_out_kind("cert", 12, test_util.load_vector("cert-san.pem")) self.assertEqual(self.test_rc.names(), ["example.com", "www.example.com"]) # Trying a non-current version - test_cert = test_util.load_vector("cert.pem") - os.unlink(self.test_rc.cert) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "cert15.pem"), self.test_rc.cert) - with open(self.test_rc.cert, "w") as f: - f.write(test_cert) + self._write_out_kind("cert", 15, test_util.load_vector("cert.pem")) self.assertEqual(self.test_rc.names(12), ["example.com", "www.example.com"]) + # Testing common name is listed first + self._write_out_kind( + "cert", 12, test_util.load_vector("cert-5sans.pem")) + self.assertEqual( + self.test_rc.names(12), + ["example.com"] + ["{0}.example.com".format(c) for c in "abcd"]) + # Trying missing cert os.unlink(self.test_rc.cert) self.assertRaises(errors.CertStorageError, self.test_rc.names) @@ -480,13 +428,7 @@ class RenewableCertTests(BaseRenewableCertTest): # No pending deployment for ver in xrange(1, 6): for kind in ALL_FOUR: - where = getattr(self.test_rc, kind) - if os.path.islink(where): - os.unlink(where) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}{1}.pem".format(kind, ver)), where) - with open(where, "w") as f: - f.write(kind) + self._write_out_kind(kind, ver) self.assertFalse(self.test_rc.should_autodeploy()) def test_autorenewal_is_enabled(self): @@ -507,11 +449,7 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertFalse(self.test_rc.should_autorenew()) self.test_rc.configuration["autorenew"] = "1" for kind in ALL_FOUR: - where = getattr(self.test_rc, kind) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}12.pem".format(kind)), where) - with open(where, "w") as f: - f.write(kind) + self._write_out_kind(kind, 12) # Mandatory renewal on the basis of OCSP revocation mock_ocsp.return_value = True self.assertTrue(self.test_rc.should_autorenew()) @@ -525,13 +463,7 @@ class RenewableCertTests(BaseRenewableCertTest): for ver in xrange(1, 6): for kind in ALL_FOUR: - where = getattr(self.test_rc, kind) - if os.path.islink(where): - os.unlink(where) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}{1}.pem".format(kind, ver)), where) - with open(where, "w") as f: - f.write(kind) + self._write_out_kind(kind, ver) self.test_rc.update_all_links_to(3) self.assertEqual( 6, self.test_rc.save_successor(3, "new cert", None, @@ -586,39 +518,33 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertFalse(os.path.islink(self.test_rc.version("privkey", 10))) self.assertFalse(os.path.exists(temp_config_file)) - @mock.patch("certbot.cli.helpful_parser") - def test_relevant_values(self, mock_parser): + def _test_relevant_values_common(self, values): + option = "rsa_key_size" + mock_parser = mock.Mock(args=["--standalone"], verb="certonly", + defaults={option: cli.flag_default(option)}) + + from certbot.storage import relevant_values + with mock.patch("certbot.cli.helpful_parser", mock_parser): + return relevant_values(values) + + def test_relevant_values(self): """Test that relevant_values() can reject an irrelevant value.""" - # pylint: disable=protected-access - from certbot import storage - mock_parser.verb = "certonly" - mock_parser.args = ["--standalone"] - mock_action = mock.Mock(dest="rsa_key_size", default=2048) - mock_parser.parser._actions = [mock_action] - self.assertEqual(storage.relevant_values({"hello": "there"}), {}) + self.assertEqual( + self._test_relevant_values_common({"hello": "there"}), {}) - @mock.patch("certbot.cli.helpful_parser") - def test_relevant_values_default(self, mock_parser): + def test_relevant_values_default(self): """Test that relevant_values() can reject a default value.""" - # pylint: disable=protected-access - from certbot import storage - mock_parser.verb = "certonly" - mock_parser.args = ["--standalone"] - mock_action = mock.Mock(dest="rsa_key_size", default=2048) - mock_parser.parser._actions = [mock_action] - self.assertEqual(storage.relevant_values({"rsa_key_size": 2048}), {}) + option = "rsa_key_size" + values = {option: cli.flag_default(option)} + self.assertEqual(self._test_relevant_values_common(values), {}) - @mock.patch("certbot.cli.helpful_parser") - def test_relevant_values_nondefault(self, mock_parser): + def test_relevant_values_nondefault(self): """Test that relevant_values() can retain a non-default value.""" - # pylint: disable=protected-access - from certbot import storage - mock_parser.verb = "certonly" - mock_parser.args = ["--standalone"] - mock_action = mock.Mock(dest="rsa_key_size", default=2048) - mock_parser.parser._actions = [mock_action] - self.assertEqual(storage.relevant_values({"rsa_key_size": 12}), - {"rsa_key_size": 12}) + values = {"rsa_key_size": 12} + # A copy is given to _test_relevant_values_common + # to make sure values isn't modified by the method + self.assertEqual( + self._test_relevant_values_common(values.copy()), values) @mock.patch("certbot.storage.relevant_values") def test_new_lineage(self, mock_rv): @@ -682,7 +608,7 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertTrue(os.path.exists(os.path.join( self.cli_config.archive_dir, "the-lineage.com", "privkey1.pem"))) - @mock.patch("certbot.storage.le_util.unique_lineage_name") + @mock.patch("certbot.storage.util.unique_lineage_name") def test_invalid_config_filename(self, mock_uln): from certbot import storage mock_uln.return_value = "this_does_not_end_with_dot_conf", "yikes" diff --git a/certbot/tests/testdata/cert-5sans.pem b/certbot/tests/testdata/cert-5sans.pem new file mode 100644 index 000000000..5de7cc6cb --- /dev/null +++ b/certbot/tests/testdata/cert-5sans.pem @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICkTCCAjugAwIBAgIJAJNbfABWQ8bbMA0GCSqGSIb3DQEBCwUAMHkxCzAJBgNV +BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNp +c2NvMScwJQYDVQQKDB5FbGVjdHJvbmljIEZyb250aWVyIEZvdW5kYXRpb24xFDAS +BgNVBAMMC2V4YW1wbGUuY29tMB4XDTE2MDYwOTIzMDEzNloXDTE2MDcwOTIzMDEz +NloweTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcM +DVNhbiBGcmFuY2lzY28xJzAlBgNVBAoMHkVsZWN0cm9uaWMgRnJvbnRpZXIgRm91 +bmRhdGlvbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0BAQEFAANL +ADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580rv2+6QWE +30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABo4GlMIGiMB0GA1UdDgQWBBQmz8jt +S9eUsuQlA1gkjwTAdNWXijAfBgNVHSMEGDAWgBQmz8jtS9eUsuQlA1gkjwTAdNWX +ijAMBgNVHRMEBTADAQH/MFIGA1UdEQRLMEmCDWEuZXhhbXBsZS5jb22CDWIuZXhh +bXBsZS5jb22CDWMuZXhhbXBsZS5jb22CDWQuZXhhbXBsZS5jb22CC2V4YW1wbGUu +Y29tMA0GCSqGSIb3DQEBCwUAA0EAVXmZxB+IJdgFvY2InOYeytTD1QmouDZRtj/T +H/HIpSdsfO7qr4d/ZprI2IhLRxp2S4BiU5Qc5HUkeADcpNd06A== +-----END CERTIFICATE----- diff --git a/certbot/tests/testdata/os-release b/certbot/tests/testdata/os-release new file mode 100644 index 000000000..cd5297acf --- /dev/null +++ b/certbot/tests/testdata/os-release @@ -0,0 +1,7 @@ +NAME="SystemdOS" +VERSION="42.42.42 LTS, Unreal" +ID=systemdos +ID_LIKE=debian +VERSION_ID="42" +HOME_URL="http://www.example.com/" +SUPPORT_URL="http://help.example.com/" diff --git a/certbot/tests/le_util_test.py b/certbot/tests/util_test.py similarity index 66% rename from certbot/tests/le_util_test.py rename to certbot/tests/util_test.py index 6e4eef0f1..8e1b330ed 100644 --- a/certbot/tests/le_util_test.py +++ b/certbot/tests/util_test.py @@ -1,4 +1,4 @@ -"""Tests for certbot.le_util.""" +"""Tests for certbot.util.""" import argparse import errno import os @@ -10,18 +10,18 @@ import unittest import mock import six -import certbot from certbot import errors +from certbot.tests import test_util class RunScriptTest(unittest.TestCase): - """Tests for certbot.le_util.run_script.""" + """Tests for certbot.util.run_script.""" @classmethod def _call(cls, params): - from certbot.le_util import run_script + from certbot.util import run_script return run_script(params) - @mock.patch("certbot.le_util.subprocess.Popen") + @mock.patch("certbot.util.subprocess.Popen") def test_default(self, mock_popen): """These will be changed soon enough with reload.""" mock_popen().returncode = 0 @@ -31,13 +31,13 @@ class RunScriptTest(unittest.TestCase): self.assertEqual(out, "stdout") self.assertEqual(err, "stderr") - @mock.patch("certbot.le_util.subprocess.Popen") + @mock.patch("certbot.util.subprocess.Popen") def test_bad_process(self, mock_popen): mock_popen.side_effect = OSError self.assertRaises(errors.SubprocessError, self._call, ["test"]) - @mock.patch("certbot.le_util.subprocess.Popen") + @mock.patch("certbot.util.subprocess.Popen") def test_failure(self, mock_popen): mock_popen().communicate.return_value = ("", "") mock_popen().returncode = 1 @@ -46,29 +46,29 @@ class RunScriptTest(unittest.TestCase): class ExeExistsTest(unittest.TestCase): - """Tests for certbot.le_util.exe_exists.""" + """Tests for certbot.util.exe_exists.""" @classmethod def _call(cls, exe): - from certbot.le_util import exe_exists + from certbot.util import exe_exists return exe_exists(exe) - @mock.patch("certbot.le_util.os.path.isfile") - @mock.patch("certbot.le_util.os.access") + @mock.patch("certbot.util.os.path.isfile") + @mock.patch("certbot.util.os.access") def test_full_path(self, mock_access, mock_isfile): mock_access.return_value = True mock_isfile.return_value = True self.assertTrue(self._call("/path/to/exe")) - @mock.patch("certbot.le_util.os.path.isfile") - @mock.patch("certbot.le_util.os.access") + @mock.patch("certbot.util.os.path.isfile") + @mock.patch("certbot.util.os.access") def test_on_path(self, mock_access, mock_isfile): mock_access.return_value = True mock_isfile.return_value = True self.assertTrue(self._call("exe")) - @mock.patch("certbot.le_util.os.path.isfile") - @mock.patch("certbot.le_util.os.access") + @mock.patch("certbot.util.os.path.isfile") + @mock.patch("certbot.util.os.access") def test_not_found(self, mock_access, mock_isfile): mock_access.return_value = False mock_isfile.return_value = True @@ -76,7 +76,7 @@ class ExeExistsTest(unittest.TestCase): class MakeOrVerifyDirTest(unittest.TestCase): - """Tests for certbot.le_util.make_or_verify_dir. + """Tests for certbot.util.make_or_verify_dir. Note that it is not possible to test for a wrong directory owner, as this testing script would have to be run as root. @@ -94,7 +94,7 @@ class MakeOrVerifyDirTest(unittest.TestCase): shutil.rmtree(self.root_path, ignore_errors=True) def _call(self, directory, mode): - from certbot.le_util import make_or_verify_dir + from certbot.util import make_or_verify_dir return make_or_verify_dir(directory, mode, self.uid, strict=True) def test_creates_dir_when_missing(self): @@ -117,7 +117,7 @@ class MakeOrVerifyDirTest(unittest.TestCase): class CheckPermissionsTest(unittest.TestCase): - """Tests for certbot.le_util.check_permissions. + """Tests for certbot.util.check_permissions. Note that it is not possible to test for a wrong file owner, as this testing script would have to be run as root. @@ -132,7 +132,7 @@ class CheckPermissionsTest(unittest.TestCase): os.remove(self.path) def _call(self, mode): - from certbot.le_util import check_permissions + from certbot.util import check_permissions return check_permissions(self.path, mode, self.uid) def test_ok_mode(self): @@ -145,7 +145,7 @@ class CheckPermissionsTest(unittest.TestCase): class UniqueFileTest(unittest.TestCase): - """Tests for certbot.le_util.unique_file.""" + """Tests for certbot.util.unique_file.""" def setUp(self): self.root_path = tempfile.mkdtemp() @@ -155,7 +155,7 @@ class UniqueFileTest(unittest.TestCase): shutil.rmtree(self.root_path, ignore_errors=True) def _call(self, mode=0o600): - from certbot.le_util import unique_file + from certbot.util import unique_file return unique_file(self.default_name, mode) def test_returns_fd_for_writing(self): @@ -190,7 +190,7 @@ class UniqueFileTest(unittest.TestCase): class UniqueLineageNameTest(unittest.TestCase): - """Tests for certbot.le_util.unique_lineage_name.""" + """Tests for certbot.util.unique_lineage_name.""" def setUp(self): self.root_path = tempfile.mkdtemp() @@ -199,7 +199,7 @@ class UniqueLineageNameTest(unittest.TestCase): shutil.rmtree(self.root_path, ignore_errors=True) def _call(self, filename, mode=0o777): - from certbot.le_util import unique_lineage_name + from certbot.util import unique_lineage_name return unique_lineage_name(self.root_path, filename, mode) def test_basic(self): @@ -214,14 +214,14 @@ class UniqueLineageNameTest(unittest.TestCase): self.assertTrue(isinstance(name, str)) self.assertTrue("wow-0009.conf" in name) - @mock.patch("certbot.le_util.os.fdopen") + @mock.patch("certbot.util.os.fdopen") def test_failure(self, mock_fdopen): err = OSError("whoops") err.errno = errno.EIO mock_fdopen.side_effect = err self.assertRaises(OSError, self._call, "wow") - @mock.patch("certbot.le_util.os.fdopen") + @mock.patch("certbot.util.os.fdopen") def test_subsequent_failure(self, mock_fdopen): self._call("wow") err = OSError("whoops") @@ -231,7 +231,7 @@ class UniqueLineageNameTest(unittest.TestCase): class SafelyRemoveTest(unittest.TestCase): - """Tests for certbot.le_util.safely_remove.""" + """Tests for certbot.util.safely_remove.""" def setUp(self): self.tmp = tempfile.mkdtemp() @@ -241,7 +241,7 @@ class SafelyRemoveTest(unittest.TestCase): shutil.rmtree(self.tmp) def _call(self): - from certbot.le_util import safely_remove + from certbot.util import safely_remove return safely_remove(self.path) def test_exists(self): @@ -255,7 +255,7 @@ class SafelyRemoveTest(unittest.TestCase): # no error, yay! self.assertFalse(os.path.exists(self.path)) - @mock.patch("certbot.le_util.os.remove") + @mock.patch("certbot.util.os.remove") def test_other_error_passthrough(self, mock_remove): mock_remove.side_effect = OSError self.assertRaises(OSError, self._call) @@ -265,7 +265,7 @@ class SafeEmailTest(unittest.TestCase): """Test safe_email.""" @classmethod def _call(cls, addr): - from certbot.le_util import safe_email + from certbot.util import safe_email return safe_email(addr) def test_valid_emails(self): @@ -293,7 +293,7 @@ class AddDeprecatedArgumentTest(unittest.TestCase): self.parser = argparse.ArgumentParser() def _call(self, argument_name, nargs): - from certbot.le_util import add_deprecated_argument + from certbot.util import add_deprecated_argument add_deprecated_argument(self.parser.add_argument, argument_name, nargs) @@ -309,14 +309,14 @@ class AddDeprecatedArgumentTest(unittest.TestCase): def _get_argparse_warnings(self, args): stderr = six.StringIO() - with mock.patch("certbot.le_util.sys.stderr", new=stderr): + with mock.patch("certbot.util.sys.stderr", new=stderr): self.parser.parse_args(args) return stderr.getvalue() def test_help(self): self._call("--old-option", 2) stdout = six.StringIO() - with mock.patch("certbot.le_util.sys.stdout", new=stdout): + with mock.patch("certbot.util.sys.stdout", new=stdout): try: self.parser.parse_args(["-h"]) except SystemExit: @@ -328,7 +328,7 @@ class EnforceDomainSanityTest(unittest.TestCase): """Test enforce_domain_sanity.""" def _call(self, domain): - from certbot.le_util import enforce_domain_sanity + from certbot.util import enforce_domain_sanity return enforce_domain_sanity(domain) def test_nonascii_str(self): @@ -340,31 +340,66 @@ class EnforceDomainSanityTest(unittest.TestCase): u"eichh\u00f6rnchen.example.com") -class GetStrictVersionTest(unittest.TestCase): - """Tests for certbot.le_util.get_strict_version.""" +class OsInfoTest(unittest.TestCase): + """Test OS / distribution detection""" - @classmethod - def _call(cls, *args, **kwargs): - from certbot.le_util import get_strict_version - return get_strict_version(*args, **kwargs) + def test_systemd_os_release(self): + from certbot.util import (get_os_info, get_systemd_os_info, + get_os_info_ua) - def test_two_dev_versions(self): - self.assertTrue( - self._call("0.0.0.dev20151006") < self._call("0.0.0.dev20151008")) + with mock.patch('os.path.isfile', return_value=True): + self.assertEqual(get_os_info( + test_util.vector_path("os-release"))[0], 'systemdos') + self.assertEqual(get_os_info( + test_util.vector_path("os-release"))[1], '42') + self.assertEqual(get_systemd_os_info("/dev/null"), ("", "")) + self.assertEqual(get_os_info_ua( + test_util.vector_path("os-release")), + "SystemdOS") + with mock.patch('os.path.isfile', return_value=False): + self.assertEqual(get_systemd_os_info(), ("", "")) - def test_one_dev_one_release_version(self): - self.assertTrue(self._call("1.0.0.dev0") < self._call("1.0.0")) - self.assertTrue(self._call("1.0.0") < self._call("1.0.1.dev0")) + @mock.patch("certbot.util.subprocess.Popen") + def test_non_systemd_os_info(self, popen_mock): + from certbot.util import (get_os_info, get_python_os_info, + get_os_info_ua) + with mock.patch('os.path.isfile', return_value=False): + with mock.patch('platform.system_alias', + return_value=('NonSystemD', '42', '42')): + self.assertEqual(get_os_info()[0], 'nonsystemd') + self.assertEqual(get_os_info_ua(), + " ".join(get_python_os_info())) - def test_two_release_versions(self): - self.assertTrue(self._call("0.0.0") < self._call("0.0.1")) - self.assertTrue(self._call("0.0.0") < self._call("0.1.0")) - self.assertTrue(self._call("0.0.0") < self._call("1.0.0")) + with mock.patch('platform.system_alias', + return_value=('darwin', '', '')): + comm_mock = mock.Mock() + comm_attrs = {'communicate.return_value': + ('42.42.42', 'error')} + comm_mock.configure_mock(**comm_attrs) # pylint: disable=star-args + popen_mock.return_value = comm_mock + self.assertEqual(get_os_info()[0], 'darwin') + self.assertEqual(get_os_info()[1], '42.42.42') - def test_current_version(self): - current_version = self._call(certbot.__version__) - self.assertTrue(self._call("0.6.0") < current_version) - self.assertTrue(current_version < self._call("99.99.99")) + with mock.patch('platform.system_alias', + return_value=('linux', '', '')): + with mock.patch('platform.linux_distribution', + return_value=('', '', '')): + self.assertEqual(get_python_os_info(), ("linux", "")) + + with mock.patch('platform.linux_distribution', + return_value=('testdist', '42', '')): + self.assertEqual(get_python_os_info(), ("testdist", "42")) + + with mock.patch('platform.system_alias', + return_value=('freebsd', '9.3-RC3-p1', '')): + self.assertEqual(get_python_os_info(), ("freebsd", "9")) + + with mock.patch('platform.system_alias', + return_value=('windows', '', '')): + with mock.patch('platform.win32_ver', + return_value=('4242', '95', '2', '')): + self.assertEqual(get_python_os_info(), + ("windows", "95")) if __name__ == "__main__": diff --git a/certbot/le_util.py b/certbot/util.py similarity index 81% rename from certbot/le_util.py rename to certbot/util.py index 020050f6d..35c599737 100644 --- a/certbot/le_util.py +++ b/certbot/util.py @@ -213,9 +213,95 @@ def safely_remove(path): raise -def get_os_info(): +def get_os_info(filepath="/etc/os-release"): + """ + Get OS name and version + + :param str filepath: File path of os-release file + :returns: (os_name, os_version) + :rtype: `tuple` of `str` + """ + + if os.path.isfile(filepath): + # Systemd os-release parsing might be viable + os_name, os_version = get_systemd_os_info(filepath=filepath) + if os_name: + return (os_name, os_version) + + # Fallback to platform module + return get_python_os_info() + + +def get_os_info_ua(filepath="/etc/os-release"): + """ + Get OS name and version string for User Agent + + :param str filepath: File path of os-release file + :returns: os_ua + :rtype: `str` + """ + + if os.path.isfile(filepath): + os_ua = _get_systemd_os_release_var("PRETTY_NAME", filepath=filepath) + if not os_ua: + os_ua = _get_systemd_os_release_var("NAME", filepath=filepath) + if os_ua: + return os_ua + + # Fallback + return " ".join(get_python_os_info()) + + +def get_systemd_os_info(filepath="/etc/os-release"): + """ + Parse systemd /etc/os-release for distribution information + + :param str filepath: File path of os-release file + :returns: (os_name, os_version) + :rtype: `tuple` of `str` + """ + + os_name = _get_systemd_os_release_var("ID", filepath=filepath) + os_version = _get_systemd_os_release_var("VERSION_ID", filepath=filepath) + + return (os_name, os_version) + + +def _get_systemd_os_release_var(varname, filepath="/etc/os-release"): + """ + Get single value from systemd /etc/os-release + + :param str varname: Name of variable to fetch + :param str filepath: File path of os-release file + :returns: requested value + :rtype: `str` + """ + + var_string = varname+"=" + if not os.path.isfile(filepath): + return "" + with open(filepath, 'r') as fh: + contents = fh.readlines() + + for line in contents: + if line.strip().startswith(var_string): + # Return the value of var, normalized + return _normalize_string(line.strip()[len(var_string):]) + return "" + + +def _normalize_string(orig): + """ + Helper function for _get_systemd_os_release_var() to remove quotes + and whitespaces + """ + return orig.replace('"', '').replace("'", "").strip() + + +def get_python_os_info(): """ Get Operating System type/distribution and major version + using python platform module :returns: (os_name, os_version) :rtype: `tuple` of `str` @@ -239,8 +325,7 @@ def get_os_info(): os_ver = subprocess.Popen( ["sw_vers", "-productVersion"], stdout=subprocess.PIPE - ).communicate()[0] - os_ver = os_ver.partition(".")[0] + ).communicate()[0].rstrip('\n') elif os_type.startswith('freebsd'): # eg "9.3-RC3-p1" os_ver = os_ver.partition("-")[0] @@ -317,7 +402,7 @@ def enforce_domain_sanity(domain): domain = domain.encode('ascii').lower() except UnicodeError: error_fmt = (u"Internationalized domain names " - "are not presently supported: {0}") + "are not presently supported: {0}") if isinstance(domain, six.text_type): raise errors.ConfigurationError(error_fmt.format(domain)) else: @@ -344,7 +429,8 @@ def enforce_domain_sanity(domain): # first and last char is not "-" fqdn = re.compile("^((?!-)[A-Za-z0-9-]{1,63}(?= 2.3.3 ). (default: + False) --redirect Automatically redirect all HTTP traffic to HTTPS for the newly authenticated vhost. (default: None) --no-redirect Do not automatically redirect all HTTP traffic to @@ -148,6 +159,11 @@ security: --no-uir Do not automatically set the "Content-Security-Policy: upgrade-insecure-requests" header to every HTTP response. (default: None) + --staple-ocsp Enables OCSP Stapling. A valid OCSP response is + stapled to the certificate that the server offers + during TLS. (default: None) + --no-staple-ocsp Do not automatically enable OCSP Stapling. (default: + None) --strict-permissions Require that all configuration files are owned by the current user; only needed if your config is somewhere unsafe like /tmp/ (default: False) @@ -173,7 +189,9 @@ renew: Command to be run in a shell after attempting to obtain/renew certificates. Can be used to deploy renewed certificates, or to restart any servers that - were stopped by --pre-hook. (default: None) + were stopped by --pre-hook. This is only run if an + attempt was made to obtain/renew a certificate. + (default: None) --renew-hook RENEW_HOOK Command to be run in a shell once for each successfully renewed certificate.For this command, the @@ -182,6 +200,15 @@ renew: and keys; the shell variable $RENEWED_DOMAINS will contain a space-delimited list of renewed cert domains (default: None) + --disable-hook-validation + Ordinarily the commands specified for --pre-hook + /--post-hook/--renew-hook will be checked for + validity, to see if the programs being run are in the + $PATH, so that mistakes can be caught early, even when + the hooks aren't being run just yet. The validation is + rather simplistic and fails if you use more advanced + shell constructs, so you can use this switch to + disable it. (default: True) certonly: Options for modifying how a cert is obtained @@ -263,15 +290,6 @@ plugins: --webroot Obtain certs by placing files in a webroot directory. (default: False) -nginx: - Nginx Web Server - currently doesn't work - - --nginx-server-root NGINX_SERVER_ROOT - Nginx server root directory. (default: /etc/nginx) - --nginx-ctl NGINX_CTL - Path to the 'nginx' binary, used for 'configtest' and - retrieving nginx version number. (default: nginx) - standalone: Automatically use a temporary webserver @@ -288,6 +306,15 @@ manual: Automatically allows public IP logging. (default: False) +nginx: + Nginx Web Server - currently doesn't work + + --nginx-server-root NGINX_SERVER_ROOT + Nginx server root directory. (default: /etc/nginx) + --nginx-ctl NGINX_CTL + Path to the 'nginx' binary, used for 'configtest' and + retrieving nginx version number. (default: nginx) + webroot: Place files in webroot directory diff --git a/docs/contributing.rst b/docs/contributing.rst index 3318ec103..267d466e4 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -266,8 +266,7 @@ with the core upstream source code. An example is provided in it with any necessary API changes. .. _`setuptools entry points`: - https://pythonhosted.org/setuptools/setuptools.html#dynamic-discovery-of-services-and-plugins - + http://setuptools.readthedocs.io/en/latest/pkg_resources.html#entry-points .. _coding-style: diff --git a/docs/man/certbot.rst b/docs/man/certbot.rst index 8fb03db49..d03f3eed4 100644 --- a/docs/man/certbot.rst +++ b/docs/man/certbot.rst @@ -1 +1 @@ -.. literalinclude:: cli-help.txt +.. literalinclude:: ../cli-help.txt diff --git a/docs/using.rst b/docs/using.rst index f9af07613..4e49c3eb5 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -440,7 +440,7 @@ Operating System Packages .. code-block:: shell - sudo pacman -S letsencrypt + sudo pacman -S certbot **Debian** @@ -552,10 +552,3 @@ Beyond the methods discussed here, other methods may be possible, such as installing Certbot directly with pip from PyPI or downloading a ZIP archive from GitHub may be technically possible but are not presently recommended or supported. - - -.. rubric:: Footnotes - -.. [#venv] By using this virtualized Python environment (`virtualenv - `_) we don't pollute the main - OS space with packages from PyPI! diff --git a/letsencrypt-apache/setup.py b/letsencrypt-apache/setup.py index b94746150..09703841c 100644 --- a/letsencrypt-apache/setup.py +++ b/letsencrypt-apache/setup.py @@ -16,7 +16,7 @@ here = os.path.abspath(os.path.dirname(__file__)) readme = read_file(os.path.join(here, 'README.rst')) -version = '0.7.0.dev0' +version = '0.8.0.dev0' # This package is a simple shim around certbot-apache diff --git a/letsencrypt-auto b/letsencrypt-auto index 8c6e6c486..80f39cf59 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.6.0" +LE_AUTO_VERSION="0.8.1" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -38,17 +38,6 @@ Help for certbot itself cannot be provided until it is installed. All arguments are accepted and forwarded to the Certbot client when run." -while getopts ":hnv" arg; do - case $arg in - h) - HELP=1;; - n) - ASSUME_YES=1;; - v) - VERBOSE=1;; - esac -done - for arg in "$@" ; do case "$arg" in --debug) @@ -65,9 +54,26 @@ for arg in "$@" ; do ASSUME_YES=1;; --verbose) VERBOSE=1;; + -[!-]*) + while getopts ":hnv" short_arg $arg; do + case "$short_arg" in + h) + HELP=1;; + n) + ASSUME_YES=1;; + v) + VERBOSE=1;; + esac + done;; esac done +if [ $BASENAME = "letsencrypt-auto" ]; then + # letsencrypt-auto does not respect --help or --yes for backwards compatibility + ASSUME_YES=1 + HELP=0 +fi + # certbot-auto needs root access to bootstrap OS dependencies, and # certbot itself needs root access for almost all modes of operation # The "normal" case is that sudo is used for the steps that need root, but @@ -107,12 +113,6 @@ else SUDO= fi -if [ $BASENAME = "letsencrypt-auto" ]; then - # letsencrypt-auto does not respect --help or --yes for backwards compatibility - ASSUME_YES=1 - HELP=0 -fi - ExperimentalBootstrap() { # Arguments: Platform name, bootstrap function name if [ "$DEBUG" = 1 ]; then @@ -172,7 +172,7 @@ BootstrapDebCommon() { # distro version (#346) virtualenv= - if apt-cache show virtualenv > /dev/null 2>&1; then + if apt-cache show virtualenv > /dev/null 2>&1 && ! apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then virtualenv="virtualenv" fi @@ -425,7 +425,8 @@ BootstrapMac() { $pkgcmd augeas $pkgcmd dialog - if [ "$(which python)" = "/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python" ]; then + if [ "$(which python)" = "/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python" \ + -o "$(which python)" = "/usr/bin/python" ]; then # We want to avoid using the system Python because it requires root to use pip. # python.org, MacPorts or HomeBrew Python installations should all be OK. echo "Installing python..." @@ -435,7 +436,8 @@ BootstrapMac() { # Workaround for _dlopen not finding augeas on OS X if [ "$pkgman" = "port" ] && ! [ -e "/usr/local/lib/libaugeas.dylib" ] && [ -e "/opt/local/lib/libaugeas.dylib" ]; then echo "Applying augeas workaround" - $SUDO ln -s /opt/local/lib/libaugeas.dylib /usr/local/lib + $SUDO mkdir -p /usr/local/lib/ + $SUDO ln -s /opt/local/lib/libaugeas.dylib /usr/local/lib/ fi if ! hash pip 2>/dev/null; then @@ -451,12 +453,44 @@ BootstrapMac() { fi } +BootstrapSmartOS() { + pkgin update + pkgin -y install 'gcc49' 'py27-augeas' 'py27-virtualenv' +} + +BootstrapMageiaCommon() { + if ! $SUDO urpmi --force \ + python \ + libpython-devel \ + python-virtualenv + then + echo "Could not install Python dependencies. Aborting bootstrap!" + exit 1 + fi + + if ! $SUDO urpmi --force \ + git \ + gcc \ + cdialog \ + python-augeas \ + libopenssl-devel \ + libffi-devel \ + rootcerts + then + echo "Could not install additional dependencies. Aborting bootstrap!" + exit 1 + fi +} + # Install required OS packages: Bootstrap() { if [ -f /etc/debian_version ]; then echo "Bootstrapping dependencies for Debian-based OSes..." BootstrapDebCommon + elif [ -f /etc/mageia-release ] ; then + # Mageia has both /etc/mageia-release and /etc/redhat-release + ExperimentalBootstrap "Mageia" BootstrapMageiaCommon elif [ -f /etc/redhat-release ]; then echo "Bootstrapping dependencies for RedHat-based OSes..." BootstrapRpmCommon @@ -469,7 +503,7 @@ Bootstrap() { BootstrapArchCommon else echo "Please use pacman to install letsencrypt packages:" - echo "# pacman -S letsencrypt letsencrypt-apache" + echo "# pacman -S certbot certbot-apache" echo echo "If you would like to use the virtualenv way, please run the script again with the" echo "--debug flag." @@ -483,14 +517,17 @@ Bootstrap() { ExperimentalBootstrap "FreeBSD" BootstrapFreeBsd elif uname | grep -iq Darwin ; then ExperimentalBootstrap "Mac OS X" BootstrapMac - elif grep -iq "Amazon Linux" /etc/issue ; then + elif [ -f /etc/issue ] && grep -iq "Amazon Linux" /etc/issue ; then ExperimentalBootstrap "Amazon Linux" BootstrapRpmCommon + elif [ -f /etc/product ] && grep -q "Joyent Instance" /etc/product ; then + ExperimentalBootstrap "Joyent SmartOS Zone" BootstrapSmartOS else echo "Sorry, I don't know how to bootstrap Certbot on your operating system!" echo echo "You will need to bootstrap, configure virtualenv, and run pip install manually." echo "Please see https://letsencrypt.readthedocs.org/en/latest/contributing.html#prerequisites" echo "for more info." + exit 1 fi } @@ -523,6 +560,7 @@ if [ "$1" = "--le-auto-phase2" ]; then echo "Installing Python packages..." TEMP_DIR=$(TempDir) + trap 'rm -rf "$TEMP_DIR"' EXIT # There is no $ interpolation due to quotes on starting heredoc delimiter. # ------------------------------------------------------------------------- cat << "UNLIKELY_EOF" > "$TEMP_DIR/letsencrypt-auto-requirements.txt" @@ -703,24 +741,21 @@ zope.interface==4.1.3 \ mock==1.0.1 \ --hash=sha256:b839dd2d9c117c701430c149956918a423a9863b48b09c90e30a6013e7d2f44f \ --hash=sha256:8f83080daa249d036cbccfb8ae5cc6ff007b88d6d937521371afabe7b19badbc +letsencrypt==0.7.0 \ + --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ + --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.6.0 \ - --hash=sha256:cbe4e7a340a19725a8740ed86e30abdbe18fc22c4c6022b7a8e56642d502bcc3 \ - --hash=sha256:ec4e6009dfbd629b58473eb06bbebfd9fb2a79fc8831c149e9205bc38a98ecc6 -certbot==0.6.0 \ - --hash=sha256:a893632d228864b0a751db9f3fdd93439ed34b988ea21b64fb0f0fa2ceded6a2 \ - --hash=sha256:80b0b7dc5afeec2816ef638a61e7c628d73cd72666eebf4984be426d1c2b492d -certbot-apache==0.6.0 \ - --hash=sha256:0ab077f0913b81ed5c1b141c3a7c4c0228ef3738d8d61a93db794d9a80718d43 \ - --hash=sha256:1cfbe751209079a803758f472200816fac559f2a36fdd582d25e3ba5601423a1 -letsencrypt==0.6.0 \ - --hash=sha256:93196c7dcd57272a753e525d145c5a9987c8968c22ec954bcf83dcc9d2499a76 \ - --hash=sha256:a16d6c395f1bf5fd61a28ef83dc78f42dbecbad9d00be6236f2ad8915645c154 -letsencrypt-apache==0.6.0 \ - --hash=sha256:02fadc52a0796e53978c508beec9c53e1fc047660240832b9bde5d53ab3a1379 \ - --hash=sha256:1c5522d94d7750bdb9bfa6201d2c263e914f662c9d0079e673167233cf4364f1 +acme==0.8.1 \ + --hash=sha256:ccd7883772efbf933f91713b8241455993834f3620c8fbd459d9ed5e50bbaaca \ + --hash=sha256:d3ea4acf280bf6253ad7d641cb0970f230a19805acfed809e7a8ddcf62157d9f +certbot==0.8.1 \ + --hash=sha256:89805d9f70249ae859ec4d7a99c00b4bb7083ca90cd12d4d202b76dfc284f7c5 \ + --hash=sha256:6ca8df3d310ced6687d38aac17c0fb8c1b2ec7a3bea156a254e4cc2a1c132771 +certbot-apache==0.8.1 \ + --hash=sha256:c9e3fdc15e65589c2e39eb0e6b1f61f0c0a1db3c17b00bb337f0ff636cc61cb3 \ + --hash=sha256:0faf2879884d3b7a58b071902fba37d4b8b58a50e2c3b8ac262c0a74134045ed UNLIKELY_EOF # ------------------------------------------------------------------------- @@ -880,7 +915,6 @@ UNLIKELY_EOF PIP_OUT=`"$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1` PIP_STATUS=$? set -e - rm -rf "$TEMP_DIR" if [ "$PIP_STATUS" != 0 ]; then # Report error. (Otherwise, be quiet.) echo "Had a problem while installing Python packages:" @@ -890,14 +924,16 @@ UNLIKELY_EOF fi echo "Installation succeeded." fi - echo "Requesting root privileges to run certbot..." + if [ -n "$SUDO" ]; then + # SUDO is su wrapper or sudo + echo "Requesting root privileges to run certbot..." + echo " $VENV_BIN/letsencrypt" "$@" + fi if [ -z "$SUDO_ENV" ] ; then # SUDO is su wrapper / noop - echo " " $SUDO "$VENV_BIN/letsencrypt" "$@" $SUDO "$VENV_BIN/letsencrypt" "$@" else # sudo - echo " " $SUDO "$SUDO_ENV" "$VENV_BIN/letsencrypt" "$@" $SUDO "$SUDO_ENV" "$VENV_BIN/letsencrypt" "$@" fi @@ -923,8 +959,8 @@ else fi if [ "$NO_SELF_UPGRADE" != 1 ]; then - echo "Checking for new version..." TEMP_DIR=$(TempDir) + trap 'rm -rf "$TEMP_DIR"' EXIT # --------------------------------------------------------------------------- cat << "UNLIKELY_EOF" > "$TEMP_DIR/fetch.py" """Do downloading and JSON parsing without additional dependencies. :: @@ -997,7 +1033,7 @@ def latest_stable_version(get): """Return the latest stable release of letsencrypt.""" metadata = loads(get( environ.get('LE_AUTO_JSON_URL', - 'https://pypi.python.org/pypi/letsencrypt/json'))) + 'https://pypi.python.org/pypi/certbot/json'))) # metadata['info']['version'] actually returns the latest of any kind of # release release, contrary to https://wiki.python.org/moin/PyPIJSON. # The regex is a sufficient regex for picking out prereleases for most @@ -1016,7 +1052,7 @@ def verified_new_le_auto(get, tag, temp_dir): """ le_auto_dir = environ.get( 'LE_AUTO_DIR_TEMPLATE', - 'https://raw.githubusercontent.com/letsencrypt/letsencrypt/%s/' + 'https://raw.githubusercontent.com/certbot/certbot/%s/' 'letsencrypt-auto-source/') % tag write(get(le_auto_dir + 'letsencrypt-auto'), temp_dir, 'letsencrypt-auto') write(get(le_auto_dir + 'letsencrypt-auto.sig'), temp_dir, 'letsencrypt-auto.sig') @@ -1079,8 +1115,6 @@ UNLIKELY_EOF # filesystems is non-atomic, doing `rm dest, cp src dest, rm src`, but the # cp is unlikely to fail (esp. under sudo) if the rm doesn't. $SUDO mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0" - # TODO: Clean up temp dir safely, even if it has quotes in its path. - rm -rf "$TEMP_DIR" fi # A newer version is available. fi # Self-upgrading is allowed. diff --git a/letsencrypt-auto-source/certbot-auto.asc b/letsencrypt-auto-source/certbot-auto.asc index 8b4f34c70..5bb725c96 100644 --- a/letsencrypt-auto-source/certbot-auto.asc +++ b/letsencrypt-auto-source/certbot-auto.asc @@ -1,11 +1,11 @@ -----BEGIN PGP SIGNATURE----- Version: GnuPG v1 -iQEcBAABAgAGBQJXM9ZDAAoJEE0XyZXNl3XyzGkH/2KeR0jYxXKlvwfCkxU6hSC0 -eXcxZVQk59hCSvkNGE6Mj6rwQcyjSqmRp14MaJpq7NZADN6F+HWb6VB/Wq6moMQs -PJtthqwhF767Qg+Py9Hp6XmlKscjXB6AKCVxq5TBwEIOTtj0rhQRLF9/+GW6jFuf -kT6aUcDWNjOyWWUtp9vOVprDtegrltp0/2DNitlvPu263pKC+7I3GyLTq4fKP4EE -auZSAhFry9SNR3Usf2wD3kzhvLSrT3h9Yh5oA04oaX9H6e86EHwt6RJJRHpg8s6b -e0CBIIuaRJEmdiMUWlV/gAfH6M2PbG1wtJdxc0ThNEoWAjTsopr61BoHJ3cpCy4= -=+e7/ +iQEcBAABAgAGBQJXYJmBAAoJEE0XyZXNl3XyyIMH/jtYFb7rl5XXN8hjlKuK5frq +z7/jdK7fvI+mtYJ4i2Cy3yMz8T4wscXGkhxNtipbATWlpevPfjYzm4ZGC25coFZx +fDX44w0hBBgel7EISXGR1ABXb2rj24TZxIYXwaeClylsK9n5CxcWBocn8tDlfr8t +7VQUJEL3l1IlrnKnvpoL4Eq11sxlIPtitDPJ5c98ZM1293ZbWzIqyZKoXLIUkKHg +pkaa80j/QMmFumxzXFenU91JusLdeoblvjjg+kzjGonjslAYIuH4wEEjz2VJuUYe +P2+2ZyW4eLA6rRZhZ3CMtV79HzTPTWiELCYbXezb+yXJJEqzCYtIXkmbNQ3jUEY= +=86lB -----END PGP SIGNATURE----- diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index fee63c2f5..80b81c898 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.7.0.dev0" +LE_AUTO_VERSION="0.9.0.dev0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -172,7 +172,7 @@ BootstrapDebCommon() { # distro version (#346) virtualenv= - if apt-cache show virtualenv > /dev/null 2>&1; then + if apt-cache show virtualenv > /dev/null 2>&1 && ! apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then virtualenv="virtualenv" fi @@ -458,12 +458,39 @@ BootstrapSmartOS() { pkgin -y install 'gcc49' 'py27-augeas' 'py27-virtualenv' } +BootstrapMageiaCommon() { + if ! $SUDO urpmi --force \ + python \ + libpython-devel \ + python-virtualenv + then + echo "Could not install Python dependencies. Aborting bootstrap!" + exit 1 + fi + + if ! $SUDO urpmi --force \ + git \ + gcc \ + cdialog \ + python-augeas \ + libopenssl-devel \ + libffi-devel \ + rootcerts + then + echo "Could not install additional dependencies. Aborting bootstrap!" + exit 1 + fi +} + # Install required OS packages: Bootstrap() { if [ -f /etc/debian_version ]; then echo "Bootstrapping dependencies for Debian-based OSes..." BootstrapDebCommon + elif [ -f /etc/mageia-release ] ; then + # Mageia has both /etc/mageia-release and /etc/redhat-release + ExperimentalBootstrap "Mageia" BootstrapMageiaCommon elif [ -f /etc/redhat-release ]; then echo "Bootstrapping dependencies for RedHat-based OSes..." BootstrapRpmCommon @@ -476,7 +503,7 @@ Bootstrap() { BootstrapArchCommon else echo "Please use pacman to install letsencrypt packages:" - echo "# pacman -S letsencrypt letsencrypt-apache" + echo "# pacman -S certbot certbot-apache" echo echo "If you would like to use the virtualenv way, please run the script again with the" echo "--debug flag." @@ -500,6 +527,7 @@ Bootstrap() { echo "You will need to bootstrap, configure virtualenv, and run pip install manually." echo "Please see https://letsencrypt.readthedocs.org/en/latest/contributing.html#prerequisites" echo "for more info." + exit 1 fi } @@ -713,24 +741,21 @@ zope.interface==4.1.3 \ mock==1.0.1 \ --hash=sha256:b839dd2d9c117c701430c149956918a423a9863b48b09c90e30a6013e7d2f44f \ --hash=sha256:8f83080daa249d036cbccfb8ae5cc6ff007b88d6d937521371afabe7b19badbc +letsencrypt==0.7.0 \ + --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ + --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.6.0 \ - --hash=sha256:cbe4e7a340a19725a8740ed86e30abdbe18fc22c4c6022b7a8e56642d502bcc3 \ - --hash=sha256:ec4e6009dfbd629b58473eb06bbebfd9fb2a79fc8831c149e9205bc38a98ecc6 -certbot==0.6.0 \ - --hash=sha256:a893632d228864b0a751db9f3fdd93439ed34b988ea21b64fb0f0fa2ceded6a2 \ - --hash=sha256:80b0b7dc5afeec2816ef638a61e7c628d73cd72666eebf4984be426d1c2b492d -certbot-apache==0.6.0 \ - --hash=sha256:0ab077f0913b81ed5c1b141c3a7c4c0228ef3738d8d61a93db794d9a80718d43 \ - --hash=sha256:1cfbe751209079a803758f472200816fac559f2a36fdd582d25e3ba5601423a1 -letsencrypt==0.6.0 \ - --hash=sha256:93196c7dcd57272a753e525d145c5a9987c8968c22ec954bcf83dcc9d2499a76 \ - --hash=sha256:a16d6c395f1bf5fd61a28ef83dc78f42dbecbad9d00be6236f2ad8915645c154 -letsencrypt-apache==0.6.0 \ - --hash=sha256:02fadc52a0796e53978c508beec9c53e1fc047660240832b9bde5d53ab3a1379 \ - --hash=sha256:1c5522d94d7750bdb9bfa6201d2c263e914f662c9d0079e673167233cf4364f1 +acme==0.8.1 \ + --hash=sha256:ccd7883772efbf933f91713b8241455993834f3620c8fbd459d9ed5e50bbaaca \ + --hash=sha256:d3ea4acf280bf6253ad7d641cb0970f230a19805acfed809e7a8ddcf62157d9f +certbot==0.8.1 \ + --hash=sha256:89805d9f70249ae859ec4d7a99c00b4bb7083ca90cd12d4d202b76dfc284f7c5 \ + --hash=sha256:6ca8df3d310ced6687d38aac17c0fb8c1b2ec7a3bea156a254e4cc2a1c132771 +certbot-apache==0.8.1 \ + --hash=sha256:c9e3fdc15e65589c2e39eb0e6b1f61f0c0a1db3c17b00bb337f0ff636cc61cb3 \ + --hash=sha256:0faf2879884d3b7a58b071902fba37d4b8b58a50e2c3b8ac262c0a74134045ed UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/letsencrypt-auto-source/letsencrypt-auto.sig b/letsencrypt-auto-source/letsencrypt-auto.sig index cb360f89e..e3da0e0a3 100644 --- a/letsencrypt-auto-source/letsencrypt-auto.sig +++ b/letsencrypt-auto-source/letsencrypt-auto.sig @@ -1,3 +1,2 @@ -.h.jqޫ{iw<3!xԞJXuL gJ9ڭ`GA5 v"a(_?8qx(2%=d~ -cy -y, Ss|ڀ*C2p5 G~h!҅KgVlz JalJ$/u'x~͑(Ŧ!_xx묕V$-!΁ \ No newline at end of file diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 43d8bc7e1..ecc9a042a 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -155,12 +155,16 @@ DeterminePythonVersion() { {{ bootstrappers/free_bsd.sh }} {{ bootstrappers/mac.sh }} {{ bootstrappers/smartos.sh }} +{{ bootstrappers/mageia_common.sh }} # Install required OS packages: Bootstrap() { if [ -f /etc/debian_version ]; then echo "Bootstrapping dependencies for Debian-based OSes..." BootstrapDebCommon + elif [ -f /etc/mageia-release ] ; then + # Mageia has both /etc/mageia-release and /etc/redhat-release + ExperimentalBootstrap "Mageia" BootstrapMageiaCommon elif [ -f /etc/redhat-release ]; then echo "Bootstrapping dependencies for RedHat-based OSes..." BootstrapRpmCommon @@ -173,7 +177,7 @@ Bootstrap() { BootstrapArchCommon else echo "Please use pacman to install letsencrypt packages:" - echo "# pacman -S letsencrypt letsencrypt-apache" + echo "# pacman -S certbot certbot-apache" echo echo "If you would like to use the virtualenv way, please run the script again with the" echo "--debug flag." @@ -197,6 +201,7 @@ Bootstrap() { echo "You will need to bootstrap, configure virtualenv, and run pip install manually." echo "Please see https://letsencrypt.readthedocs.org/en/latest/contributing.html#prerequisites" echo "for more info." + exit 1 fi } diff --git a/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh index bfbcfa31d..8eb7e16ee 100644 --- a/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh @@ -23,7 +23,7 @@ BootstrapDebCommon() { # distro version (#346) virtualenv= - if apt-cache show virtualenv > /dev/null 2>&1; then + if apt-cache show virtualenv > /dev/null 2>&1 && ! apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then virtualenv="virtualenv" fi diff --git a/letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh new file mode 100644 index 000000000..d6651574a --- /dev/null +++ b/letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh @@ -0,0 +1,23 @@ +BootstrapMageiaCommon() { + if ! $SUDO urpmi --force \ + python \ + libpython-devel \ + python-virtualenv + then + echo "Could not install Python dependencies. Aborting bootstrap!" + exit 1 + fi + + if ! $SUDO urpmi --force \ + git \ + gcc \ + cdialog \ + python-augeas \ + libopenssl-devel \ + libffi-devel \ + rootcerts + then + echo "Could not install additional dependencies. Aborting bootstrap!" + exit 1 + fi +} diff --git a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt index f4ceae536..1291b2c96 100644 --- a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt +++ b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt @@ -175,21 +175,18 @@ zope.interface==4.1.3 \ mock==1.0.1 \ --hash=sha256:b839dd2d9c117c701430c149956918a423a9863b48b09c90e30a6013e7d2f44f \ --hash=sha256:8f83080daa249d036cbccfb8ae5cc6ff007b88d6d937521371afabe7b19badbc +letsencrypt==0.7.0 \ + --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ + --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.6.0 \ - --hash=sha256:cbe4e7a340a19725a8740ed86e30abdbe18fc22c4c6022b7a8e56642d502bcc3 \ - --hash=sha256:ec4e6009dfbd629b58473eb06bbebfd9fb2a79fc8831c149e9205bc38a98ecc6 -certbot==0.6.0 \ - --hash=sha256:a893632d228864b0a751db9f3fdd93439ed34b988ea21b64fb0f0fa2ceded6a2 \ - --hash=sha256:80b0b7dc5afeec2816ef638a61e7c628d73cd72666eebf4984be426d1c2b492d -certbot-apache==0.6.0 \ - --hash=sha256:0ab077f0913b81ed5c1b141c3a7c4c0228ef3738d8d61a93db794d9a80718d43 \ - --hash=sha256:1cfbe751209079a803758f472200816fac559f2a36fdd582d25e3ba5601423a1 -letsencrypt==0.6.0 \ - --hash=sha256:93196c7dcd57272a753e525d145c5a9987c8968c22ec954bcf83dcc9d2499a76 \ - --hash=sha256:a16d6c395f1bf5fd61a28ef83dc78f42dbecbad9d00be6236f2ad8915645c154 -letsencrypt-apache==0.6.0 \ - --hash=sha256:02fadc52a0796e53978c508beec9c53e1fc047660240832b9bde5d53ab3a1379 \ - --hash=sha256:1c5522d94d7750bdb9bfa6201d2c263e914f662c9d0079e673167233cf4364f1 +acme==0.8.1 \ + --hash=sha256:ccd7883772efbf933f91713b8241455993834f3620c8fbd459d9ed5e50bbaaca \ + --hash=sha256:d3ea4acf280bf6253ad7d641cb0970f230a19805acfed809e7a8ddcf62157d9f +certbot==0.8.1 \ + --hash=sha256:89805d9f70249ae859ec4d7a99c00b4bb7083ca90cd12d4d202b76dfc284f7c5 \ + --hash=sha256:6ca8df3d310ced6687d38aac17c0fb8c1b2ec7a3bea156a254e4cc2a1c132771 +certbot-apache==0.8.1 \ + --hash=sha256:c9e3fdc15e65589c2e39eb0e6b1f61f0c0a1db3c17b00bb337f0ff636cc61cb3 \ + --hash=sha256:0faf2879884d3b7a58b071902fba37d4b8b58a50e2c3b8ac262c0a74134045ed diff --git a/letsencrypt-auto-source/tests/auto_test.py b/letsencrypt-auto-source/tests/auto_test.py index 7e131f4cf..56023bc6f 100644 --- a/letsencrypt-auto-source/tests/auto_test.py +++ b/letsencrypt-auto-source/tests/auto_test.py @@ -11,7 +11,7 @@ from shutil import copy, rmtree import socket import ssl from stat import S_IRUSR, S_IXUSR -from subprocess import CalledProcessError, check_output, Popen, PIPE +from subprocess import CalledProcessError, Popen, PIPE import sys from tempfile import mkdtemp from threading import Thread @@ -146,7 +146,9 @@ def out_and_err(command, input=None, shell=False, env=None): out, err = process.communicate(input=input) status = process.poll() # same as in check_output(), though wait() sounds better if status: - raise CalledProcessError(status, command, output=out) + error = CalledProcessError(status, command) + error.output = out + raise error return out, err diff --git a/letsencrypt-nginx/setup.py b/letsencrypt-nginx/setup.py index 95ffd6cd8..25db12a47 100644 --- a/letsencrypt-nginx/setup.py +++ b/letsencrypt-nginx/setup.py @@ -16,7 +16,7 @@ here = os.path.abspath(os.path.dirname(__file__)) readme = read_file(os.path.join(here, 'README.rst')) -version = '0.7.0.dev0' +version = '0.8.0.dev0' # This package is a simple shim around certbot-nginx diff --git a/letsencrypt/setup.py b/letsencrypt/setup.py index 7c974ea9b..4541e85fe 100644 --- a/letsencrypt/setup.py +++ b/letsencrypt/setup.py @@ -20,7 +20,7 @@ readme = read_file(os.path.join(here, 'README.rst')) install_requires = ['certbot'] -version = '0.7.0.dev0' +version = '0.8.0.dev0' setup( diff --git a/letsencrypt/tests/testdata/os-release b/letsencrypt/tests/testdata/os-release new file mode 100644 index 000000000..b7c3ceb1b --- /dev/null +++ b/letsencrypt/tests/testdata/os-release @@ -0,0 +1,8 @@ +NAME="SystemdOS" +VERSION="42.42.42 LTS, Unreal" +ID=systemdos +ID_LIKE=debian +PRETTY_NAME="SystemdOS 42.42.42 Unreal" +VERSION_ID="42" +HOME_URL="http://www.example.com/" +SUPPORT_URL="http://help.example.com/" diff --git a/pep8.travis.sh b/pep8.travis.sh index c13547a78..cadea8489 100755 --- a/pep8.travis.sh +++ b/pep8.travis.sh @@ -2,16 +2,4 @@ set -e # Fail fast -# PEP8 is not ignored in ACME pep8 --config=acme/.pep8 acme - -pep8 \ - setup.py \ - certbot \ - certbot-apache \ - certbot-nginx \ - certbot-compatibility-test \ - letshelp-certbot \ - || echo "PEP8 checking failed, but it's ignored in Travis" - -# echo exits with 0 diff --git a/setup.py b/setup.py index 59da23de4..53fde5282 100644 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ install_requires = [ 'configobj', 'cryptography>=0.7', # load_pem_x509_certificate 'parsedatetime>=1.3', # Calendar.parseDT - 'psutil>=2.1.0', # net_connections introduced in 2.1.0 + 'psutil>=2.2.1', # 2.1.0 for net_connections and 2.2.1 resolves #1080 'PyOpenSSL', 'pyrfc3339', 'python2-pythondialog>=3.2.2rc1', # Debian squeeze support, cf. #280 @@ -71,7 +71,6 @@ dev_extras = [ 'nose', 'nosexcover', 'pep8', - 'pkginfo<=1.2.1', 'pylint==1.4.2', # upstream #248 'tox', 'twine', diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index 323ea004b..ab8fde5f6 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -84,6 +84,24 @@ if [ "$size1" -lt 3000 ] || [ "$size2" -lt 3000 ] || [ "$size3" -gt 1800 ] ; the exit 1 fi +# ECDSA +openssl ecparam -genkey -name secp384r1 -out "${root}/privkey-p384.pem" +SAN="DNS:ecdsa.le.wtf" openssl req -new -sha256 \ + -config "${OPENSSL_CNF:-openssl.cnf}" \ + -key "${root}/privkey-p384.pem" \ + -subj "/" \ + -reqexts san \ + -outform der \ + -out "${root}/csr-p384.der" +common auth --csr "${root}/csr-p384.der" \ + --cert-path "${root}/csr/cert-p384.pem" \ + --chain-path "${root}/csr/chain-p384.pem" +openssl x509 -in "${root}/csr/cert-p384.pem" -text | grep 'ASN1 OID: secp384r1' + +# OCSP Must Staple +common auth --must-staple --domains "must-staple.le.wtf" +openssl x509 -in "${root}/conf/live/must-staple.le.wtf/cert.pem" -text | grep '1.3.6.1.5.5.7.1.24' + # revoke by account key common revoke --cert-path "$root/conf/live/le.wtf/cert.pem" # revoke renewed diff --git a/tools/_venv_common.sh b/tools/_venv_common.sh index dc6ca3dd2..a121af82d 100755 --- a/tools/_venv_common.sh +++ b/tools/_venv_common.sh @@ -18,8 +18,7 @@ virtualenv --no-site-packages $VENV_NAME $VENV_ARGS # Separately install setuptools and pip to make sure following # invocations use latest pip install -U setuptools -# --force-reinstall used to fix broken pip installation on some systems -pip install --force-reinstall -U pip +pip install -U pip pip install "$@" set +x diff --git a/tools/release.sh b/tools/release.sh index abaad09ff..c883e3d61 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -45,7 +45,7 @@ export GPG_TTY=$(tty) PORT=${PORT:-1234} # subpackages to be released -SUBPKGS=${SUBPKGS:-"acme certbot-apache certbot-nginx letshelp-certbot letsencrypt letsencrypt-apache letsencrypt-nginx letshelp-letsencrypt"} +SUBPKGS=${SUBPKGS:-"acme certbot-apache certbot-nginx"} subpkgs_modules="$(echo $SUBPKGS | sed s/-/_/g)" # certbot_compatibility_test is not packaged because: # - it is not meant to be used by anyone else than Certbot devs @@ -162,15 +162,15 @@ for module in certbot $subpkgs_modules ; do echo testing $module nosetests $module done -deactivate # pin pip hashes of the things we just built -for pkg in acme certbot certbot-apache letsencrypt letsencrypt-apache ; do +for pkg in acme certbot certbot-apache ; do echo $pkg==$version \\ pip hash dist."$version/$pkg"/*.{whl,gz} | grep "^--hash" | python2 -c 'from sys import stdin; input = stdin.read(); print " ", input.replace("\n--hash", " \\\n --hash"),' done > /tmp/hashes.$$ +deactivate -if ! wc -l /tmp/hashes.$$ | grep -qE "^\s*15 " ; then +if ! wc -l /tmp/hashes.$$ | grep -qE "^\s*9 " ; then echo Unexpected pip hash output exit 1 fi diff --git a/tox.ini b/tox.ini index 5c88dfd21..cb625ba8d 100644 --- a/tox.ini +++ b/tox.ini @@ -64,14 +64,14 @@ basepython = python2.7 # duplicate code checking; if one of the commands fails, others will # continue, but tox return code will reflect previous error commands = - pip install -e acme[dev] -e .[dev] -e certbot-apache -e certbot-nginx -e certbot-compatibility-test -e letshelp-certbot + pip install -q -e acme[dev] -e .[dev] -e certbot-apache -e certbot-nginx -e certbot-compatibility-test -e letshelp-certbot ./pep8.travis.sh - pylint --rcfile=.pylintrc certbot - pylint --rcfile=acme/.pylintrc acme/acme - pylint --rcfile=.pylintrc certbot-apache/certbot_apache - pylint --rcfile=.pylintrc certbot-nginx/certbot_nginx - pylint --rcfile=.pylintrc certbot-compatibility-test/certbot_compatibility_test - pylint --rcfile=.pylintrc letshelp-certbot/letshelp_certbot + pylint --reports=n --rcfile=.pylintrc certbot + pylint --reports=n --rcfile=acme/.pylintrc acme/acme + pylint --reports=n --rcfile=.pylintrc certbot-apache/certbot_apache + pylint --reports=n --rcfile=.pylintrc certbot-nginx/certbot_nginx + pylint --reports=n --rcfile=.pylintrc certbot-compatibility-test/certbot_compatibility_test + pylint --reports=n --rcfile=.pylintrc letshelp-certbot/letshelp_certbot [testenv:apacheconftest] #basepython = python2.7