Merge pull request #52 from kuba/pep8

PEP-8, code base cleanup, bug fixes
This commit is contained in:
James Kasten 2014-11-21 22:48:55 -08:00
commit c260232f61
18 changed files with 618 additions and 677 deletions

View file

@ -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]"]

51
letsencrypt/client/acme.py Executable file → Normal file
View file

@ -1,33 +1,48 @@
#!/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
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"]
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"
]
}
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)

View file

@ -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 = "<VirtualHost " + " ".join(ip_addrs) + "> \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 \
</VirtualHost> \n\n "
return configText
@ -1187,8 +1180,8 @@ DocumentRoot " + CONFIG_DIR + "challenge_page/ \n \
configText += "</IfModule> \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)
"""

View file

@ -1,12 +1,17 @@
import abc, 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__()
@ -16,54 +21,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
@ -78,15 +35,17 @@ class AugeasConfigurator(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
@ -105,11 +64,12 @@ class AugeasConfigurator(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:
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
@ -138,20 +98,18 @@ 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
# routine startup
sys.exit(9)
self.aug.set("/augeas/save", save_state)
self.save_notes = ""
self.aug.save()
@ -163,19 +121,19 @@ 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()
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:
@ -185,14 +143,15 @@ 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)))
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")
@ -208,32 +167,33 @@ 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:
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:
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:
@ -247,12 +207,12 @@ 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)
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")
@ -260,7 +220,8 @@ class AugeasConfigurator(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
@ -282,7 +243,8 @@ class AugeasConfigurator(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()
@ -290,8 +252,6 @@ class AugeasConfigurator(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
@ -305,7 +265,8 @@ class AugeasConfigurator(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)
@ -323,28 +284,28 @@ 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()
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:
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:
@ -354,29 +315,29 @@ class AugeasConfigurator(Configurator):
except:
logger.error("ERROR: Unable to register file creation")
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
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(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
self.aug.load()
def __remove_contained_files(self, file_list):
"""
Erase any files contained within the text file, file_list
@ -389,36 +350,18 @@ class AugeasConfigurator(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
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")

View file

@ -1,12 +1,13 @@
from letsencrypt.client import logger
#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()

110
letsencrypt/client/client.py Executable file → Normal file
View file

@ -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
@ -423,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:
@ -443,7 +448,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 +459,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 +471,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 +493,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 +516,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))
@ -553,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:
@ -581,11 +586,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 +604,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)
@ -619,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
@ -695,15 +700,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)
@ -741,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()

View file

@ -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
def deploy_cert(self, vhost, cert, key, cert_chain=None):
raise NotImplementedError()
@abc.abstractmethod
def deploy_cert(self, vhost, cert, key , cert_chain=None):
return
@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
"""Redirect all traffic 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
def rollback_checkpoints(self, rollback=1):
"""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
def restart(self, quiet=False):
"""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()

View file

@ -1,59 +1,72 @@
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 b64_cert_to_pem(b64_der_cert):
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):
# 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 = 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)
signature = signer.sign(h)
signer_nonce = Random.get_random_bytes(signer_nonce_len)
hashed = Crypto.Hash.SHA256.new(signer_nonce + msg)
signer = Crypto.Signature.PKCS1_v1_5.new(key)
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=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,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 = EVP.PKey()
pk.assign_rsa(rsa_key)
pubkey = M2Crypto.EVP.PKey()
pubkey.assign_rsa(rsa_key)
x = 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"
@ -80,72 +93,81 @@ 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)
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 = EVP.PKey()
pk.assign_rsa(rsa_key)
pubkey = M2Crypto.EVP.PKey()
pubkey.assign_rsa(rsa_key)
x = 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 = ASN1.ASN1_UTCTIME()
current.set_time(t)
expire = ASN1.ASN1_UTCTIME()
expire.set_time((7 * 24 * 60 * 60) + t)
x.set_not_before(current)
x.set_not_after(expire)
current_ts = long(time.time())
current = M2Crypto.ASN1.ASN1_UTCTIME()
current.set_time(current_ts)
expire = M2Crypto.ASN1.ASN1_UTCTIME()
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(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])))
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

View file

@ -1,167 +1,141 @@
import textwrap
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):
def generic_notification(self, message, width = WIDTH, height = HEIGHT):
raise Exception("Error no display defined")
def generic_menu(self, message, choices, input_text = "", width = WIDTH, height = HEIGHT):
raise Exception("Error no display defined")
"""Generic display."""
def generic_notification(self, message, width=WIDTH, height=HEIGHT):
raise NotImplementedError()
def generic_menu(self, message, choices, input_text="",
width=WIDTH, height=HEIGHT):
raise NotImplementedError()
def generic_input(self, message):
raise Exception("Error no display defined")
def generic_yesno(self, message, yes_label = "Yes", no_label = "No"):
raise Exception("Error no display defined")
raise NotImplementedError()
def generic_yesno(self, message, yes_label="Yes", no_label="No"):
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")
def gen_https_names(self, domains):
"""
Returns a string of the domains 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
raise NotImplementedError()
def display_certs(self, certs):
raise Exception("Error no display define")
raise NotImplementedError()
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)
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
raise NotImplementedError()
def more_info_cert(self, cert):
raise Exception("Error no display defined")
raise NotImplementedError()
class NcursesDisplay(Display):
import dialog
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)
def generic_notification(self, message, w=WIDTH, h=HEIGHT):
self.dialog.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)
return c, str(selection)
code, selection = self.dialog.menu(
message, choices=choices, width=WIDTH, height=HEIGHT)
return code, str(selection)
else:
choices = [((i + 1), c) for c in choices]
code, s = self.d.menu(message, choices = choices,
width = WIDTH, height = HEIGHT)
choices = list(enumerate(choices, 1))
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
def generic_yesno(self, message, yes="Yes", no="No"):
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 "
+ self.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(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)
if not s:
s = -1
return code, (int(s) - 1)
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 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 += self.cert_info_frame(cert)
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 += self.cert_info_frame(cert)
text += cert_info_frame(cert)
print text
self.d.msgbox(text, width=WIDTH, height=HEIGHT)
self.dialog.msgbox(text, width=WIDTH, height=HEIGHT)
textwrap = None
class FileDisplay(Display):
global textwrap
import textwrap
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]
@ -170,13 +144,14 @@ 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)
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)
@ -188,19 +163,18 @@ 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')
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"] +
@ -208,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
@ -229,7 +202,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
@ -240,25 +213,24 @@ class FileDisplay(Display):
return code, selection
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))
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("This action cannot be reversed!\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")
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"
@ -266,38 +238,77 @@ CANCEL = "cancel"
HELP = "help"
def setDisplay(display_inst):
def set_display(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_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):
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):
return display.success_installation(domains)
def redirect_by_default():
choices = [
("Easy", "Allow both HTTP and HTTPS access to these sites"),
@ -307,17 +318,18 @@ def redirect_by_default():
"is required or optional.",
choices,
"Please enter the appropriate number",
width = WIDTH)
width=WIDTH)
if result[0] != OK:
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):
return display.confirm_revocation(cert)
def more_info_cert(cert):
return display.more_info_cert(cert)

View file

@ -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):
@ -16,16 +20,16 @@ class Interactive_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
# def formatted_reasons(self):
# return "\n\t* %s\n", self.reason

View file

@ -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):

View file

@ -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'))

View file

@ -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,10 @@ from letsencrypt.client import logger, le_util, configurator
# def add_name(self, name):
# self.names.append(name)
class NginxConfigurator(AugeasConfigurator):
def __init__(self, server_root=SERVER_ROOT):
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
@ -34,27 +32,16 @@ class NginxConfigurator(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):
@ -75,7 +62,7 @@ class NginxConfigurator(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:
@ -90,7 +77,8 @@ class NginxConfigurator(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
@ -103,8 +91,9 @@ class NginxConfigurator(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)
@ -113,7 +102,6 @@ class NginxConfigurator(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
@ -123,7 +111,6 @@ class NginxConfigurator(AugeasConfigurator):
"""
return
def enable_ocsp_stapling(self, ssl_vhost):
return False
@ -159,9 +146,7 @@ class NginxConfigurator(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
@ -172,31 +157,29 @@ class NginxConfigurator(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_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):
"""
Restarts nginx server
"""
return
"""Restarts nginx server"""
# May be of use?
# def __add_httpd_transform(self, incl):
@ -209,12 +192,10 @@ class NginxConfigurator(AugeasConfigurator):
# self.aug.set("/augeas/load/Httpd/incl[last()]", incl)
def config_test(self):
""" Check Configuration """
"""Check Configuration"""
return False
def main():
return

View file

@ -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 = ""
@ -27,7 +27,7 @@ class RecoveryContact(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:")

View file

@ -1,13 +1,17 @@
from letsencrypt.client.challenge import Challenge
import dialog
class RecoveryToken(Challenge):
from letsencrypt.client import challenge
def __init__(self):
class RecoveryToken(challenge.Challenge):
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
@ -17,4 +21,4 @@ class RecoveryToken(Challenge):
pass
def generate_response(self):
return {"type":"recoveryToken", "token":self.token}
return {"type": "recoveryToken", "token": self.token}

0
letsencrypt/client/setup.sh Executable file → Normal file
View file

View file

@ -1,12 +1,14 @@
class Validator(object):
"""
This Class will contain an API to validate configurations.
"""
def redirect(name):
return
def ocsp_stapling(name):
return
def https(names):
return
def hsts(name):
return
"""Configuration validator."""
def redirect(self, name):
raise NotImplementedError()
def ocsp_stapling(self, name):
raise NotImplementedError()
def https(self, names):
raise NotImplementedError()
def hsts(self, name):
raise NotImplementedError()

View file

@ -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()
@ -79,12 +78,12 @@ 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 = ACME_SERVER
server = CONFIG.ACME_SERVER
c = client.Client(server, csr, privkey, curses)
if flag_revoke: