diff --git a/acertmgr.py b/acertmgr.py index fe2e4d6..1e56b8e 100755 --- a/acertmgr.py +++ b/acertmgr.py @@ -16,8 +16,18 @@ import re import shutil import subprocess import tempfile +import threading import yaml +try: + from SimpleHTTPServer import SimpleHTTPRequestHandler +except ImportError: + from http.server import SimpleHTTPRequestHandler + +try: + from SocketServer import TCPServer as HTTPServer +except ImportError: + from http.server import HTTPServer ACME_DIR="/etc/acme/" ACME_CONF=ACME_DIR + "acme.conf" @@ -31,6 +41,36 @@ class FileNotFoundError(OSError): class InvalidCertificateError(Exception): pass +# @brief custom request handler for ACME challenges +# @note current working directory is temporarily changed by the script before +# the webserver starts, which allows using SimpleHTTPRequestHandler +class ACMERequestHandler(SimpleHTTPRequestHandler): + # @brief remove directories from GET URL + # @details the current working directory contains the challenge files, + # there is no need for creating subdirectories for the path + # that ACME expects. + # Additionally, this allows redirecting the ACME path to this + # webserver without having to know which subdirectory is + # redirected, which simplifies integration with existing + # webservers. + def translate_path(self, path): + spath = path.split('/') + assert(spath[0] == '') + spath = spath[1:] + if spath[0] == '.well-known': + spath = spath[1:] + if spath[0] == 'acme-challenge': + spath = spath[1:] + assert(len(spath) == 1) + spath.insert(0, '') + path = '/'.join(spath) + return SimpleHTTPRequestHandler.translate_path(self, path) + +# @brief start the standalone webserver +# @param server the HTTPServer object +# @note this function is used to be passed to threading.Thread +def start_standalone(server): + server.serve_forever() # @brief check whether existing certificate is still valid or expiring soon # @param crt_file string containing the path to the certificate file @@ -87,6 +127,16 @@ def cert_get(domain, settings): if not os.path.isdir(challenge_dir): raise FileNotFoundError("Challenge directory (%s) does not exist!" % challenge_dir) + if settings['mode'] == 'standalone': + port = settings.get('port', 80) + + current_dir = os.getcwd() + os.chdir(challenge_dir) + HTTPServer.allow_reuse_address = True + server = HTTPServer(("", port), ACMERequestHandler) + server_thread = threading.Thread(target=start_standalone, args=(server, )) + server_thread.start() + try: cr = subprocess.check_output(['openssl', 'req', '-new', '-sha256', '-key', key_file, '-out', csr_file, '-subj', '/CN=%s' % domain]) @@ -101,6 +151,10 @@ def cert_get(domain, settings): shutil.copy2(crt_file, crt_final) finally: + if settings['mode'] == 'standalone': + os.chdir(current_dir) + server.shutdown() + server_thread.join() os.remove(csr_file) os.remove(crt_file)