1
0
mirror of https://github.com/moepman/acertmgr.git synced 2024-11-13 07:55:26 +01:00

standalone: remove dependency to webdir and add ipv6 support

- Serve the challenge authorizations from in-memory instead of files
- Try to establish a dual-stack IPv6 HTTPServer before falling back
This commit is contained in:
Kishi85 2019-03-27 11:40:40 +01:00
parent 8cfcdf9385
commit ff3a57eaff
2 changed files with 60 additions and 61 deletions

View File

@ -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.<domain>) 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.<domain> 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) | |

View File

@ -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://<domain>/.well-known/acme-challenge/<token>
match = re.match(r'.*/(?P<token>[^/]*)$', 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)