From e4102e985b960bc4c02d1499e9280290d6541a4a Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 21 Nov 2014 19:17:11 +0100 Subject: [PATCH 01/21] Get rid of abc --- letsencrypt/client/augeas_configurator.py | 72 +------------ letsencrypt/client/configurator.py | 123 +++++++--------------- letsencrypt/client/display.py | 38 ++++--- 3 files changed, 63 insertions(+), 170 deletions(-) diff --git a/letsencrypt/client/augeas_configurator.py b/letsencrypt/client/augeas_configurator.py index 040fce388..6e4d54fb1 100644 --- a/letsencrypt/client/augeas_configurator.py +++ b/letsencrypt/client/augeas_configurator.py @@ -1,4 +1,4 @@ -import abc, os, sys, shutil, time +import os, sys, shutil, time from letsencrypt.client.configurator import Configurator import augeas @@ -16,54 +16,6 @@ class AugeasConfigurator(Configurator): self.aug = augeas.Augeas(flags=augeas.Augeas.NONE) self.save_notes = "" - def deploy_cert(self, vhost, cert, key , cert_chain=None): - raise Exception("Error: augeas Configurator class") - - - def choose_virtual_host(self, name): - """ - Chooses a virtual host based on a given domain name - """ - raise Exception("Error: augeas Configurator class") - - def get_all_names(self): - """ - Return all names found in the Configuration - """ - raise Exception("Error: augeas Configurator class") - - def enable_redirect(self, ssl_vhost): - """ - Makes all traffic redirect to the given ssl_vhost - ie. port 80 => 443 - """ - raise Exception("Error: augeas Configurator class") - - def enable_hsts(self, ssl_vhost): - """ - Enable HSTS on the given ssl_vhost - """ - raise Exception("Error: augeas Configurator class") - - def enable_ocsp_stapling(self, ssl_vhost): - """ - Enable OCSP stapling on given ssl_vhost - """ - raise Exception("Error: augeas Configurator class") - - def get_all_certs_keys(self): - """ - Retrieve all certs and keys set in configuration - return list of tuples with form [(cert, key, path)] - """ - raise Exception("Error: augeas Configurator class") - - def enable_site(self, vhost): - """ - Enable the site at the given vhost - """ - raise Exception("Error: augeas Configurator class") - def check_parsing_errors(self, lens): """ This function checks to see if Augeas was unable to parse any of the @@ -400,25 +352,3 @@ class AugeasConfigurator(Configurator): sys.exit(41) return True - - - - def config_test(self): - """ - Make sure the configuration is valid - """ - raise Exception("Error: augeas Configurator class") - - def restart(self): - """ - Restart or refresh the server content - """ - raise Exception("Error: augeas Configurator class") - - def perform(self, challenge): - """ Perform the challenge """ - raise Exception("Error: augeas Configurator class") - - def cleanup(self): - """ Clean up any challenge configurations """ - raise Exception("Error: augeas Configurator class") diff --git a/letsencrypt/client/configurator.py b/letsencrypt/client/configurator.py index 0219b1c6a..ba6a8f5bd 100644 --- a/letsencrypt/client/configurator.py +++ b/letsencrypt/client/configurator.py @@ -1,83 +1,55 @@ -# Note: abc requires python 2.6 so we may remove this before -# launch. This should help in the creation of other configurators while -# we develop though +"""Configurator.""" -import abc class Configurator(object): - """ + """Generic Let's Encrypt configurator. + Class represents all possible webservers and configuration editors This includes the generic webserver which wont have configuration files at all, but instead create a new process to handle the DVSNI and other challenges. """ - __metaclass__ = abc.ABCMeta - def __init__(self): - return - - @abc.abstractmethod def deploy_cert(self, vhost, cert, key , cert_chain=None): - return + raise NotImplementedError() - @abc.abstractmethod def choose_virtual_host(self, name): - """ - Chooses a virtual host based on a given domain name - """ - return + """Chooses a virtual host based on a given domain name.""" + raise NotImplementedError() - @abc.abstractmethod def get_all_names(self): - """ - Returns all names found in the Configuration - """ - return + """Returns all names found in the configuration.""" + raise NotImplementedError() - @abc.abstractmethod def enable_redirect(self, ssl_vhost): - """ - Makes all traffic redirect to the given ssl_vhost - ie. port 80 => 443 - """ - return + """Makes all traffic redirect to the given ssl_vhost (port 80 => 443).""" + raise NotImplementedError() - @abc.abstractmethod def enable_hsts(self, ssl_vhost): - """ - Enable HSTS on the given ssl_vhost - """ - return + """Enable HSTS on the given ssl_vhost.""" + raise NotImplementedError() - @abc.abstractmethod def enable_ocsp_stapling(self, ssl_vhost): - """ - Enable OCSP stapling on given ssl_vhost - """ - return + """Enable OCSP stapling on given ssl_vhost.""" + raise NotImplementedError() - @abc.abstractmethod def get_all_certs_keys(self): - """ - Retrieve all certs and keys set in configuration + """Retrieve all certs and keys set in configuration. + returns: list of tuples with form [(cert, key, path)] """ - return + raise NotImplementedError() - @abc.abstractmethod def enable_site(self, vhost): - """ - Enable the site at the given vhost - """ - return + """Enable the site at the given vhost.""" + raise NotImplementedError() - @abc.abstractmethod def save(self, title=None, temporary=False): - """ - Saves all changes to the configuration files, both - title and temporary are needed because a save may be - intended to be permanent, but the save is not ready - to be a full checkpoint + """Saves all changes to the configuration files. + + Both title and temporary are needed because a save may be + intended to be permanent, but the save is not ready to be a full + checkpoint title: string - The title of the save. If a title is given, the configuration will be saved as a new checkpoint @@ -86,49 +58,32 @@ class Configurator(object): temporary: boolean - Indicates whether the changes made will be quickly reversed in the future (challenges) """ - return + raise NotImplementedError() - @abc.abstractmethod def revert_challenge_config(self): - """ - This function should reload the users original configuration files - """ - return + """Reload the users original configuration files.""" + raise NotImplementedError() - @abc.abstractmethod def rollback_checkpoints(self, rollback = 1): - """ - Revert `rollback` number of configuration checkpoints - """ - return + """Revert `rollback` number of configuration checkpoints.""" + raise NotImplementedError() - @abc.abstractmethod def display_checkpoints(self): - """ - Display the saved configuration checkpoints - """ - return + """Display the saved configuration checkpoints.""" + raise NotImplementedError() - @abc.abstractmethod def config_test(self): - """ - Make sure the configuration is valid - """ - return + """Make sure the configuration is valid.""" + raise NotImplementedError() - @abc.abstractmethod def restart(self): - """ - Restart or refresh the server content - """ - return + """Restart or refresh the server content.""" + raise NotImplementedError() - @abc.abstractmethod def perform(self, chall_type, tup): - """ Perform the given challenge""" - return + """Perform the given challenge""" + raise NotImplementedError() - @abc.abstractmethod def cleanup(self): - """ Cleanup configuration changes from challenge """ - return + """Cleanup configuration changes from challenge.""" + raise NotImplementedError() diff --git a/letsencrypt/client/display.py b/letsencrypt/client/display.py index 00d992ab0..c0c430fab 100644 --- a/letsencrypt/client/display.py +++ b/letsencrypt/client/display.py @@ -1,5 +1,6 @@ import dialog + WIDTH = 72 HEIGHT = 20 @@ -13,18 +14,34 @@ class SingletonD(object): class Display(SingletonD): + """Generic display.""" + def generic_notification(self, message, width = WIDTH, height = HEIGHT): - raise Exception("Error no display defined") + raise NotImplementedError() + def generic_menu(self, message, choices, input_text = "", width = WIDTH, height = HEIGHT): - raise Exception("Error no display defined") + raise NotImplementedError() + def generic_input(self, message): - raise Exception("Error no display defined") + raise NotImplementedError() + def generic_yesno(self, message, yes_label = "Yes", no_label = "No"): - raise Exception("Error no display defined") + raise NotImplementedError() + def filter_names(self, names): - raise Exception("Error no display defined") + raise NotImplementedError() + def success_installation(self, domains): - raise Exception("Error no display defined") + raise NotImplementedError() + + def display_certs(self, certs): + raise NotImplementedError() + + def confirm_revocation(self, cert): + raise NotImplementedError() + + def more_info_cert(self, cert): + raise NotImplementedError() def gen_https_names(self, domains): """ @@ -43,12 +60,6 @@ class Display(SingletonD): return result - def display_certs(self, certs): - raise Exception("Error no display define") - - def confirm_revocation(self, cert): - raise Exception("Error no display defined") - def cert_info_frame(self, cert): text = "-" * (WIDTH - 4) + "\n" text += self.cert_info_string(cert) @@ -67,9 +78,6 @@ class Display(SingletonD): text += "Installed: %s\n" % cert["installed"] return text - def more_info_cert(self, cert): - raise Exception("Error no display defined") - class NcursesDisplay(Display): import dialog From aafcaa4ef8350a2950097e329e8770b4585fd661 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 21 Nov 2014 19:36:33 +0100 Subject: [PATCH 02/21] Remove unnecessary executable bits --- letsencrypt/client/acme.py | 0 letsencrypt/client/client.py | 0 letsencrypt/client/setup.sh | 0 3 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 letsencrypt/client/acme.py mode change 100755 => 100644 letsencrypt/client/client.py mode change 100755 => 100644 letsencrypt/client/setup.sh diff --git a/letsencrypt/client/acme.py b/letsencrypt/client/acme.py old mode 100755 new mode 100644 diff --git a/letsencrypt/client/client.py b/letsencrypt/client/client.py old mode 100755 new mode 100644 diff --git a/letsencrypt/client/setup.sh b/letsencrypt/client/setup.sh old mode 100755 new mode 100644 From de53f8e940ec4798b96386581829d622bb06c26d Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 21 Nov 2014 21:06:13 +0100 Subject: [PATCH 03/21] Clean up imports --- letsencrypt/client/acme.py | 11 ++- letsencrypt/client/apache_configurator.py | 81 ++++++++--------- letsencrypt/client/augeas_configurator.py | 61 +++++++------ letsencrypt/client/challenge.py | 2 +- letsencrypt/client/client.py | 86 ++++++++++--------- letsencrypt/client/crypto_util.py | 55 ++++++------ letsencrypt/client/display.py | 6 +- letsencrypt/client/interactive_challenge.py | 8 +- letsencrypt/client/le_util.py | 15 ++-- letsencrypt/client/logger.py | 26 +++--- letsencrypt/client/nginx_configurator.py | 17 ++-- .../client/recovery_contact_challenge.py | 12 +-- .../client/recovery_token_challenge.py | 7 +- letsencrypt/scripts/main.py | 13 ++- 14 files changed, 203 insertions(+), 197 deletions(-) diff --git a/letsencrypt/client/acme.py b/letsencrypt/client/acme.py index 633b8c2b6..e8ffeeb74 100644 --- a/letsencrypt/client/acme.py +++ b/letsencrypt/client/acme.py @@ -1,11 +1,10 @@ -#!/usr/bin/env python - -# acme.py -# validate JSON objects as ACME protocol messages - -import json, jsonschema +"""Validate JSON objects as ACME protocol messages.""" +import json import pkg_resources +import jsonschema + + schemata = {schema: json.load(open(pkg_resources.resource_filename( __name__, "schemata/%s.json" % schema))) for schema in [ "authorization", "authorizationRequest", "certificate", "certificateRequest", diff --git a/letsencrypt/client/apache_configurator.py b/letsencrypt/client/apache_configurator.py index 068c6c099..1a3cd73b7 100644 --- a/letsencrypt/client/apache_configurator.py +++ b/letsencrypt/client/apache_configurator.py @@ -1,30 +1,23 @@ -import augeas -import subprocess -import re +import hashlib import os -import sys -import socket -import time +import pkg_resources +import re import shutil -from pkg_resources import Requirement, resource_filename +import socket +import subprocess +import sys +import time -from letsencrypt.client.CONFIG import SERVER_ROOT, BACKUP_DIR -from letsencrypt.client.CONFIG import REWRITE_HTTPS_ARGS, CONFIG_DIR, WORK_DIR -from letsencrypt.client.CONFIG import TEMP_CHECKPOINT_DIR, IN_PROGRESS_DIR -from letsencrypt.client.CONFIG import OPTIONS_SSL_CONF, LE_VHOST_EXT -from letsencrypt.client import logger, le_util, augeas_configurator -from letsencrypt.client import crypto_util - -# Challenge specific imports -import binascii, hashlib from Crypto import Random -from letsencrypt.client.CONFIG import S_SIZE, APACHE_CHALLENGE_CONF, INVALID_EXT -options_ssl_conf = resource_filename(__name__, os.path.basename(OPTIONS_SSL_CONF)) +from letsencrypt.client import augeas_configurator +from letsencrypt.client import CONFIG +from letsencrypt.client import crypto_util +from letsencrypt.client import le_util +from letsencrypt.client import logger -#from CONFIG import SERVER_ROOT, BACKUP_DIR, REWRITE_HTTPS_ARGS, CONFIG_DIR, -#from CONFIG import WORK_DIR, TEMP_CHECKPOINT_DIR, IN_PROGRESS_DIR, OPTIONS_SSL_CONF, LE_VHOST_EXT -#import logger, le_util + +options_ssl_conf = pkg_resources.resource_filename(__name__, os.path.basename(CONFIG.OPTIONS_SSL_CONF)) # Configurator should be turned into a Singleton @@ -92,7 +85,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): so that other Configurators (like Nginx) can be developed and interoperate with the client. """ - def __init__(self, server_root=SERVER_ROOT): + def __init__(self, server_root=CONFIG.SERVER_ROOT): super(ApacheConfigurator, self).__init__() self.server_root = server_root @@ -566,14 +559,14 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): def make_vhost_ssl(self, nonssl_vhost): """ Duplicates vhost and adds default ssl options - New vhost will reside as (nonssl_vhost.path) + LE_VHOST_EXT + New vhost will reside as (nonssl_vhost.path) + CONFIG.LE_VHOST_EXT """ avail_fp = nonssl_vhost.file # Copy file if avail_fp.endswith(".conf"): - ssl_fp = avail_fp[:-(len(".conf"))] + LE_VHOST_EXT + ssl_fp = avail_fp[:-(len(".conf"))] + CONFIG.LE_VHOST_EXT else: - ssl_fp = avail_fp + LE_VHOST_EXT + ssl_fp = avail_fp + CONFIG.LE_VHOST_EXT # First register the creation so that it is properly removed if # configuration is rolled back @@ -678,7 +671,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): return False, general_v #Add directives to server self.add_dir(general_v.path, "RewriteEngine", "On") - self.add_dir(general_v.path, "RewriteRule", REWRITE_HTTPS_ARGS) + self.add_dir(general_v.path, "RewriteRule", CONFIG.REWRITE_HTTPS_ARGS) self.save_notes += 'Redirecting host in %s to ssl vhost in %s\n' % (general_v.file, ssl_vhost.file) self.save() return True, general_v @@ -704,9 +697,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if not rewrite_path: # "No existing redirection for virtualhost" return False, -1 - if len(rewrite_path) == len(REWRITE_HTTPS_ARGS): + if len(rewrite_path) == len(CONFIG.REWRITE_HTTPS_ARGS): for idx, m in enumerate(rewrite_path): - if self.aug.get(m) != REWRITE_HTTPS_ARGS[idx]: + if self.aug.get(m) != CONFIG.REWRITE_HTTPS_ARGS[idx]: # Not a letsencrypt https rewrite return True, 2 # Existing letsencrypt https rewrite rule is in place @@ -979,7 +972,7 @@ LogLevel warn \n\ def save_apache_config(self): # Not currently used # Should be safe because it is a protected directory - shutil.copytree(self.server_root, BACKUP_DIR + "apache2-" + str(time.time())) + shutil.copytree(self.server_root, CONFIG.BACKUP_DIR + "apache2-" + str(time.time())) def verify_setup(self): @@ -988,9 +981,9 @@ LogLevel warn \n\ Aim for defensive coding... make sure all input files have permissions of root ''' - le_util.make_or_verify_dir(CONFIG_DIR, 0755) - le_util.make_or_verify_dir(WORK_DIR, 0755) - le_util.make_or_verify_dir(BACKUP_DIR, 0755) + le_util.make_or_verify_dir(CONFIG.CONFIG_DIR, 0755) + le_util.make_or_verify_dir(CONFIG.WORK_DIR, 0755) + le_util.make_or_verify_dir(CONFIG.BACKUP_DIR, 0755) def standardize_excl(self): """ @@ -1112,7 +1105,7 @@ LogLevel warn \n\ addresses.append(vhost.addrs) # Generate S - s = Random.get_random_bytes(S_SIZE) + s = Random.get_random_bytes(CONFIG.S_SIZE) # Create all of the challenge certs for t in chall_dict["listSNITuple"]: # Need to decode from base64 @@ -1140,7 +1133,7 @@ LogLevel warn \n\ nonce: string - hex result: returns certificate file name """ - return WORK_DIR + nonce + ".crt" + return CONFIG.WORK_DIR + nonce + ".crt" def __getConfigText(self, nonce, ip_addrs, key): """ @@ -1153,7 +1146,7 @@ LogLevel warn \n\ result: returns virtual host configuration text """ configText = " \n \ -ServerName " + nonce + INVALID_EXT + " \n \ +ServerName " + nonce + CONFIG.INVALID_EXT + " \n \ UseCanonicalName on \n \ SSLStrictSNIVHostCheck on \n \ \n \ @@ -1163,7 +1156,7 @@ Include " + options_ssl_conf + " \n \ SSLCertificateFile " + self.dvsni_get_cert_file(nonce) + " \n \ SSLCertificateKeyFile " + key + " \n \ \n \ -DocumentRoot " + CONFIG_DIR + "challenge_page/ \n \ +DocumentRoot " + CONFIG.CONFIG_DIR + "challenge_page/ \n \ \n\n " return configText @@ -1187,8 +1180,8 @@ DocumentRoot " + CONFIG_DIR + "challenge_page/ \n \ configText += " \n" self.dvsni_conf_include_check(mainConfig) - self.register_file_creation(True, APACHE_CHALLENGE_CONF) - newConf = open(APACHE_CHALLENGE_CONF, 'w') + self.register_file_creation(True, CONFIG.APACHE_CHALLENGE_CONF) + newConf = open(CONFIG.APACHE_CHALLENGE_CONF, 'w') newConf.write(configText) newConf.close() @@ -1203,9 +1196,9 @@ DocumentRoot " + CONFIG_DIR + "challenge_page/ \n \ result: User Apache configuration includes chocolate sni challenge file """ - if len(self.find_directive(self.case_i("Include"), APACHE_CHALLENGE_CONF)) == 0: + if len(self.find_directive(self.case_i("Include"), CONFIG.APACHE_CHALLENGE_CONF)) == 0: #print "Including challenge virtual host(s)" - self.add_dir("/files" + mainConfig, "Include", APACHE_CHALLENGE_CONF) + self.add_dir("/files" + mainConfig, "Include", CONFIG.APACHE_CHALLENGE_CONF) def dvsni_create_chall_cert(self, name, ext, nonce, key): """ @@ -1219,7 +1212,7 @@ DocumentRoot " + CONFIG_DIR + "challenge_page/ \n \ """ self.register_file_creation(True, self.dvsni_get_cert_file(nonce)) - cert_pem = crypto_util.make_ss_cert(key, [nonce + INVALID_EXT, name, ext]) + cert_pem = crypto_util.make_ss_cert(key, [nonce + CONFIG.INVALID_EXT, name, ext]) with open(self.dvsni_get_cert_file(nonce), 'w') as f: f.write(cert_pem) @@ -1230,16 +1223,16 @@ DocumentRoot " + CONFIG_DIR + "challenge_page/ \n \ r: byte array s: byte array - result: returns z + INVALID_EXT + result: returns z + CONFIG.INVALID_EXT """ h = hashlib.new('sha256') h.update(r) h.update(s) - return h.hexdigest() + INVALID_EXT + return h.hexdigest() + CONFIG.INVALID_EXT def main(): - config = Configurator() + config = ApacheConfigurator() logger.setLogger(logger.FileLogger(sys.stdout)) logger.setLogLevel(logger.DEBUG) """ diff --git a/letsencrypt/client/augeas_configurator.py b/letsencrypt/client/augeas_configurator.py index 6e4d54fb1..d2f6c6b44 100644 --- a/letsencrypt/client/augeas_configurator.py +++ b/letsencrypt/client/augeas_configurator.py @@ -1,12 +1,17 @@ -import os, sys, shutil, time -from letsencrypt.client.configurator import Configurator +import os +import sys +import shutil +import time import augeas -from letsencrypt.client import le_util, logger -from letsencrypt.client.CONFIG import TEMP_CHECKPOINT_DIR, IN_PROGRESS_DIR -from letsencrypt.client.CONFIG import BACKUP_DIR -class AugeasConfigurator(Configurator): +from letsencrypt.client import CONFIG +from letsencrypt.client import configurator +from letsencrypt.client import le_util +from letsencrypt.client import logger + + +class AugeasConfigurator(configurator.Configurator): def __init__(self): super(AugeasConfigurator, self).__init__() @@ -90,13 +95,13 @@ class AugeasConfigurator(Configurator): # Create Checkpoint if temporary: - self.add_to_checkpoint(TEMP_CHECKPOINT_DIR, save_files) + self.add_to_checkpoint(CONFIG.TEMP_CHECKPOINT_DIR, save_files) else: - self.add_to_checkpoint(IN_PROGRESS_DIR, save_files) + self.add_to_checkpoint(CONFIG.IN_PROGRESS_DIR, save_files) - if title and not temporary and os.path.isdir(IN_PROGRESS_DIR): - success = self.__finalize_checkpoint(IN_PROGRESS_DIR, title) + if title and not temporary and os.path.isdir(CONFIG.IN_PROGRESS_DIR): + success = self.__finalize_checkpoint(CONFIG.IN_PROGRESS_DIR, title) if not success: # This should never happen # This will be hopefully be cleaned up on the recovery @@ -115,12 +120,12 @@ class AugeasConfigurator(Configurator): This function should reload the users original configuration files for all saves with reversible=True """ - if os.path.isdir(TEMP_CHECKPOINT_DIR): - result = self.__recover_checkpoint(TEMP_CHECKPOINT_DIR) + if os.path.isdir(CONFIG.TEMP_CHECKPOINT_DIR): + result = self.__recover_checkpoint(CONFIG.TEMP_CHECKPOINT_DIR) changes = True if result != 0: # We have a partial or incomplete recovery - logger.fatal("Incomplete or failed recovery for %s" % TEMP_CHECKPOINT_DIR) + logger.fatal("Incomplete or failed recovery for %s" % CONFIG.TEMP_CHECKPOINT_DIR) sys.exit(67) # Remember to reload Augeas self.aug.load() @@ -137,14 +142,14 @@ class AugeasConfigurator(Configurator): logger.error("Rollback argument must be a positive integer") return - backups = os.listdir(BACKUP_DIR) + backups = os.listdir(CONFIG.BACKUP_DIR) backups.sort() if len(backups) < rollback: logger.error("Unable to rollback %d checkpoints, only %d exist" % (rollback, len(backups))) while rollback > 0 and backups: - cp_dir = BACKUP_DIR + backups.pop() + cp_dir = CONFIG.BACKUP_DIR + backups.pop() result = self.__recover_checkpoint(cp_dir) if result != 0: logger.fatal("Failed to load checkpoint during rollback") @@ -160,7 +165,7 @@ class AugeasConfigurator(Configurator): script found in the constructor, before this function would ever be called """ - backups = os.listdir(BACKUP_DIR) + backups = os.listdir(CONFIG.BACKUP_DIR) backups.sort(reverse=True) if not backups: @@ -171,21 +176,21 @@ class AugeasConfigurator(Configurator): for bu in backups: float(bu) except: - assert False, "Invalid files in %s" % BACKUP_DIR + assert False, "Invalid files in %s" % CONFIG.BACKUP_DIR for bu in backups: print time.ctime(float(bu)) - with open(BACKUP_DIR + bu + "/CHANGES_SINCE") as f: + with open(CONFIG.BACKUP_DIR + bu + "/CHANGES_SINCE") as f: print f.read() print "Affected files:" - with open(BACKUP_DIR + bu + "/FILEPATHS") as f: + with open(CONFIG.BACKUP_DIR + bu + "/FILEPATHS") as f: filepaths = f.read().splitlines() for fp in filepaths: print " %s" % fp try: - with open(BACKUP_DIR + bu + "/NEW_FILES") as f: + with open(CONFIG.BACKUP_DIR + bu + "/NEW_FILES") as f: print "New Configuration Files:" filepaths = f.read().splitlines() for fp in filepaths: @@ -199,7 +204,7 @@ class AugeasConfigurator(Configurator): Add title to cp_dir CHANGES_SINCE Move cp_dir to Backups directory and rename with timestamp """ - final_dir = BACKUP_DIR + str(time.time()) + final_dir = CONFIG.BACKUP_DIR + str(time.time()) try: with open(cp_dir + "CHANGES_SINCE.tmp", 'w') as ft: ft.write("-- %s --\n" % title) @@ -275,7 +280,7 @@ class AugeasConfigurator(Configurator): return 0 def check_tempfile_saves(self, save_files, temporary): - temp_path = "%sFILEPATHS" % TEMP_CHECKPOINT_DIR + temp_path = "%sFILEPATHS" % CONFIG.TEMP_CHECKPOINT_DIR if os.path.isfile(temp_path): with open(temp_path, 'r') as protected_fd: protected_files = protected_fd.read().splitlines() @@ -294,9 +299,9 @@ class AugeasConfigurator(Configurator): (Before a save occurs) """ if temporary: - cp_dir = TEMP_CHECKPOINT_DIR + cp_dir = CONFIG.TEMP_CHECKPOINT_DIR else: - cp_dir = IN_PROGRESS_DIR + cp_dir = CONFIG.IN_PROGRESS_DIR le_util.make_or_verify_dir(cp_dir) try: @@ -310,19 +315,19 @@ class AugeasConfigurator(Configurator): def recovery_routine(self): """ Revert all previously modified files. First, any changes found in - TEMP_CHECKPOINT_DIR are removed, then IN_PROGRESS changes are removed + CONFIG.TEMP_CHECKPOINT_DIR are removed, then IN_PROGRESS changes are removed The order is important. IN_PROGRESS is unable to add files that are already added by a TEMP change. Thus TEMP must be rolled back first because that will be the 'latest' occurrence of the file. """ self.revert_challenge_config() - if os.path.isdir(IN_PROGRESS_DIR): - result = self.__recover_checkpoint(IN_PROGRESS_DIR) + if os.path.isdir(CONFIG.IN_PROGRESS_DIR): + result = self.__recover_checkpoint(CONFIG.IN_PROGRESS_DIR) if result != 0: # We have a partial or incomplete recovery # Not as egregious # TODO: Additional tests? recovery - logger.fatal("Incomplete or failed recovery for %s" % IN_PROGRESS_DIR) + logger.fatal("Incomplete or failed recovery for %s" % CONFIG.IN_PROGRESS_DIR) sys.exit(68) # Need to reload configuration after these changes take effect diff --git a/letsencrypt/client/challenge.py b/letsencrypt/client/challenge.py index 2ccf9406f..0ddd6ab51 100644 --- a/letsencrypt/client/challenge.py +++ b/letsencrypt/client/challenge.py @@ -1,5 +1,5 @@ from letsencrypt.client import logger -#import logger + class Challenge(object): def __init__(self, configurator): diff --git a/letsencrypt/client/client.py b/letsencrypt/client/client.py index 0788800b2..41c966100 100644 --- a/letsencrypt/client/client.py +++ b/letsencrypt/client/client.py @@ -1,21 +1,25 @@ -#!/usr/bin/env python +import csv +import datetime +import json +import os +import shutil +import socket +import string +import sys +import time import M2Crypto -import json -import os, time, sys, shutil - -import csv - import requests -from letsencrypt.client.acme import acme_object_validate -from letsencrypt.client import configurator, apache_configurator -from letsencrypt.client import logger, display -from letsencrypt.client import le_util, crypto_util -from letsencrypt.client.CONFIG import RSA_KEY_SIZE, CERT_PATH -from letsencrypt.client.CONFIG import CHAIN_PATH, SERVER_ROOT, KEY_DIR, CERT_DIR -from letsencrypt.client.CONFIG import CERT_KEY_BACKUP, EXCLUSIVE_CHALLENGES -from letsencrypt.client.CONFIG import CHALLENGE_PREFERENCES, CONFIG_CHALLENGES +from letsencrypt.client import acme +from letsencrypt.client import apache_configurator +from letsencrypt.client import CONFIG +from letsencrypt.client import crypto_util +from letsencrypt.client import display +from letsencrypt.client import le_util +from letsencrypt.client import logger + + # it's weird to point to chocolate servers via raw IPv6 addresses, and such # addresses can be %SCARY in some contexts, so out of paranoia let's disable # them by default @@ -35,7 +39,7 @@ class Client(object): # TODO: Can probably figure out which configurator to use without # special packaging based on system info # Command line arg or client function to discover - self.config = apache_configurator.ApacheConfigurator(SERVER_ROOT) + self.config = apache_configurator.ApacheConfigurator(CONFIG.SERVER_ROOT) self.server = ca_server @@ -168,8 +172,8 @@ class Client(object): self.list_certs_keys() def remove_cert_key(self, c): - list_file = CERT_KEY_BACKUP + "LIST" - list_file2 = CERT_KEY_BACKUP + "LIST.tmp" + list_file = CONFIG.CERT_KEY_BACKUP + "LIST" + list_file2 = CONFIG.CERT_KEY_BACKUP + "LIST.tmp" with open(list_file, 'rb') as orgfile: csvreader = csv.reader(orgfile) with open(list_file2, 'wb') as newfile: @@ -187,10 +191,10 @@ class Client(object): def list_certs_keys(self): - list_file = CERT_KEY_BACKUP + "LIST" + list_file = CONFIG.CERT_KEY_BACKUP + "LIST" certs = [] - if not os.path.isfile(CERT_KEY_BACKUP + "LIST"): + if not os.path.isfile(CONFIG.CERT_KEY_BACKUP + "LIST"): logger.info("You don't have any certificates saved from letsencrypt") return @@ -206,8 +210,8 @@ class Client(object): for row in csvreader: c = crypto_util.get_cert_info(row[1]) - b_k = CERT_KEY_BACKUP + os.path.basename(row[2]) + "_" + row[0] - b_c = CERT_KEY_BACKUP + os.path.basename(row[1]) + "_" + row[0] + b_k = CONFIG.CERT_KEY_BACKUP + os.path.basename(row[2]) + "_" + row[0] + b_c = CONFIG.CERT_KEY_BACKUP + os.path.basename(row[1]) + "_" + row[0] c["orig_key_file"] = row[2] c["orig_cert_file"] = row[1] @@ -246,7 +250,7 @@ class Client(object): def install_certificate(self, certificate_dict, vhost): cert_chain_abspath = None - cert_fd, self.cert_file = le_util.unique_file(CERT_PATH, 644) + cert_fd, self.cert_file = le_util.unique_file(CONFIG.CERT_PATH, 644) cert_fd.write( crypto_util.b64_cert_to_pem(certificate_dict["certificate"])) cert_fd.close() @@ -254,7 +258,7 @@ class Client(object): self.cert_file) if certificate_dict.get("chain", None): - chain_fd, chain_fn = le_util.unique_file(CHAIN_PATH, 644) + chain_fd, chain_fn = le_util.unique_file(CONFIG.CHAIN_PATH, 644) for c in certificate_dict.get("chain", []): chain_fd.write(crypto_util.b64_cert_to_pem(c)) chain_fd.close() @@ -308,7 +312,7 @@ class Client(object): def cleanup_challenges(self, challenge_objs): logger.info("Cleaning up challenges...") for c in challenge_objs: - if c["type"] in CONFIG_CHALLENGES: + if c["type"] in CONFIG.CONFIG_CHALLENGES: self.config.cleanup() else: #Handle other cleanup if needed @@ -377,7 +381,7 @@ class Client(object): # Perform challenges for i, c_obj in enumerate(challenge_objs): response = "null" - if c_obj["type"] in CONFIG_CHALLENGES: + if c_obj["type"] in CONFIG.CONFIG_CHALLENGES: response = self.config.perform(c_obj) else: # Handle RecoveryToken type challenges @@ -411,7 +415,7 @@ class Client(object): """ chall_cost = {} max_cost = 0 - for i, chall in enumerate(CHALLENGE_PREFERENCES): + for i, chall in enumerate(CONFIG.CHALLENGE_PREFERENCES): chall_cost[chall] = i max_cost += i @@ -443,7 +447,7 @@ class Client(object): # Add logic for a crappy server # Choose a DV path = [] - for pref_c in CHALLENGE_PREFERENCES: + for pref_c in CONFIG.CHALLENGE_PREFERENCES: for i, offered_c in enumerate(challenges): if (pref_c == offered_c["type"] and self.is_preferred(offered_c["type"], path)): @@ -454,7 +458,7 @@ class Client(object): def is_preferred(self, offered_c_type, path): for tup in path: - for s in EXCLUSIVE_CHALLENGES: + for s in CONFIG.EXCLUSIVE_CHALLENGES: # Second part is in case we eventually allow multiple names # to be challenges at the same time if (tup[1] in s and offered_c_type in s and @@ -466,14 +470,14 @@ class Client(object): def send(self, json_obj): try: json_encoded = json.dumps(json_obj) - acme_object_validate(json_encoded) + acme.acme_object_validate(json_encoded) response = requests.post( self.server_url, data=json_encoded, headers={"Content-Type": "application/json"}, ) body = response.content - acme_object_validate(body) + acme.acme_object_validate(body) return response.json() except Exception as e: logger.fatal("Send() failed... may have lost connection to server") @@ -488,8 +492,8 @@ class Client(object): def store_cert_key(self, encrypt = False): - list_file = CERT_KEY_BACKUP + "LIST" - le_util.make_or_verify_dir(CERT_KEY_BACKUP, 0700) + list_file = CONFIG.CERT_KEY_BACKUP + "LIST" + le_util.make_or_verify_dir(CONFIG.CERT_KEY_BACKUP, 0700) idx = 0 if encrypt: @@ -511,10 +515,10 @@ class Client(object): csvwriter.writerow(["0", self.cert_file, self.key_file]) shutil.copy2(self.key_file, - CERT_KEY_BACKUP + os.path.basename(self.key_file) + + CONFIG.CERT_KEY_BACKUP + os.path.basename(self.key_file) + "_" + str(idx)) shutil.copy2(self.cert_file, - CERT_KEY_BACKUP + os.path.basename(self.cert_file) + + CONFIG.CERT_KEY_BACKUP + os.path.basename(self.cert_file) + "_" + str(idx)) @@ -581,11 +585,11 @@ class Client(object): key_pem = None csr_pem = None if not self.key_file: - key_pem = crypto_util.make_key(RSA_KEY_SIZE) + key_pem = crypto_util.make_key(CONFIG.RSA_KEY_SIZE) # Save file - le_util.make_or_verify_dir(KEY_DIR, 0700) + le_util.make_or_verify_dir(CONFIG.KEY_DIR, 0700) key_f, self.key_file = le_util.unique_file( - KEY_DIR + "key-letsencrypt.pem", 0600) + CONFIG.KEY_DIR + "key-letsencrypt.pem", 0600) key_f.write(key_pem) key_f.close() logger.info("Generating key: %s" % self.key_file) @@ -599,9 +603,9 @@ class Client(object): if not self.csr_file: csr_pem, csr_der = crypto_util.make_csr(self.key_file, self.names) # Save CSR - le_util.make_or_verify_dir(CERT_DIR, 0755) + le_util.make_or_verify_dir(CONFIG.CERT_DIR, 0755) csr_f, self.csr_file = le_util.unique_file( - CERT_DIR + "csr-letsencrypt.pem", 0644) + CONFIG.CERT_DIR + "csr-letsencrypt.pem", 0644) csr_f.write(csr_pem) csr_f.close() logger.info("Creating CSR: %s" % self.csr_file) @@ -695,15 +699,13 @@ class Client(object): Do enough to avoid shellcode from the environment. There's no need to do more. """ - import string as s - allowed = s.ascii_letters + s.digits + "-." # hostnames & IPv4 + allowed = string.ascii_letters + string.digits + "-." # hostnames & IPv4 if all([c in allowed for c in hostname]): return True if not allow_raw_ipv6_server: return False # ipv6 is messy and complicated, can contain %zoneindex etc. - import socket try: # is this a valid IPv6 address? socket.getaddrinfo(hostname,443,socket.AF_INET6) diff --git a/letsencrypt/client/crypto_util.py b/letsencrypt/client/crypto_util.py index 3ad7ff6a3..875858351 100644 --- a/letsencrypt/client/crypto_util.py +++ b/letsencrypt/client/crypto_util.py @@ -1,29 +1,32 @@ -import M2Crypto -import time, binascii +import binascii import hashlib -from Crypto.Random import get_random_bytes -from Crypto.PublicKey import RSA -from Crypto.Signature import PKCS1_v1_5 -from Crypto.Hash import SHA256 -from M2Crypto import EVP, X509, ASN1 +import time -from letsencrypt.client.CONFIG import NONCE_SIZE, RSA_KEY_SIZE +from Crypto import Random +import Crypto.Hash.SHA256 +import Crypto.PublicKey.RSA +import Crypto.Signature.PKCS1_v1_5 + +import M2Crypto + +from letsencrypt.client import CONFIG from letsencrypt.client import le_util + def b64_cert_to_pem(b64_der_cert): x = M2Crypto.X509.load_cert_der_string(le_util.b64_url_dec(b64_der_cert)) return x.as_pem() -def create_sig(msg, key_file, signer_nonce = None, signer_nonce_len = NONCE_SIZE): +def create_sig(msg, key_file, signer_nonce = None, signer_nonce_len = CONFIG.NONCE_SIZE): # DOES prepend signer_nonce to message # TODO: Change this over to M2Crypto... PKey # Protect against crypto unicode errors... is this sufficient? Do I need to escape? msg = str(msg) - key = RSA.importKey(open(key_file).read()) + key = Crypto.PublicKey.RSA.importKey(open(key_file).read()) if signer_nonce is None: - signer_nonce = get_random_bytes(signer_nonce_len) - h = SHA256.new(signer_nonce + msg) - signer = PKCS1_v1_5.new(key) + signer_nonce = Random.get_random_bytes(signer_nonce_len) + h = Crypto.Hash.SHA256.new(signer_nonce + msg) + signer = Crypto.Signature.PKCS1_v1_5.new(key) signature = signer.sign(h) #print "signing:", signer_nonce + msg #print "signature:", signature @@ -48,12 +51,12 @@ def sha256(m): return hashlib.sha256(m).hexdigest() # based on M2Crypto unit test written by Toby Allsopp -def make_key(bits=RSA_KEY_SIZE): +def make_key(bits=CONFIG.RSA_KEY_SIZE): """ Returns new RSA key in PEM form with specified bits """ #Python Crypto module doesn't produce any stdout - key = RSA.generate(bits) + key = Crypto.PublicKey.RSA.generate(bits) #rsa = M2Crypto.RSA.gen_key(bits, 65537) #key_pem = rsa.as_pem(cipher=None) #rsa = None # should not be freed here @@ -67,10 +70,10 @@ def make_csr(key_file, domains): """ assert domains, "Must provide one or more hostnames for the CSR." rsa_key = M2Crypto.RSA.load_key(key_file) - pk = EVP.PKey() + pk = M2Crypto.EVP.PKey() pk.assign_rsa(rsa_key) - x = X509.Request() + x = M2Crypto.X509.Request() x.set_pubkey(pk) name = x.get_subject() name.C = "US" @@ -80,8 +83,8 @@ def make_csr(key_file, domains): name.OU = "University of Michigan" name.CN = domains[0] - extstack = X509.X509_Extension_Stack() - ext = X509.new_extension('subjectAltName', ", ".join(["DNS:%s" % d for d in domains])) + extstack = M2Crypto.X509.X509_Extension_Stack() + ext = M2Crypto.X509.new_extension('subjectAltName', ", ".join(["DNS:%s" % d for d in domains])) extstack.push(ext) x.add_extensions(extstack) @@ -97,18 +100,18 @@ def make_ss_cert(key_file, domains): """ assert domains, "Must provide one or more hostnames for the CSR." rsa_key = M2Crypto.RSA.load_key(key_file) - pk = EVP.PKey() + pk = M2Crypto.EVP.PKey() pk.assign_rsa(rsa_key) - x = X509.X509() + x = M2Crypto.X509.X509() x.set_pubkey(pk) x.set_serial_number(1337) x.set_version(2) t = long(time.time()) - current = ASN1.ASN1_UTCTIME() + current = M2Crypto.ASN1.ASN1_UTCTIME() current.set_time(t) - expire = ASN1.ASN1_UTCTIME() + expire = M2Crypto.ASN1.ASN1_UTCTIME() expire.set_time((7 * 24 * 60 * 60) + t) x.set_not_before(current) x.set_not_after(expire) @@ -121,9 +124,9 @@ def make_ss_cert(key_file, domains): name.CN = domains[0] x.set_issuer(x.get_subject()) - x.add_ext(X509.new_extension('basicConstraints', 'CA:FALSE')) - #x.add_ext(X509.new_extension('extendedKeyUsage', 'TLS Web Server Authentication')) - x.add_ext(X509.new_extension('subjectAltName', ", ".join(["DNS:%s" % d for d in domains]))) + x.add_ext(M2Crypto.X509.new_extension('basicConstraints', 'CA:FALSE')) + #x.add_ext(M2Crypto.X509.new_extension('extendedKeyUsage', 'TLS Web Server Authentication')) + x.add_ext(M2Crypto.X509.new_extension('subjectAltName', ", ".join(["DNS:%s" % d for d in domains]))) x.sign(pk, 'sha256') assert x.verify(pk) diff --git a/letsencrypt/client/display.py b/letsencrypt/client/display.py index c0c430fab..4c1633776 100644 --- a/letsencrypt/client/display.py +++ b/letsencrypt/client/display.py @@ -1,3 +1,5 @@ +import textwrap + import dialog @@ -80,7 +82,6 @@ class Display(SingletonD): class NcursesDisplay(Display): - import dialog def __init__(self): self.d = dialog.Dialog() @@ -153,11 +154,8 @@ class NcursesDisplay(Display): print text self.d.msgbox(text, width=WIDTH, height=HEIGHT) -textwrap = None class FileDisplay(Display): - global textwrap - import textwrap def __init__(self, outfile): self.outfile = outfile diff --git a/letsencrypt/client/interactive_challenge.py b/letsencrypt/client/interactive_challenge.py index dfa960483..7d3c8d062 100644 --- a/letsencrypt/client/interactive_challenge.py +++ b/letsencrypt/client/interactive_challenge.py @@ -1,6 +1,10 @@ -from letsencrypt.client.challenge import Challenge import textwrap +import dialog + +from letsencrypt.client import challenge + + ########################################################### # Interactive challenge displays the string sent by the CA # formatted to fit on the screen of the client @@ -8,7 +12,7 @@ import textwrap # client should continue the letsencrypt process ########################################################### -class Interactive_Challenge(Challenge): +class Interactive_Challenge(challenge.Challenge): BOX_SIZE = 70 def __init__(self, string): diff --git a/letsencrypt/client/le_util.py b/letsencrypt/client/le_util.py index fbe7fb53e..f9afd8171 100644 --- a/letsencrypt/client/le_util.py +++ b/letsencrypt/client/le_util.py @@ -1,12 +1,13 @@ -# This file will contain functions useful for all Letsencrypt Classes -import errno -import stat -import os, pwd, grp -import time +"""Utilities for all Let's Encrypt.""" import base64 -from letsencrypt.client import logger -#import logger +import grp +import errno +import os +import pwd +import stat +import sys +from letsencrypt.client import logger def make_or_verify_dir(directory, permissions=0755, uid=0): diff --git a/letsencrypt/client/logger.py b/letsencrypt/client/logger.py index 86a8da846..ac1b85033 100644 --- a/letsencrypt/client/logger.py +++ b/letsencrypt/client/logger.py @@ -1,6 +1,12 @@ +import logger +import textwrap import time + +import dialog + from letsencrypt.client import display + class Singleton(object): _instance = None def __new__(cls, *args, **kwargs): @@ -36,11 +42,8 @@ class Logger(Singleton): t = time.time() return time.strftime("%b %d %Y %H:%M:%S", time.localtime(t)) + ('%.03f' % (t - int(t)))[1:] -textwrap = None class FileLogger(Logger): - global textwrap - import textwrap def __init__(self, outfile): self.outfile = outfile @@ -51,7 +54,7 @@ class FileLogger(Logger): wm = textwrap.fill(msg, 80) self.outfile.write("%s\n" % wm) -import dialog + class NcursesLogger(Logger): def __init__(self, @@ -143,28 +146,27 @@ def none(data): if __name__ == "__main__": # Unit test/example usage: - import logger # Set the logging type you want to use (stdout logging): #logger.setLogger(FileLogger(sys.stdout)) - logger.setLogger(NcursesLogger()) + setLogger(NcursesLogger()) # Set the most verbose you want to log (TRACE, DEBUG, INFO, WARN, ERROR, FATAL, NONE) - logger.setLogLevel(logger.TRACE) + setLogLevel(logger.TRACE) # Log a message: #logger.log(logger.INFO, "logger!") time.sleep(0.01) - logger.info("This is a long line, it's pretty long, butitalso hasbig wordsthat areprobably hardtobreak oninan easywayforthe ncurseslib, sowhatdoes itdo then?") - logger.info("aa " + "a"*70 + "B") + info("This is a long line, it's pretty long, butitalso hasbig wordsthat areprobably hardtobreak oninan easywayforthe ncurseslib, sowhatdoes itdo then?") + info("aa " + "a"*70 + "B") for i in range(20): - logger.info("iteration #%d/20" % i) + info("iteration #%d/20" % i) time.sleep(0.3) # Alternatively, use - logger.error("errrrr") + error("errrrr") - logger.trace("some trace data: %d - %f - %s" % (5, 8.3, 'cows')) + trace("some trace data: %d - %f - %s" % (5, 8.3, 'cows')) diff --git a/letsencrypt/client/nginx_configurator.py b/letsencrypt/client/nginx_configurator.py index d1b927e5b..887180aed 100644 --- a/letsencrypt/client/nginx_configurator.py +++ b/letsencrypt/client/nginx_configurator.py @@ -1,8 +1,5 @@ -from letsencrypt.client.CONFIG import SERVER_ROOT, BACKUP_DIR -from letsencrypt.client.CONFIG import REWRITE_HTTPS_ARGS, CONFIG_DIR, WORK_DIR -from letsencrypt.client.CONFIG import TEMP_CHECKPOINT_DIR, IN_PROGRESS_DIR -from letsencrypt.client.CONFIG import OPTIONS_SSL_CONF, LE_VHOST_EXT -from letsencrypt.client import logger, le_util, configurator +from letsencrypt.client import CONFIG +from letsencrypt.client import augeas_configurator # This might be helpful... but feel free to use whatever you want @@ -21,9 +18,9 @@ from letsencrypt.client import logger, le_util, configurator # def add_name(self, name): # self.names.append(name) -class NginxConfigurator(AugeasConfigurator): +class NginxConfigurator(augeas_configurator.AugeasConfigurator): - def __init__(self, server_root=SERVER_ROOT): + def __init__(self, server_root=CONFIG.SERVER_ROOT): self.server_root = server_root # See if any temporary changes need to be recovered @@ -188,9 +185,9 @@ class NginxConfigurator(AugeasConfigurator): # Aim for defensive coding... make sure all input files # have permissions of root # ''' - # le_util.make_or_verify_dir(CONFIG_DIR, 0755) - # le_util.make_or_verify_dir(WORK_DIR, 0755) - # le_util.make_or_verify_dir(BACKUP_DIR, 0755) + # le_util.make_or_verify_dir(CONFIG.CONFIG_DIR, 0755) + # le_util.make_or_verify_dir(CONFIG.WORK_DIR, 0755) + # le_util.make_or_verify_dir(CONFIG.BACKUP_DIR, 0755) def restart(self, quiet=False): """ diff --git a/letsencrypt/client/recovery_contact_challenge.py b/letsencrypt/client/recovery_contact_challenge.py index 3509f93fb..65f6940a0 100644 --- a/letsencrypt/client/recovery_contact_challenge.py +++ b/letsencrypt/client/recovery_contact_challenge.py @@ -1,11 +1,11 @@ -import requests - -from letsencrypt.client.challenge import Challenge -from letsencrypt.client import logger -from letsencrypt.client.CONFIG import RECOVERY_TOKEN_EXT import dialog +import requests +import time -class RecoveryContact(Challenge): +from letsencrypt.client import challenge + + +class RecoveryContact(challenge.Challenge): def __init__(self, activationURL = "", successURL = "", contact = "", poll_delay = 3): self.token = "" diff --git a/letsencrypt/client/recovery_token_challenge.py b/letsencrypt/client/recovery_token_challenge.py index 1e81bd775..4b1206541 100644 --- a/letsencrypt/client/recovery_token_challenge.py +++ b/letsencrypt/client/recovery_token_challenge.py @@ -1,6 +1,9 @@ -from letsencrypt.client.challenge import Challenge +import dialog -class RecoveryToken(Challenge): +from letsencrypt.client import challenge + + +class RecoveryToken(challenge.Challenge): def __init__(self): self.token = "" diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index e21a557e4..fa2166127 100755 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -1,14 +1,15 @@ #!/usr/bin/env python - -# This file parses the command line and calls the appropriate functions - +"""Parse command line and call the appropriate functions.""" import getopt import os import sys +from letsencrypt.client import apache_configurator +from letsencrypt.client import CONFIG from letsencrypt.client import client from letsencrypt.client import display -from letsencrypt.client.CONFIG import ACME_SERVER +from letsencrypt.client import logger + def main(): # Check to make sure user is root @@ -49,7 +50,6 @@ def main(): elif o == "--server": server = a elif o == "--rollback": - from letsencrypt.client import apache_configurator, logger logger.setLogger(logger.FileLogger(sys.stdout)) logger.setLogLevel(logger.INFO) config = apache_configurator.ApacheConfigurator() @@ -57,7 +57,6 @@ def main(): config.restart() sys.exit(0) elif o == "--view-checkpoints": - from letsencrypt.client import apache_configurator, logger logger.setLogger(logger.FileLogger(sys.stdout)) logger.setLogLevel(logger.INFO) config = apache_configurator.ApacheConfigurator() @@ -84,7 +83,7 @@ def main(): display.setDisplay(display.FileDisplay(sys.stdout)) if not server: - server = ACME_SERVER + server = CONFIG.ACME_SERVER c = client.Client(server, csr, privkey, curses) if flag_revoke: From d88eb92fbe68d02e9c766c2b234f59c64d60a4c9 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 21 Nov 2014 21:18:57 +0100 Subject: [PATCH 04/21] Fix undefined-variable --- letsencrypt/client/augeas_configurator.py | 2 +- letsencrypt/client/client.py | 8 +++++--- letsencrypt/client/display.py | 2 +- letsencrypt/client/interactive_challenge.py | 6 +++--- letsencrypt/client/recovery_contact_challenge.py | 2 +- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/letsencrypt/client/augeas_configurator.py b/letsencrypt/client/augeas_configurator.py index d2f6c6b44..1b7c9f9ce 100644 --- a/letsencrypt/client/augeas_configurator.py +++ b/letsencrypt/client/augeas_configurator.py @@ -62,7 +62,7 @@ class AugeasConfigurator(configurator.Configurator): except: # Check for the root of save problems new_errs = self.aug.match("/augeas//error") - logger.error("During Save - " + mod_conf) + # logger.error("During Save - " + mod_conf) # Only print new errors caused by recent save for err in new_errs: if err not in ex_errs: diff --git a/letsencrypt/client/client.py b/letsencrypt/client/client.py index 41c966100..401cc2963 100644 --- a/letsencrypt/client/client.py +++ b/letsencrypt/client/client.py @@ -427,8 +427,9 @@ class Client(object): for combo in combos: for c in combo: combo_total += chall_cost.get(challenges[c]["type"], max_cost) - if combo_total < best_combo_total: + if combo_total < best_combo_cost: best_combo = combo + best_combo_cost = combo_total combo_total = 0 if not best_combo: @@ -557,7 +558,7 @@ class Client(object): elif challenges[c]["type"] == "recoveryToken": logger.info("\tRecovery Token Challenge for name: %s." % name) - challenge_objs_indicies.append(c) + challenge_obj_indicies.append(c) challenge_objs.append({type:"recoveryToken"}) else: @@ -743,7 +744,8 @@ def renew(config): cert = M2Crypto.X509.load_cert(tup[0]) issuer = cert.get_issuer() if recognized_ca(issuer): - generate_renewal_req() + pass + # generate_renewal_req() # Wait for response, act accordingly gen_req_from_cert() diff --git a/letsencrypt/client/display.py b/letsencrypt/client/display.py index 4c1633776..d04f3785f 100644 --- a/letsencrypt/client/display.py +++ b/letsencrypt/client/display.py @@ -97,7 +97,7 @@ class NcursesDisplay(Display): width = WIDTH, height = HEIGHT) return c, str(selection) else: - choices = [((i + 1), c) for c in choices] + choices = list(enumerate(choices, 1)) code, s = self.d.menu(message, choices = choices, width = WIDTH, height = HEIGHT) diff --git a/letsencrypt/client/interactive_challenge.py b/letsencrypt/client/interactive_challenge.py index 7d3c8d062..467403328 100644 --- a/letsencrypt/client/interactive_challenge.py +++ b/letsencrypt/client/interactive_challenge.py @@ -20,16 +20,16 @@ class Interactive_Challenge(challenge.Challenge): def perform(self, quiet=True): if quiet: - dialog.Dialog().msgbox(get_display_string(), width=BOX_SIZE) + dialog.Dialog().msgbox(self.get_display_string(), width=self.BOX_SIZE) else: - print get_display_string() + print self.get_display_string() raw_input('') return True def get_display_string(self): - return textwrap.fill(self.string, width=BOX_SIZE) + "\n\nPlease Press Enter to Continue" + return textwrap.fill(self.string, width=self.BOX_SIZE) + "\n\nPlease Press Enter to Continue" def formatted_reasons(self): return "\n\t* %s\n", self.reason diff --git a/letsencrypt/client/recovery_contact_challenge.py b/letsencrypt/client/recovery_contact_challenge.py index 65f6940a0..7e0a26693 100644 --- a/letsencrypt/client/recovery_contact_challenge.py +++ b/letsencrypt/client/recovery_contact_challenge.py @@ -27,7 +27,7 @@ class RecoveryContact(challenge.Challenge): else: print self.get_display_string() - if successURL: + if self.successURL: return self.poll(10, quiet) else: self.token = raw_input("Enter the recovery token:") From a62c02a9cfd41dfb6a6240d170aaa09b0f8b20cd Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 21 Nov 2014 21:42:25 +0100 Subject: [PATCH 05/21] Fix no-self-argument --- letsencrypt/client/validator.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/letsencrypt/client/validator.py b/letsencrypt/client/validator.py index 99532f1a2..c647f7d2b 100644 --- a/letsencrypt/client/validator.py +++ b/letsencrypt/client/validator.py @@ -2,11 +2,11 @@ class Validator(object): """ This Class will contain an API to validate configurations. """ - def redirect(name): + def redirect(self, name): return - def ocsp_stapling(name): + def ocsp_stapling(self, name): return - def https(names): + def https(self, names): return - def hsts(name): + def hsts(self, name): return From 9cdb7dbae2c08591aca2f35354f773d22f881558 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 21 Nov 2014 21:51:06 +0100 Subject: [PATCH 06/21] Fix pylint errors or comment unused invalid code. augeas_configurator.py: E:220,12: Too many positional arguments for function call (too-many-function-args) client.py: E:628,18: Instance of 'Client' has no 'get_cas' member (no-member) interactive_challenge.py: E: 35,29: Instance of 'Interactive_Challenge' has no 'reason' member (no-member) --- letsencrypt/client/augeas_configurator.py | 2 +- letsencrypt/client/client.py | 16 ++++++++-------- letsencrypt/client/interactive_challenge.py | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/letsencrypt/client/augeas_configurator.py b/letsencrypt/client/augeas_configurator.py index 1b7c9f9ce..d66b42234 100644 --- a/letsencrypt/client/augeas_configurator.py +++ b/letsencrypt/client/augeas_configurator.py @@ -217,7 +217,7 @@ class AugeasConfigurator(configurator.Configurator): try: os.rename(cp_dir, final_dir) except: - logger.error("Unable to finalize checkpoint, %s -> %s" % cp_dir, final_dir) + logger.error("Unable to finalize checkpoint, %s -> %s" % (cp_dir, final_dir)) return False return True diff --git a/letsencrypt/client/client.py b/letsencrypt/client/client.py index 401cc2963..eff28bbb3 100644 --- a/letsencrypt/client/client.py +++ b/letsencrypt/client/client.py @@ -624,16 +624,16 @@ class Client(object): return key_pem, csr_pem - def choice_of_ca(self): - choices = self.get_cas() - message = "Pick a Certificate Authority. They're all unique and special!" - in_txt = "Enter the number of a Certificate Authority (c to cancel): " - code, selection = display.generic_menu(message, choices, in_txt) + # def choice_of_ca(self): + # choices = self.get_cas() + # message = "Pick a Certificate Authority. They're all unique and special!" + # in_txt = "Enter the number of a Certificate Authority (c to cancel): " + # code, selection = display.generic_menu(message, choices, in_txt) - if code != display.OK: - sys.exit(0) + # if code != display.OK: + # sys.exit(0) - return selection + # return selection # Legacy Code: Although I would like to see a free and open marketplace # in the future. The Let's Encrypt Client will not have this feature at diff --git a/letsencrypt/client/interactive_challenge.py b/letsencrypt/client/interactive_challenge.py index 467403328..720a0381d 100644 --- a/letsencrypt/client/interactive_challenge.py +++ b/letsencrypt/client/interactive_challenge.py @@ -31,5 +31,5 @@ class Interactive_Challenge(challenge.Challenge): def get_display_string(self): return textwrap.fill(self.string, width=self.BOX_SIZE) + "\n\nPlease Press Enter to Continue" - def formatted_reasons(self): - return "\n\t* %s\n", self.reason + # def formatted_reasons(self): + # return "\n\t* %s\n", self.reason From 123e64ff03fe4d042d48acd45aed03bbefb6e44b Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 21 Nov 2014 22:03:52 +0100 Subject: [PATCH 07/21] Clean validator module. --- letsencrypt/client/validator.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/letsencrypt/client/validator.py b/letsencrypt/client/validator.py index c647f7d2b..716d1528f 100644 --- a/letsencrypt/client/validator.py +++ b/letsencrypt/client/validator.py @@ -1,12 +1,14 @@ class Validator(object): - """ - This Class will contain an API to validate configurations. - """ + """Configuration validator.""" + def redirect(self, name): - return + raise NotImplementedError() + def ocsp_stapling(self, name): - return + raise NotImplementedError() + def https(self, names): - return + raise NotImplementedError() + def hsts(self, name): - return + raise NotImplementedError() From de555cec87048cff359363c0a4ce61d1c9e677e2 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 21 Nov 2014 22:11:45 +0100 Subject: [PATCH 08/21] Clean challenge module. --- letsencrypt/client/challenge.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/letsencrypt/client/challenge.py b/letsencrypt/client/challenge.py index 0ddd6ab51..ce85516eb 100644 --- a/letsencrypt/client/challenge.py +++ b/letsencrypt/client/challenge.py @@ -1,12 +1,13 @@ -from letsencrypt.client import logger - - class Challenge(object): + def __init__(self, configurator): self.config = configurator + def perform(self, quiet=True): - logger.error("Error - base class challenge.perform()") + raise NotImplementedError() + def generate_response(self): - logger.error("Error - base class challenge.generate_response()") - def clean(self): - logger.error("Error - base class challenge.clean()") + raise NotImplementedError() + + def cleanup(self): + raise NotImplementedError() From 0fcb2a056f7878c4fa190bf5cee2d0bbdf4d87f7 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 21 Nov 2014 22:21:44 +0100 Subject: [PATCH 09/21] Clean recovery_token_challenge module. --- letsencrypt/client/recovery_token_challenge.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/letsencrypt/client/recovery_token_challenge.py b/letsencrypt/client/recovery_token_challenge.py index 4b1206541..06eeb0283 100644 --- a/letsencrypt/client/recovery_token_challenge.py +++ b/letsencrypt/client/recovery_token_challenge.py @@ -5,12 +5,13 @@ from letsencrypt.client import challenge class RecoveryToken(challenge.Challenge): - def __init__(self): + def __init__(self, configurator): + super(RecoveryToken, self).__init__(configurator) self.token = "" - def perform(self, quiet = True): - - cancel, self.token = dialog.generic_input("Please Input Recovery Token: ") + def perform(self, quiet=True): + cancel, self.token = dialog.generic_input( + "Please Input Recovery Token: ") if cancel == 1: return False @@ -20,4 +21,4 @@ class RecoveryToken(challenge.Challenge): pass def generate_response(self): - return {"type":"recoveryToken", "token":self.token} + return {"type": "recoveryToken", "token": self.token} From b5462f8f8822c39a482fe29490bee87638759687 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 21 Nov 2014 22:26:51 +0100 Subject: [PATCH 10/21] Clean acme module. --- letsencrypt/client/acme.py | 40 ++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/letsencrypt/client/acme.py b/letsencrypt/client/acme.py index e8ffeeb74..4812aa10d 100644 --- a/letsencrypt/client/acme.py +++ b/letsencrypt/client/acme.py @@ -5,28 +5,44 @@ import pkg_resources import jsonschema -schemata = {schema: json.load(open(pkg_resources.resource_filename( - __name__, "schemata/%s.json" % schema))) for schema in [ - "authorization", "authorizationRequest", "certificate", "certificateRequest", - "challenge", "challengeRequest", "defer", "error", "revocation", - "revocationRequest", "statusRequest"] +SCHEMATA = { + schema: json.load(open(pkg_resources.resource_filename( + __name__, "schemata/%s.json" % schema))) for schema in [ + "authorization", + "authorizationRequest", + "certificate", + "certificateRequest", + "challenge", + "challengeRequest", + "defer", + "error", + "revocation", + "revocationRequest", + "statusRequest" + ] } + def acme_object_validate(j): """Validate a JSON object against the ACME protocol using JSON Schema. + Success will return None; failure to validate will raise a jsonschema.ValidationError exception describing the reason that the - object could not be validated successfully.""" + object could not be validated successfully. + """ j = json.loads(j) if not isinstance(j, dict): raise jsonschema.ValidationError("this is not a dictionary object") if "type" not in j: raise jsonschema.ValidationError("missing type field") - if j["type"] not in schemata: + if j["type"] not in SCHEMATA: raise jsonschema.ValidationError("unknown type %s" % j["type"]) - jsonschema.validate(j, schemata[j["type"]]) + jsonschema.validate(j, SCHEMATA[j["type"]]) -def pretty(s): - """Return a pretty-printed version of any JSON string (useful when - printing out protocol messages for debugging purposes.""" - return json.dumps(json.loads(s), indent=4) + +def pretty(json_string): + """Return a pretty-printed version of any JSON string. + + Useful when printing out protocol messages for debugging purposes. + """ + return json.dumps(json.loads(json_string), indent=4) From 30c6c16fa8956a38ba16a8171a8b1829f0ba962a Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 21 Nov 2014 22:32:18 +0100 Subject: [PATCH 11/21] Clean CONFIG module, use os.path. --- letsencrypt/client/CONFIG.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/letsencrypt/client/CONFIG.py b/letsencrypt/client/CONFIG.py index bf8779c01..bccfc777f 100644 --- a/letsencrypt/client/CONFIG.py +++ b/letsencrypt/client/CONFIG.py @@ -1,3 +1,5 @@ +import os.path + # CA hostname # If you create your own server... change this line # Note: the server certificate must be trusted in order to avoid @@ -10,24 +12,24 @@ CONFIG_DIR = "/etc/letsencrypt/" # Working directory for letsencrypt WORK_DIR = "/var/lib/letsencrypt/" # Directory where configuration backups are stored -BACKUP_DIR = WORK_DIR + "backups/" +BACKUP_DIR = os.path.join(WORK_DIR, "backups/") # Replaces MODIFIED_FILES, directory where temp checkpoint is created -TEMP_CHECKPOINT_DIR = WORK_DIR + "temp_checkpoint/" +TEMP_CHECKPOINT_DIR = os.path.join(WORK_DIR, "temp_checkpoint/") # Directory used before a permanent checkpoint is finalized -IN_PROGRESS_DIR = BACKUP_DIR + "IN_PROGRESS/" +IN_PROGRESS_DIR = os.path.join(BACKUP_DIR, "IN_PROGRESS/") # Directory where all certificates/keys are stored - used for easy revocation -CERT_KEY_BACKUP = WORK_DIR + "keys-certs/" +CERT_KEY_BACKUP = os.path.join(WORK_DIR, "keys-certs/") # Where all keys should be stored -KEY_DIR = SERVER_ROOT + "ssl/" +KEY_DIR = os.path.join(SERVER_ROOT, "ssl/") # Certificate storage -CERT_DIR = SERVER_ROOT + "certs/" +CERT_DIR = os.path.join(SERVER_ROOT, "certs/") # Contains standard Apache SSL directives -OPTIONS_SSL_CONF = CONFIG_DIR + "options-ssl.conf" +OPTIONS_SSL_CONF = os.path.join(CONFIG_DIR, "options-ssl.conf") # Let's Encrypt SSL vhost configuration extension LE_VHOST_EXT = "-le-ssl.conf" # Temporary file for challenge virtual hosts -APACHE_CHALLENGE_CONF = CONFIG_DIR + "le_dvsni_cert_challenge.conf" +APACHE_CHALLENGE_CONF = os.path.join(CONFIG_DIR, "le_dvsni_cert_challenge.conf") # Byte size of S and Nonce S_SIZE = 32 @@ -37,13 +39,13 @@ NONCE_SIZE = 16 RSA_KEY_SIZE = 2048 # bits of hashcash to generate -difficulty = 23 +DIFFICULTY = 23 # Let's Encrypt cert and chain files CERT_PATH = CERT_DIR + "cert-letsencrypt.pem" CHAIN_PATH = CERT_DIR + "chain-letsencrypt.pem" -#Invalid Extension +# Invalid Extension INVALID_EXT = ".acme.invalid" # Challenge Preferences Dict for currently supported challenges @@ -56,4 +58,5 @@ EXCLUSIVE_CHALLENGES = [set(["dvsni", "simpleHttps"])] CONFIG_CHALLENGES = {"dvsni", "simpleHttps"} # Rewrite rule arguments used for redirections to https vhost -REWRITE_HTTPS_ARGS = ["^.*$", "https://%{SERVER_NAME}%{REQUEST_URI}", "[L,R=permanent]"] +REWRITE_HTTPS_ARGS = [ + "^.*$", "https://%{SERVER_NAME}%{REQUEST_URI}", "[L,R=permanent]"] From bc6f0b4b6b87460f3d77fabd35412d11c89343ce Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 21 Nov 2014 22:43:11 +0100 Subject: [PATCH 12/21] Clean configurator module. --- letsencrypt/client/configurator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/letsencrypt/client/configurator.py b/letsencrypt/client/configurator.py index ba6a8f5bd..b968956f4 100644 --- a/letsencrypt/client/configurator.py +++ b/letsencrypt/client/configurator.py @@ -10,7 +10,7 @@ class Configurator(object): and other challenges. """ - def deploy_cert(self, vhost, cert, key , cert_chain=None): + def deploy_cert(self, vhost, cert, key, cert_chain=None): raise NotImplementedError() def choose_virtual_host(self, name): @@ -22,7 +22,7 @@ class Configurator(object): raise NotImplementedError() def enable_redirect(self, ssl_vhost): - """Makes all traffic redirect to the given ssl_vhost (port 80 => 443).""" + """Redirect all traffic to the given ssl_vhost (port 80 => 443).""" raise NotImplementedError() def enable_hsts(self, ssl_vhost): @@ -64,7 +64,7 @@ class Configurator(object): """Reload the users original configuration files.""" raise NotImplementedError() - def rollback_checkpoints(self, rollback = 1): + def rollback_checkpoints(self, rollback=1): """Revert `rollback` number of configuration checkpoints.""" raise NotImplementedError() From e70c6cc65c3f4f519246a112c64deb5459736755 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 21 Nov 2014 23:40:01 +0100 Subject: [PATCH 13/21] Configurator restart quiet --- letsencrypt/client/configurator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/client/configurator.py b/letsencrypt/client/configurator.py index b968956f4..7bce85ea5 100644 --- a/letsencrypt/client/configurator.py +++ b/letsencrypt/client/configurator.py @@ -76,7 +76,7 @@ class Configurator(object): """Make sure the configuration is valid.""" raise NotImplementedError() - def restart(self): + def restart(self, quiet=False): """Restart or refresh the server content.""" raise NotImplementedError() From a449a8917e2f2afd3dd2d22fd495b3fa05a33bb1 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 21 Nov 2014 23:07:20 +0100 Subject: [PATCH 14/21] Clean crypto_util module. --- letsencrypt/client/crypto_util.py | 153 +++++++++++++++++------------- 1 file changed, 86 insertions(+), 67 deletions(-) diff --git a/letsencrypt/client/crypto_util.py b/letsencrypt/client/crypto_util.py index 875858351..815e795cd 100644 --- a/letsencrypt/client/crypto_util.py +++ b/letsencrypt/client/crypto_util.py @@ -14,41 +14,51 @@ from letsencrypt.client import le_util def b64_cert_to_pem(b64_der_cert): - x = M2Crypto.X509.load_cert_der_string(le_util.b64_url_dec(b64_der_cert)) - return x.as_pem() + return M2Crypto.X509.load_cert_der_string( + le_util.b64_url_dec(b64_der_cert)).as_pem() -def create_sig(msg, key_file, signer_nonce = None, signer_nonce_len = CONFIG.NONCE_SIZE): + +def create_sig(msg, key_file, signer_nonce=None, + signer_nonce_len=CONFIG.NONCE_SIZE): # DOES prepend signer_nonce to message # TODO: Change this over to M2Crypto... PKey - # Protect against crypto unicode errors... is this sufficient? Do I need to escape? + # Protect against crypto unicode errors... is this sufficient? + # Do I need to escape? msg = str(msg) key = Crypto.PublicKey.RSA.importKey(open(key_file).read()) if signer_nonce is None: signer_nonce = Random.get_random_bytes(signer_nonce_len) - h = Crypto.Hash.SHA256.new(signer_nonce + msg) + hashed = Crypto.Hash.SHA256.new(signer_nonce + msg) signer = Crypto.Signature.PKCS1_v1_5.new(key) - signature = signer.sign(h) + signature = signer.sign(hashed) #print "signing:", signer_nonce + msg #print "signature:", signature - n, e = key.n, key.e - n_bytes = binascii.unhexlify(leading_zeros(hex(n)[2:].replace("L", ""))) - e_bytes = binascii.unhexlify(leading_zeros(hex(e)[2:].replace("L", ""))) + n_bytes = binascii.unhexlify(leading_zeros(hex(key.n)[2:].replace("L", ""))) + e_bytes = binascii.unhexlify(leading_zeros(hex(key.e)[2:].replace("L", ""))) n_encoded = le_util.b64_url_enc(n_bytes) e_encoded = le_util.b64_url_enc(e_bytes) signer_nonce_encoded = le_util.b64_url_enc(signer_nonce) sig_encoded = le_util.b64_url_enc(signature) - jwk = { "kty": "RSA", "n": n_encoded, "e": e_encoded } - signature = { "nonce": signer_nonce_encoded, "alg": "RS256", "jwk": jwk, "sig": sig_encoded } + jwk = {"kty": "RSA", "n": n_encoded, "e": e_encoded} + signature = { + "nonce": signer_nonce_encoded, + "alg": "RS256", + "jwk": jwk, + "sig": sig_encoded + } # return json.dumps(signature) - return (signature) + return signature -def leading_zeros(s): - if len(s) % 2: - return "0" + s - return s -def sha256(m): - return hashlib.sha256(m).hexdigest() +def leading_zeros(arg): + if len(arg) % 2: + return "0" + arg + return arg + + +def sha256(arg): + return hashlib.sha256(arg).hexdigest() + # based on M2Crypto unit test written by Toby Allsopp def make_key(bits=CONFIG.RSA_KEY_SIZE): @@ -70,12 +80,12 @@ def make_csr(key_file, domains): """ assert domains, "Must provide one or more hostnames for the CSR." rsa_key = M2Crypto.RSA.load_key(key_file) - pk = M2Crypto.EVP.PKey() - pk.assign_rsa(rsa_key) + pubkey = M2Crypto.EVP.PKey() + pubkey.assign_rsa(rsa_key) - x = M2Crypto.X509.Request() - x.set_pubkey(pk) - name = x.get_subject() + csr = M2Crypto.X509.Request() + csr.set_pubkey(pubkey) + name = csr.get_subject() name.C = "US" name.ST = "Michigan" name.L = "Ann Arbor" @@ -84,71 +94,80 @@ def make_csr(key_file, domains): name.CN = domains[0] extstack = M2Crypto.X509.X509_Extension_Stack() - ext = M2Crypto.X509.new_extension('subjectAltName', ", ".join(["DNS:%s" % d for d in domains])) + ext = M2Crypto.X509.new_extension( + 'subjectAltName', ", ".join(["DNS:%s" % d for d in domains])) extstack.push(ext) - x.add_extensions(extstack) - x.sign(pk,'sha256') - assert x.verify(pk) - pk2 = x.get_pubkey() - assert x.verify(pk2) - return x.as_pem(), x.as_der() + csr.add_extensions(extstack) + csr.sign(pubkey, 'sha256') + assert csr.verify(pubkey) + pubkey2 = csr.get_pubkey() + assert csr.verify(pubkey2) + return csr.as_pem(), csr.as_der() + def make_ss_cert(key_file, domains): - """ - Returns new self-signed cert in PEM form using key_file containing all domains + """Returns new self-signed cert in PEM form. + + Uses key_file and contains all domains. """ assert domains, "Must provide one or more hostnames for the CSR." rsa_key = M2Crypto.RSA.load_key(key_file) - pk = M2Crypto.EVP.PKey() - pk.assign_rsa(rsa_key) + pubkey = M2Crypto.EVP.PKey() + pubkey.assign_rsa(rsa_key) - x = M2Crypto.X509.X509() - x.set_pubkey(pk) - x.set_serial_number(1337) - x.set_version(2) + cert = M2Crypto.X509.X509() + cert.set_pubkey(pubkey) + cert.set_serial_number(1337) + cert.set_version(2) - t = long(time.time()) + current_ts = long(time.time()) current = M2Crypto.ASN1.ASN1_UTCTIME() - current.set_time(t) + current.set_time(current_ts) expire = M2Crypto.ASN1.ASN1_UTCTIME() - expire.set_time((7 * 24 * 60 * 60) + t) - x.set_not_before(current) - x.set_not_after(expire) + expire.set_time((7 * 24 * 60 * 60) + current_ts) + cert.set_not_before(current) + cert.set_not_after(expire) - name = x.get_subject() + name = cert.get_subject() name.C = "US" name.ST = "Michigan" name.L = "Ann Arbor" name.O = "University of Michigan and the EFF" name.CN = domains[0] - x.set_issuer(x.get_subject()) + cert.set_issuer(cert.get_subject()) - x.add_ext(M2Crypto.X509.new_extension('basicConstraints', 'CA:FALSE')) - #x.add_ext(M2Crypto.X509.new_extension('extendedKeyUsage', 'TLS Web Server Authentication')) - x.add_ext(M2Crypto.X509.new_extension('subjectAltName', ", ".join(["DNS:%s" % d for d in domains]))) + cert.add_ext(M2Crypto.X509.new_extension('basicConstraints', 'CA:FALSE')) + #cert.add_ext(M2Crypto.X509.new_extension( + # 'extendedKeyUsage', 'TLS Web Server Authentication')) + cert.add_ext(M2Crypto.X509.new_extension( + 'subjectAltName', ", ".join(["DNS:%s" % d for d in domains]))) - x.sign(pk, 'sha256') - assert x.verify(pk) - assert x.verify() + cert.sign(pubkey, 'sha256') + assert cert.verify(pubkey) + assert cert.verify() #print check_purpose(,0 - return x.as_pem() + return cert.as_pem() + def get_cert_info(filename): - d = {} # M2Crypto Library only supports RSA right now - x = M2Crypto.X509.load_cert(filename) - d["not_before"] = x.get_not_before().get_datetime() - d["not_after"] = x.get_not_after().get_datetime() - d["subject"] = x.get_subject().as_text() - d["cn"] = x.get_subject().CN - d["issuer"] = x.get_issuer().as_text() - d["fingerprint"] = x.get_fingerprint(md='sha1') - try: - d["san"] = x.get_ext("subjectAltName").get_value() - except: - d["san"] = "" + cert = M2Crypto.X509.load_cert(filename) + + try: + san = cert.get_ext("subjectAltName").get_value() + except: + san = "" + + return { + "not_before": cert.get_not_before().get_datetime(), + "not_after": cert.get_not_after().get_datetime(), + "subject": cert.get_subject().as_text(), + "cn": cert.get_subject().CN, + "issuer": cert.get_issuer().as_text(), + "fingerprint": cert.get_fingerprint(md='sha1'), + "san": san, + "serial": cert.get_serial_number(), + "pub_key": "RSA " + str(cert.get_pubkey().size() * 8), + } - d["serial"] = x.get_serial_number() - d["pub_key"] = "RSA " + str(x.get_pubkey().size() * 8) - return d From 6b7d0eaa9e824c37de6b4ec081a4519a02e1ca50 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 21 Nov 2014 23:15:28 +0100 Subject: [PATCH 15/21] Clean nginx_configurator module. --- letsencrypt/client/nginx_configurator.py | 64 +++++++++--------------- 1 file changed, 24 insertions(+), 40 deletions(-) diff --git a/letsencrypt/client/nginx_configurator.py b/letsencrypt/client/nginx_configurator.py index 887180aed..70d775a8b 100644 --- a/letsencrypt/client/nginx_configurator.py +++ b/letsencrypt/client/nginx_configurator.py @@ -19,8 +19,9 @@ from letsencrypt.client import augeas_configurator # self.names.append(name) class NginxConfigurator(augeas_configurator.AugeasConfigurator): - + def __init__(self, server_root=CONFIG.SERVER_ROOT): + super(NginxConfigurator, self).__init__() self.server_root = server_root # See if any temporary changes need to be recovered @@ -31,27 +32,16 @@ class NginxConfigurator(augeas_configurator.AugeasConfigurator): # Check for errors in parsing files with Augeas # TODO - insert nginx lens info here??? #self.check_parsing_errors("httpd.aug") - def deploy_cert(self, vhost, cert, key, cert_chain=None): - """ - Deploy cert in nginx - """ - return + """Deploy cert in nginx""" def choose_virtual_host(self, name): - """ - Chooses a virtual host based on the given domain name - """ - return None + """Chooses a virtual host based on the given domain name""" def get_all_names(self): - """ - Returns all names found in the nginx configuration - """ - all_names = set() - - return all_names + """Returns all names found in the nginx configuration""" + return set() # Might be helpful... I know nothing about nginx lens # def get_include_path(self, cur_dir, arg): @@ -72,7 +62,7 @@ class NginxConfigurator(augeas_configurator.AugeasConfigurator): # # would create a race condition between the check and this input # # TODO: Fix this - # # Check to make sure only expected characters are used <- maybe remove + # # Check to make sure only expected characters are used, maybe remove # # validChars = re.compile("[a-zA-Z0-9.*?_-/]*") # # matchObj = validChars.match(arg) # # if matchObj.group() != arg: @@ -87,7 +77,8 @@ class NginxConfigurator(augeas_configurator.AugeasConfigurator): # arg = self.server_root + arg[5:] # # TODO: Test if Apache allows ../ or ~/ for Includes - # # Attempts to add a transform to the file if one does not already exist + # # Attempts to add a transform to the file if one does not already + # # exist # self.parse_file(arg) # # Argument represents an fnmatch regular expression, convert it @@ -100,8 +91,9 @@ class NginxConfigurator(augeas_configurator.AugeasConfigurator): # # * and ? are the two special fnmatch characters # if "*" in split or "?" in split: # # Turn it into a augeas regex - # # TODO: Can this instead be an augeas glob instead of regex - # splitArg[idx] = "* [label()=~regexp('%s')]" % self.fnmatch_to_re(split) + # # TODO: Can this be an augeas glob instead of regex + # splitArg[idx] = ("* [label()=~regexp('%s')]" % + # self.fnmatch_to_re(split) # # Reassemble the argument # arg = "/".join(splitArg) @@ -110,7 +102,6 @@ class NginxConfigurator(augeas_configurator.AugeasConfigurator): # return "/files" + arg[:len(arg)-1] # return "/files"+arg - def enable_redirect(self, ssl_vhost): """ Adds Redirect directive to the port 80 equivalent of ssl_vhost @@ -120,7 +111,6 @@ class NginxConfigurator(augeas_configurator.AugeasConfigurator): """ return - def enable_ocsp_stapling(self, ssl_vhost): return False @@ -156,9 +146,7 @@ class NginxConfigurator(augeas_configurator.AugeasConfigurator): # return avail_fp def enable_site(self, vhost): - """ - Enables an available site, Apache restart required - """ + """Enables an available site, Apache restart required""" return False # Might be a usefule reference @@ -169,31 +157,29 @@ class NginxConfigurator(augeas_configurator.AugeasConfigurator): # """ # # Test if augeas included file for Httpd.lens # # Note: This works for augeas globs, ie. *.conf - # incTest = self.aug.match("/augeas/load/Httpd/incl [. ='" + file_path + "']") + # incTest = self.aug.match( + # "/augeas/load/Httpd/incl [. ='" + file_path + "']") # if not incTest: # # Load up files # #self.httpd_incl.append(file_path) - # #self.aug.add_transform("Httpd.lns", self.httpd_incl, None, self.httpd_excl) + # #self.aug.add_transform( + # # "Httpd.lns", self.httpd_incl, None, self.httpd_excl) # self.__add_httpd_transform(file_path) # self.aug.load() - # Helpful reference? # def verify_setup(self): - # ''' - # Make sure that files/directories are setup with appropriate permissions - # Aim for defensive coding... make sure all input files + # """ + # Make sure that files/directories are setup with appropriate + # permissions. Aim for defensive coding... make sure all input files # have permissions of root - # ''' + # """ # le_util.make_or_verify_dir(CONFIG.CONFIG_DIR, 0755) # le_util.make_or_verify_dir(CONFIG.WORK_DIR, 0755) # le_util.make_or_verify_dir(CONFIG.BACKUP_DIR, 0755) - + def restart(self, quiet=False): - """ - Restarts nginx server - """ - return + """Restarts nginx server""" # May be of use? # def __add_httpd_transform(self, incl): @@ -206,12 +192,10 @@ class NginxConfigurator(augeas_configurator.AugeasConfigurator): # self.aug.set("/augeas/load/Httpd/incl[last()]", incl) def config_test(self): - """ Check Configuration """ + """Check Configuration""" return False - - def main(): return From 9ff88c1c987a02ca2f00aef3cd73306c408e629a Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 21 Nov 2014 23:25:25 +0100 Subject: [PATCH 16/21] Clean display module. --- letsencrypt/client/display.py | 93 ++++++++++++++++++++--------------- 1 file changed, 53 insertions(+), 40 deletions(-) diff --git a/letsencrypt/client/display.py b/letsencrypt/client/display.py index d04f3785f..b42aad6b4 100644 --- a/letsencrypt/client/display.py +++ b/letsencrypt/client/display.py @@ -6,28 +6,31 @@ import dialog WIDTH = 72 HEIGHT = 20 + class SingletonD(object): _instance = None + def __new__(cls, *args, **kwargs): if not cls._instance: cls._instance = super(SingletonD, cls).__new__( - cls, *args, **kwargs) + cls, *args, **kwargs) return cls._instance class Display(SingletonD): """Generic display.""" - def generic_notification(self, message, width = WIDTH, height = HEIGHT): + def generic_notification(self, message, width=WIDTH, height=HEIGHT): raise NotImplementedError() - def generic_menu(self, message, choices, input_text = "", width = WIDTH, height = HEIGHT): + def generic_menu(self, message, choices, input_text="", + width=WIDTH, height=HEIGHT): raise NotImplementedError() def generic_input(self, message): raise NotImplementedError() - def generic_yesno(self, message, yes_label = "Yes", no_label = "No"): + def generic_yesno(self, message, yes_label="Yes", no_label="No"): raise NotImplementedError() def filter_names(self, names): @@ -46,9 +49,9 @@ class Display(SingletonD): raise NotImplementedError() def gen_https_names(self, domains): - """ - Returns a string of the domains formatted nicely with https:// prepended - to each + """Returns a string of the https domains. + + Domains are formatted nicely with https:// prepended to each. """ result = "" if len(domains) > 2: @@ -86,28 +89,28 @@ class NcursesDisplay(Display): def __init__(self): self.d = dialog.Dialog() - def generic_notification(self, message, w = WIDTH, h = HEIGHT): - self.d.msgbox(message, width = w, height = h) + def generic_notification(self, message, w=WIDTH, h=HEIGHT): + self.d.msgbox(message, width=w, height=h) - def generic_menu(self, message, choices, input_text = "", width = WIDTH, - height = HEIGHT): + def generic_menu(self, message, choices, input_text="", width=WIDTH, + height=HEIGHT): # Can accept either tuples or just the actual choices if choices and isinstance(choices[0], tuple): - c, selection = self.d.menu(message, choices = choices, - width = WIDTH, height = HEIGHT) + c, selection = self.d.menu( + message, choices=choices, width=WIDTH, height=HEIGHT) return c, str(selection) else: choices = list(enumerate(choices, 1)) - code, s = self.d.menu(message, choices = choices, - width = WIDTH, height = HEIGHT) + code, s = self.d.menu( + message, choices=choices, width=WIDTH, height=HEIGHT) - return code (int(s) - 1) + return code(int(s) - 1) def generic_input(self, message): return self.d.inputbox(message) - def generic_yesno(self, message, yes = "Yes", no = "No"): - a = self.d.yesno(message, HEIGHT, WIDTH, yes_label = yes, no_label = no) + def generic_yesno(self, message, yes="Yes", no="No"): + a = self.d.yesno(message, HEIGHT, WIDTH, yes_label=yes, no_label=no) return a == self.d.DIALOG_OK @@ -118,24 +121,23 @@ class NcursesDisplay(Display): return c, [str(s) for s in names] - def success_installation(self, domains): self.d.msgbox("\nCongratulations! You have successfully enabled " + self.gen_https_names(domains) + "!", width=WIDTH) def display_certs(self, certs): list_choices = [ - (str(i+1), - "%s | %s | %s" % - (str(c["cn"].ljust(WIDTH - 39)), - c["not_before"].strftime("%m-%d-%y"), - "Installed" if c["installed"] else "")) + (str(i+1), "%s | %s | %s" % + (str(c["cn"].ljust(WIDTH - 39)), + c["not_before"].strftime("%m-%d-%y"), + "Installed" if c["installed"] else "")) for i, c in enumerate(certs)] - code, s = self.d.menu("Which certificates would you like to revoke?", - choices = list_choices, help_button=True, - help_label="More Info", ok_label="Revoke", - width=WIDTH, height=HEIGHT) + code, s = self.d.menu( + "Which certificates would you like to revoke?", + choices=list_choices, help_button=True, + help_label="More Info", ok_label="Revoke", + width=WIDTH, height=HEIGHT) if not s: s = -1 return code, (int(s) - 1) @@ -160,14 +162,15 @@ class FileDisplay(Display): def __init__(self, outfile): self.outfile = outfile - def generic_notification(self, message, width = WIDTH, height = HEIGHT): + def generic_notification(self, message, width=WIDTH, height=HEIGHT): side_frame = '-' * (79) wm = textwrap.fill(message, 80) text = "\n%s\n%s\n%s\n" % (side_frame, wm, side_frame) self.outfile.write(text) raw_input("Press Enter to Continue") - def generic_menu(self, message, choices, input_text = "", width = WIDTH, height = HEIGHT): + def generic_menu(self, message, choices, input_text="", + width=WIDTH, height=HEIGHT): # Can take either tuples or single items in choices list if choices and isinstance(choices[0], tuple): choices = ["%s - %s" % (c[0], c[1]) for c in choices] @@ -182,7 +185,8 @@ class FileDisplay(Display): self.outfile.write("%s\n" % side_frame) - code, selection = self.__get_valid_int_ans("%s (c to cancel): " % input_text) + code, selection = self.__get_valid_int_ans( + "%s (c to cancel): " % input_text) return code, (selection - 1) @@ -194,7 +198,7 @@ class FileDisplay(Display): else: return OK, ans - def generic_yesno(self, message, yes_label = "Yes", no_label = "No"): + def generic_yesno(self, message, yes_label="Yes", no_label="No"): self.outfile.write("\n%s\n" % textwrap.fill(message, 80)) ans = raw_input("y/n: ") return ans.startswith('y') or ans.startswith('Y') @@ -235,7 +239,7 @@ class FileDisplay(Display): else: try: selection = int(ans) - #TODO add check to make sure it is liess than max + # TODO add check to make sure it is liess than max if selection < 0: self.outfile.write(e_msg) continue @@ -246,7 +250,6 @@ class FileDisplay(Display): return code, selection - def success_installation(self, domains): s_f = '*' * (79) wm = textwrap.fill(("Congratulations! You have successfully " + @@ -258,7 +261,7 @@ class FileDisplay(Display): self.outfile.write("Are you sure you would like to revoke \ the following certificate:\n") self.outfile.write(self.cert_info_frame(cert)) - self.outfile("This action cannot be reversed!\n"); + self.outfile("This action cannot be reversed!\n") ans = raw_input("y/n") return ans.startswith('y') or ans.startswith('Y') @@ -272,38 +275,47 @@ CANCEL = "cancel" HELP = "help" - def setDisplay(display_inst): global display display = display_inst -def generic_notification(message, width = WIDTH, height = HEIGHT): + +def generic_notification(message, width=WIDTH, height=HEIGHT): display.generic_notification(message, width, height) -def generic_menu(message, choices, input_text = "", width = WIDTH, height = HEIGHT): + +def generic_menu(message, choices, input_text="", width=WIDTH, height=HEIGHT): return display.generic_menu(message, choices, input_text, width, height) + def generic_input(message): return display.generic_message(message) -def generic_yesno(message, yes_label = "Yes", no_label = "No"): + +def generic_yesno(message, yes_label="Yes", no_label="No"): return display.generic_yesno(message, yes_label, no_label) + def filter_names(names): return display.filter_names(names) + def display_certs(certs): return display.display_certs(certs) + def cert_info_string(cert): return display.cert_info_string(cert) + def gen_https_names(domains): return display.gen_https_names(domains) + def success_installation(domains): return display.success_installation(domains) + def redirect_by_default(): choices = [ ("Easy", "Allow both HTTP and HTTPS access to these sites"), @@ -313,7 +325,7 @@ def redirect_by_default(): "is required or optional.", choices, "Please enter the appropriate number", - width = WIDTH) + width=WIDTH) if result[0] != OK: return False @@ -325,5 +337,6 @@ def redirect_by_default(): def confirm_revocation(cert): return display.confirm_revocation(cert) + def more_info_cert(cert): return display.more_info_cert(cert) From 6032eb639313b72112900017035ecd86e89f4b00 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 21 Nov 2014 23:44:36 +0100 Subject: [PATCH 17/21] setDisplay -> set_display --- letsencrypt/client/display.py | 2 +- letsencrypt/scripts/main.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/letsencrypt/client/display.py b/letsencrypt/client/display.py index b42aad6b4..766191061 100644 --- a/letsencrypt/client/display.py +++ b/letsencrypt/client/display.py @@ -275,7 +275,7 @@ CANCEL = "cancel" HELP = "help" -def setDisplay(display_inst): +def set_display(display_inst): global display display = display_inst diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index fa2166127..22236cc6d 100755 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -78,9 +78,9 @@ def main(): continue if curses: - display.setDisplay(display.NcursesDisplay()) + display.set_display(display.NcursesDisplay()) else: - display.setDisplay(display.FileDisplay(sys.stdout)) + display.set_display(display.FileDisplay(sys.stdout)) if not server: server = CONFIG.ACME_SERVER From 233804aec206db70289021910dd1e70db6e6e5e2 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 21 Nov 2014 23:47:49 +0100 Subject: [PATCH 18/21] gen_https method -> function (no-self-use) --- letsencrypt/client/display.py | 37 ++++++++++++++++------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/letsencrypt/client/display.py b/letsencrypt/client/display.py index 766191061..acbafbf23 100644 --- a/letsencrypt/client/display.py +++ b/letsencrypt/client/display.py @@ -48,23 +48,6 @@ class Display(SingletonD): def more_info_cert(self, cert): raise NotImplementedError() - def gen_https_names(self, domains): - """Returns a string of the https domains. - - Domains are formatted nicely with https:// prepended to each. - """ - result = "" - if len(domains) > 2: - for i in range(len(domains)-1): - result = result + "https://" + domains[i] + ", " - result = result + "and " - if len(domains) == 2: - return "https://" + domains[0] + " and https://" + domains[1] - if domains: - result = result + "https://" + domains[len(domains)-1] - - return result - def cert_info_frame(self, cert): text = "-" * (WIDTH - 4) + "\n" text += self.cert_info_string(cert) @@ -123,7 +106,7 @@ class NcursesDisplay(Display): def success_installation(self, domains): self.d.msgbox("\nCongratulations! You have successfully enabled " - + self.gen_https_names(domains) + "!", width=WIDTH) + + gen_https_names(domains) + "!", width=WIDTH) def display_certs(self, certs): list_choices = [ @@ -253,7 +236,7 @@ class FileDisplay(Display): def success_installation(self, domains): s_f = '*' * (79) wm = textwrap.fill(("Congratulations! You have successfully " + - "enabled %s!") % self.gen_https_names(domains)) + "enabled %s!") % gen_https_names(domains)) msg = "%s\n%s\n%s\n" self.outfile.write(msg % (s_f, wm, s_f)) @@ -309,7 +292,21 @@ def cert_info_string(cert): def gen_https_names(domains): - return display.gen_https_names(domains) + """Returns a string of the https domains. + + Domains are formatted nicely with https:// prepended to each. + """ + result = "" + if len(domains) > 2: + for i in range(len(domains)-1): + result = result + "https://" + domains[i] + ", " + result = result + "and " + if len(domains) == 2: + return "https://" + domains[0] + " and https://" + domains[1] + if domains: + result = result + "https://" + domains[len(domains)-1] + + return result def success_installation(domains): From bc294cbe2c4a158da87c4eadbf0d4a4bf5d62896 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 21 Nov 2014 23:51:00 +0100 Subject: [PATCH 19/21] cert_info_{frame,string} method -> function (no-self-use) --- letsencrypt/client/display.py | 44 +++++++++++++++++------------------ 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/letsencrypt/client/display.py b/letsencrypt/client/display.py index acbafbf23..0a56b658e 100644 --- a/letsencrypt/client/display.py +++ b/letsencrypt/client/display.py @@ -48,24 +48,6 @@ class Display(SingletonD): def more_info_cert(self, cert): raise NotImplementedError() - def cert_info_frame(self, cert): - text = "-" * (WIDTH - 4) + "\n" - text += self.cert_info_string(cert) - text += "-" * (WIDTH - 4) - return text - - def cert_info_string(self, cert): - text = "Subject: %s\n" % cert["subject"] - text += "SAN: %s\n" % cert["san"] - text += "Issuer: %s\n" % cert["issuer"] - text += "Public Key: %s\n" % cert["pub_key"] - text += "Not Before: %s\n" % str(cert["not_before"]) - text += "Not After: %s\n" % str(cert["not_after"]) - text += "Serial Number: %s\n" % cert["serial"] - text += "SHA1: %s\n" % cert["fingerprint"] - text += "Installed: %s\n" % cert["installed"] - return text - class NcursesDisplay(Display): @@ -128,14 +110,14 @@ class NcursesDisplay(Display): def confirm_revocation(self, cert): text = "Are you sure you would like to revoke the following \ certificate:\n" - text += self.cert_info_frame(cert) + text += cert_info_frame(cert) text += "This action cannot be reversed!" a = self.d.yesno(text, width=WIDTH, height=HEIGHT) return a == self.d.DIALOG_OK def more_info_cert(self, cert): text = "Certificate Information:\n" - text += self.cert_info_frame(cert) + text += cert_info_frame(cert) print text self.d.msgbox(text, width=WIDTH, height=HEIGHT) @@ -243,14 +225,14 @@ class FileDisplay(Display): def confirm_revocation(self, cert): self.outfile.write("Are you sure you would like to revoke \ the following certificate:\n") - self.outfile.write(self.cert_info_frame(cert)) + self.outfile.write(cert_info_frame(cert)) self.outfile("This action cannot be reversed!\n") ans = raw_input("y/n") return ans.startswith('y') or ans.startswith('Y') def more_info_cert(self, cert): self.outfile.write("\nCertificate Information:\n") - self.outfile.write(self.cert_info_frame(cert)) + self.outfile.write(cert_info_frame(cert)) display = None OK = "ok" @@ -287,8 +269,24 @@ def display_certs(certs): return display.display_certs(certs) +def cert_info_frame(cert): + text = "-" * (WIDTH - 4) + "\n" + text += cert_info_string(cert) + text += "-" * (WIDTH - 4) + return text + + def cert_info_string(cert): - return display.cert_info_string(cert) + text = "Subject: %s\n" % cert["subject"] + text += "SAN: %s\n" % cert["san"] + text += "Issuer: %s\n" % cert["issuer"] + text += "Public Key: %s\n" % cert["pub_key"] + text += "Not Before: %s\n" % str(cert["not_before"]) + text += "Not After: %s\n" % str(cert["not_after"]) + text += "Serial Number: %s\n" % cert["serial"] + text += "SHA1: %s\n" % cert["fingerprint"] + text += "Installed: %s\n" % cert["installed"] + return text def gen_https_names(domains): From 3dbfa9b4cd8a14b2303587b16295d98451b4e439 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 22 Nov 2014 00:13:09 +0100 Subject: [PATCH 20/21] More display module cleanups. --- letsencrypt/client/display.py | 86 +++++++++++++++++------------------ 1 file changed, 42 insertions(+), 44 deletions(-) diff --git a/letsencrypt/client/display.py b/letsencrypt/client/display.py index 0a56b658e..ac1f9d819 100644 --- a/letsencrypt/client/display.py +++ b/letsencrypt/client/display.py @@ -52,74 +52,74 @@ class Display(SingletonD): class NcursesDisplay(Display): def __init__(self): - self.d = dialog.Dialog() + self.dialog = dialog.Dialog() def generic_notification(self, message, w=WIDTH, h=HEIGHT): - self.d.msgbox(message, width=w, height=h) + self.dialog.msgbox(message, width=w, height=h) def generic_menu(self, message, choices, input_text="", width=WIDTH, height=HEIGHT): # Can accept either tuples or just the actual choices if choices and isinstance(choices[0], tuple): - c, selection = self.d.menu( + code, selection = self.dialog.menu( message, choices=choices, width=WIDTH, height=HEIGHT) - return c, str(selection) + return code, str(selection) else: choices = list(enumerate(choices, 1)) - code, s = self.d.menu( + code, tag = self.dialog.menu( message, choices=choices, width=WIDTH, height=HEIGHT) - return code(int(s) - 1) + return code(int(tag) - 1) def generic_input(self, message): - return self.d.inputbox(message) + return self.dialog.inputbox(message) def generic_yesno(self, message, yes="Yes", no="No"): - a = self.d.yesno(message, HEIGHT, WIDTH, yes_label=yes, no_label=no) - - return a == self.d.DIALOG_OK + return self.dialog.DIALOG_OK == self.dialog.yesno( + message, HEIGHT, WIDTH, yes_label=yes, no_label=no) def filter_names(self, names): choices = [(n, "", 0) for n in names] - c, names = self.d.checklist("Which names would you like to activate \ - HTTPS for?", choices=choices) - - return c, [str(s) for s in names] + code, names = self.dialog.checklist( + "Which names would you like to activate HTTPS for?", + choices=choices) + return code, [str(s) for s in names] def success_installation(self, domains): - self.d.msgbox("\nCongratulations! You have successfully enabled " - + gen_https_names(domains) + "!", width=WIDTH) + self.dialog.msgbox( + "\nCongratulations! You have successfully enabled " + + gen_https_names(domains) + "!", width=WIDTH) def display_certs(self, certs): list_choices = [ (str(i+1), "%s | %s | %s" % - (str(c["cn"].ljust(WIDTH - 39)), - c["not_before"].strftime("%m-%d-%y"), - "Installed" if c["installed"] else "")) + (str(c["cn"].ljust(WIDTH - 39)), + c["not_before"].strftime("%m-%d-%y"), + "Installed" if c["installed"] else "")) for i, c in enumerate(certs)] - code, s = self.d.menu( + code, tag = self.dialog.menu( "Which certificates would you like to revoke?", choices=list_choices, help_button=True, help_label="More Info", ok_label="Revoke", width=WIDTH, height=HEIGHT) - if not s: - s = -1 - return code, (int(s) - 1) + if not tag: + tag = -1 + return code, (int(tag) - 1) def confirm_revocation(self, cert): - text = "Are you sure you would like to revoke the following \ - certificate:\n" + text = ("Are you sure you would like to revoke the following " + "certificate:\n") text += cert_info_frame(cert) text += "This action cannot be reversed!" - a = self.d.yesno(text, width=WIDTH, height=HEIGHT) - return a == self.d.DIALOG_OK + return self.dialog.DIALOG_OK == self.dialog.yesno( + text, width=WIDTH, height=HEIGHT) def more_info_cert(self, cert): text = "Certificate Information:\n" text += cert_info_frame(cert) print text - self.d.msgbox(text, width=WIDTH, height=HEIGHT) + self.dialog.msgbox(text, width=WIDTH, height=HEIGHT) class FileDisplay(Display): @@ -144,9 +144,9 @@ class FileDisplay(Display): side_frame = '-' * (79) self.outfile.write("%s\n" % side_frame) - for i, c in enumerate(choices): - wc = textwrap.fill("%d: %s" % (i + 1, c), 80) - self.outfile.write("%s\n" % wc) + for i, choice in enumerate(choices, 1): + self.outfile.write(textwrap.fill( + "%d: %s" % (i, choice), 80) + '\n') self.outfile.write("%s\n" % side_frame) @@ -169,13 +169,12 @@ class FileDisplay(Display): return ans.startswith('y') or ans.startswith('Y') def filter_names(self, names): - c, s = self.generic_menu( + code, tag = self.generic_menu( "Choose the names would you like to upgrade to HTTPS?", - names, - "Select the number of the name: ") + names, "Select the number of the name: ") # Make sure to return a list... - return c, [names[s]] + return code, [names[tag]] def display_certs(self, certs): menu_choices = [(str(i+1), str(c["cn"]) + " - " + c["pub_key"] + @@ -183,12 +182,11 @@ class FileDisplay(Display): for i, c in enumerate(certs)] self.outfile.write("Which certificate would you like to revoke?\n") - for c in menu_choices: - wm = textwrap.fill("%s: %s - %s Signed (UTC): %s\n" % - (c[0], c[1], c[2], c[3])) - self.outfile.write(wm) + for choice in menu_choices: + self.outfile.write(textwrap.fill( + "%s: %s - %s Signed (UTC): %s\n" % choice[:4])) - return (self.__get_valid_int_ans("Revoke Number (c to cancel): ") - 1) + return self.__get_valid_int_ans("Revoke Number (c to cancel): ") - 1 def __get_valid_int_ans(self, input_string): valid_ans = False @@ -218,13 +216,13 @@ class FileDisplay(Display): def success_installation(self, domains): s_f = '*' * (79) wm = textwrap.fill(("Congratulations! You have successfully " + - "enabled %s!") % gen_https_names(domains)) + "enabled %s!") % gen_https_names(domains)) msg = "%s\n%s\n%s\n" self.outfile.write(msg % (s_f, wm, s_f)) def confirm_revocation(self, cert): - self.outfile.write("Are you sure you would like to revoke \ - the following certificate:\n") + self.outfile.write("Are you sure you would like to revoke " + "the following certificate:\n") self.outfile.write(cert_info_frame(cert)) self.outfile("This action cannot be reversed!\n") ans = raw_input("y/n") @@ -326,7 +324,7 @@ def redirect_by_default(): return False # different answer for each type of display - return (str(result[1]) == "Secure" or result[1] == 1) + return str(result[1]) == "Secure" or result[1] == 1 def confirm_revocation(cert): From 6fa555788ee41740dbeaba9e011523d7c1623d70 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 21 Nov 2014 23:37:14 +0100 Subject: [PATCH 21/21] Clean augeas_configurator module. --- letsencrypt/client/augeas_configurator.py | 86 +++++++++++++---------- 1 file changed, 47 insertions(+), 39 deletions(-) diff --git a/letsencrypt/client/augeas_configurator.py b/letsencrypt/client/augeas_configurator.py index d66b42234..bb96c8866 100644 --- a/letsencrypt/client/augeas_configurator.py +++ b/letsencrypt/client/augeas_configurator.py @@ -35,15 +35,17 @@ class AugeasConfigurator(configurator.Configurator): # As aug.get may return null if lens_path and lens in lens_path: # Strip off /augeas/files and /error - logger.error('There has been an error in parsing the file: %s' % e[13:len(e) - 6]) + logger.error('There has been an error in parsing the file: ' + '%s' % e[13:len(e) - 6]) logger.error(self.aug.get(e + '/message')) - def save(self, title=None, temporary=False): - """ - Saves all changes to the configuration files + """Saves all changes to the configuration files. + This function is not transactional - TODO: Instead rely on challenge to backup all files before modifications + + TODO: Instead rely on challenge to backup all files before + modifications title: string - The title of the save. If a title is given, the configuration will be saved as a new checkpoint @@ -66,7 +68,8 @@ class AugeasConfigurator(configurator.Configurator): # Only print new errors caused by recent save for err in new_errs: if err not in ex_errs: - logger.error("Unable to save file - %s" % err[13:len(err)-6]) + logger.error("Unable to save file - " + "%s" % err[13:len(err)-6]) logger.error("Attempted Save Notes") logger.error(self.save_notes) # Erase Save Notes @@ -99,7 +102,6 @@ class AugeasConfigurator(configurator.Configurator): else: self.add_to_checkpoint(CONFIG.IN_PROGRESS_DIR, save_files) - if title and not temporary and os.path.isdir(CONFIG.IN_PROGRESS_DIR): success = self.__finalize_checkpoint(CONFIG.IN_PROGRESS_DIR, title) if not success: @@ -108,7 +110,6 @@ class AugeasConfigurator(configurator.Configurator): # routine startup sys.exit(9) - self.aug.set("/augeas/save", save_state) self.save_notes = "" self.aug.save() @@ -125,14 +126,14 @@ class AugeasConfigurator(configurator.Configurator): changes = True if result != 0: # We have a partial or incomplete recovery - logger.fatal("Incomplete or failed recovery for %s" % CONFIG.TEMP_CHECKPOINT_DIR) + logger.fatal("Incomplete or failed recovery for " + "%s" % CONFIG.TEMP_CHECKPOINT_DIR) sys.exit(67) # Remember to reload Augeas self.aug.load() - - def rollback_checkpoints(self, rollback = 1): - """ Revert 'rollback' number of configuration checkpoints """ + def rollback_checkpoints(self, rollback=1): + """Revert 'rollback' number of configuration checkpoints.""" try: rollback = int(rollback) except: @@ -146,7 +147,8 @@ class AugeasConfigurator(configurator.Configurator): backups.sort() if len(backups) < rollback: - logger.error("Unable to rollback %d checkpoints, only %d exist" % (rollback, len(backups))) + logger.error(("Unable to rollback %d checkpoints, only " + "%d exist") % (rollback, len(backups))) while rollback > 0 and backups: cp_dir = CONFIG.BACKUP_DIR + backups.pop() @@ -169,7 +171,8 @@ class AugeasConfigurator(configurator.Configurator): backups.sort(reverse=True) if not backups: - print "Letsencrypt has not saved any backups of your apache configuration" + print ("Letsencrypt has not saved any backups of your " + "apache configuration") # Make sure there isn't anything unexpected in the backup folder # There should only be timestamped (float) directories try: @@ -209,7 +212,7 @@ class AugeasConfigurator(configurator.Configurator): with open(cp_dir + "CHANGES_SINCE.tmp", 'w') as ft: ft.write("-- %s --\n" % title) with open(cp_dir + "CHANGES_SINCE", 'r') as f: - ft.write(f.read()) + ft.write(f.read()) shutil.move(cp_dir + "CHANGES_SINCE.tmp", cp_dir + "CHANGES_SINCE") except: logger.error("Unable to finalize checkpoint - adding title") @@ -217,7 +220,8 @@ class AugeasConfigurator(configurator.Configurator): try: os.rename(cp_dir, final_dir) except: - logger.error("Unable to finalize checkpoint, %s -> %s" % (cp_dir, final_dir)) + logger.error("Unable to finalize checkpoint, %s -> %s" % + (cp_dir, final_dir)) return False return True @@ -239,7 +243,8 @@ class AugeasConfigurator(configurator.Configurator): # Tag files with index so multiple files can # have the same filename logger.debug("Creating backup of %s" % filename) - shutil.copy2(filename, cp_dir + os.path.basename(filename) + "_" + str(idx)) + shutil.copy2(filename, cp_dir + os.path.basename(filename) + + "_" + str(idx)) op_fd.write(filename + '\n') idx += 1 op_fd.close() @@ -247,8 +252,6 @@ class AugeasConfigurator(configurator.Configurator): with open(cp_dir + "CHANGES_SINCE", 'a') as notes_fd: notes_fd.write(self.save_notes) - - def __recover_checkpoint(self, cp_dir): """ Recover a specific checkpoint provided by cp_dir @@ -262,7 +265,8 @@ class AugeasConfigurator(configurator.Configurator): with open(cp_dir + "/FILEPATHS") as f: filepaths = f.read().splitlines() for idx, fp in enumerate(filepaths): - shutil.copy2(cp_dir + '/' + os.path.basename(fp) + '_' + str(idx), fp) + shutil.copy2(cp_dir + '/' + os.path.basename(fp) + + '_' + str(idx), fp) except: # This file is required in all checkpoints. logger.error("Unable to recover files from %s" % cp_dir) @@ -286,16 +290,16 @@ class AugeasConfigurator(configurator.Configurator): protected_files = protected_fd.read().splitlines() for filename in protected_files: if filename in save_files: - return False, "Attempting to overwrite challenge file - %s" % filename + return False, ("Attempting to overwrite challenge " + "file - %s" % filename) return True, "Successful" - def register_file_creation(self, temporary, *files): - """ - This is used to register the creation of all files during Letsencrypt - execution. Call this method before writing to the file to make sure - that the file will be cleaned up if the program exits unexpectedly. + """Register the creation of all files during Letsencrypt execution. + + Call this method before writing to the file to make sure that the + file will be cleaned up if the program exits unexpectedly. (Before a save occurs) """ if temporary: @@ -311,14 +315,14 @@ class AugeasConfigurator(configurator.Configurator): except: logger.error("ERROR: Unable to register file creation") - def recovery_routine(self): - """ - Revert all previously modified files. First, any changes found in - CONFIG.TEMP_CHECKPOINT_DIR are removed, then IN_PROGRESS changes are removed - The order is important. IN_PROGRESS is unable to add files that are - already added by a TEMP change. Thus TEMP must be rolled back first - because that will be the 'latest' occurrence of the file. + """Revert all previously modified files. + + First, any changes found in CONFIG.TEMP_CHECKPOINT_DIR are removed, + then IN_PROGRESS changes are removed The order is important. + IN_PROGRESS is unable to add files that are already added by a TEMP + change. Thus TEMP must be rolled back first because that will be the + 'latest' occurrence of the file. """ self.revert_challenge_config() if os.path.isdir(CONFIG.IN_PROGRESS_DIR): @@ -327,13 +331,13 @@ class AugeasConfigurator(configurator.Configurator): # We have a partial or incomplete recovery # Not as egregious # TODO: Additional tests? recovery - logger.fatal("Incomplete or failed recovery for %s" % CONFIG.IN_PROGRESS_DIR) + logger.fatal("Incomplete or failed recovery for %s" % + CONFIG.IN_PROGRESS_DIR) sys.exit(68) # Need to reload configuration after these changes take effect self.aug.load() - def __remove_contained_files(self, file_list): """ Erase any files contained within the text file, file_list @@ -346,14 +350,18 @@ class AugeasConfigurator(configurator.Configurator): with open(file_list, 'r') as f: filepaths = f.read().splitlines() for fp in filepaths: - # Files are registered before they are added... so check to see if file - # exists first + # Files are registered before they are added... so + # check to see if file exists first if os.path.lexists(fp): os.remove(fp) else: - logger.warn("File: %s - Could not be found to be deleted\nProgram was probably shut down unexpectedly, in which case this is not a problem" % fp) + logger.warn(( + "File: %s - Could not be found to be deleted\n" + "Program was probably shut down unexpectedly, " + "in which case this is not a problem") % fp) except IOError: - logger.fatal("Unable to remove filepaths contained within %s" % file_list) + logger.fatal( + "Unable to remove filepaths contained within %s" % file_list) sys.exit(41) return True