diff --git a/README.md b/README.md index d05dbfe..5304011 100644 --- a/README.md +++ b/README.md @@ -27,9 +27,7 @@ Setup You should decide which challenge mode you want to use with acertmgr: * webdir: In this mode, responses to challenges are put into a directory, to be served by an existing webserver - * standalone: In this mode, challenges are completed by acertmgr directly. - This starts a webserver to solve the challenges, which can be used standalone or together with an existing webserver that forwards request to a specified local port - * webdir/standalone: Make sure that the `webdir` directory exists in both cases (Note: the standalone webserver does not yet serve the files in situation) + * standalone: In this mode, challenges are completed by acertmgr directly. This starts a webserver to solve the challenges, which can be used standalone or together with an existing webserver that forwards request to a specified local port/address. * dns.*: This mode puts the challenge into a TXT record for the domain (usually _acme-challenge.) where it will be parsed from by the authority * dns.* (Alias mode): Can be used similar to the above but allows redirection of _acme-challenge. to any other (updatable domain) defined in dns_updatedomain via CNAME (e.g. _acme-challenge.example.net IN CNAME bla.foo.bar with dns_updatedomain="bla.foo.bar" in domainconfig) * dns.nsupdate: Updates the TXT record using RFC2136 @@ -76,7 +74,8 @@ By default the directory (work_dir) containing the working data (csr,certificate | cert_file | **d** | Path to store (and load) the certificate file | {cert_dir}/{cert_id}.crt | | key_file | **d**,g | Path to store (and load) the private key file | {cert_dir}/{cert_id}.key | | mode | **d**,g | Mode of challenge handling used | standalone | -| webdir | **d**,g | [webdir,standalone] Put acme challenges into this path | /var/www/acme-challenge/ | +| webdir | **d**,g | [webdir] Put acme challenges into this path | /var/www/acme-challenge/ | +| bind_address | **d**,g | [standalone] Serve the challenge using a HTTP server on given IP | | | port | **d**,g | [standalone] Serve the challenge using a HTTP server on this port | 80 | | dns_ttl | **d**,g | [dns.*] Write TXT records with this TTL (also determines the update wait time at twice this value | 60 | | dns_updatedomain | **d**,g | [dns.*] Write the TXT records to this domain (you have to create the necessary CNAME on the real challenge domain manually) | | diff --git a/acertmgr/modes/standalone.py b/acertmgr/modes/standalone.py index db6cda4..49654b7 100644 --- a/acertmgr/modes/standalone.py +++ b/acertmgr/modes/standalone.py @@ -7,77 +7,77 @@ # available under the ISC license, see LICENSE try: - from SimpleHTTPServer import SimpleHTTPRequestHandler + from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler except ImportError: - from http.server import SimpleHTTPRequestHandler + from http.server import HTTPServer, BaseHTTPRequestHandler -try: - from SocketServer import TCPServer as HTTPServer -except ImportError: - from http.server import HTTPServer - -import os +import datetime +import re +import socket import threading -from acertmgr.modes.webdir import ChallengeHandler as WebChallengeHandler - - -# @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('/') - if spath[0] != '': - raise ValueError("spath should be '' is {}".format(spath[0])) - spath = spath[1:] - if spath[0] == '.well-known': - spath = spath[1:] - if spath[0] == 'acme-challenge': - spath = spath[1:] - if len(spath) != 1: - raise ValueError("spath length {} != 1".format(len(spath))) - 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() - +from acertmgr.modes.abstract import AbstractChallengeHandler HTTPServer.allow_reuse_address = True -class ChallengeHandler(WebChallengeHandler): +class HTTPServer6(HTTPServer): + address_family = socket.AF_INET6 + + +class ChallengeHandler(AbstractChallengeHandler): def __init__(self, config): - WebChallengeHandler.__init__(self, config) - self._verify_challenge = False - self.current_directory = os.getcwd() - if "port" in config: - port = int(config["port"]) - else: - port = 80 + AbstractChallengeHandler.__init__(self, config) + bind_address = config.get("bind_address", "") + port = int(config.get("port", 80)) + + self.challenges = {} # Initialize the challenge data dict + _self = self + + # Custom HTTP request handler + class _HTTPRequestHandler(BaseHTTPRequestHandler): + def log_message(self, fmt, *args): + print("Request from '%s': %s" % (self.address_string(), fmt % args)) + + def do_GET(self): + # Match token on http:///.well-known/acme-challenge/ + match = re.match(r'.*/(?P[^/]*)$', self.path) + if match and match.group('token') in _self.challenges: + value = _self.challenges[match.group('token')].encode('utf-8') + rcode = 200 + else: + value = "404 - NOT FOUND".encode('utf-8') + rcode = 404 + self.send_response(rcode) + self.send_header('Content-type', 'text/plain') + self.send_header('Content-length', len(value)) + self.end_headers() + self.wfile.write(value) + self.server_thread = None - self.server = HTTPServer(("", port), ACMERequestHandler) + try: + self.server = HTTPServer6((bind_address, port), _HTTPRequestHandler) + except socket.gaierror: + self.server = HTTPServer((bind_address, port), _HTTPRequestHandler) + + @staticmethod + def get_challenge_type(): + return "http-01" + + def create_challenge(self, domain, thumbprint, token): + self.challenges[token] = "{0}.{1}".format(token, thumbprint) + return datetime.datetime.now() + + def destroy_challenge(self, domain, thumbprint, token): + del self.challenges[token] def start_challenge(self): - self.server_thread = threading.Thread(target=start_standalone, args=(self.server,)) - os.chdir(self.challenge_directory) + def _(): + self.server.serve_forever() + + self.server_thread = threading.Thread(target=_) self.server_thread.start() def stop_challenge(self): self.server.shutdown() self.server_thread.join() - os.chdir(self.current_directory)