mirror of
https://github.com/moepman/acertmgr.git
synced 2025-01-01 06:41:51 +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:
parent
8cfcdf9385
commit
ff3a57eaff
@ -27,9 +27,7 @@ Setup
|
|||||||
|
|
||||||
You should decide which challenge mode you want to use with acertmgr:
|
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
|
* 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.
|
* 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.
|
||||||
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)
|
|
||||||
* 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.*: 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.* (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
|
* 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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_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) | |
|
| 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) | |
|
||||||
|
@ -7,77 +7,77 @@
|
|||||||
# available under the ISC license, see LICENSE
|
# available under the ISC license, see LICENSE
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from SimpleHTTPServer import SimpleHTTPRequestHandler
|
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from http.server import SimpleHTTPRequestHandler
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||||
|
|
||||||
try:
|
import datetime
|
||||||
from SocketServer import TCPServer as HTTPServer
|
import re
|
||||||
except ImportError:
|
import socket
|
||||||
from http.server import HTTPServer
|
|
||||||
|
|
||||||
import os
|
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
from acertmgr.modes.webdir import ChallengeHandler as WebChallengeHandler
|
from acertmgr.modes.abstract import AbstractChallengeHandler
|
||||||
|
|
||||||
|
|
||||||
# @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()
|
|
||||||
|
|
||||||
|
|
||||||
HTTPServer.allow_reuse_address = True
|
HTTPServer.allow_reuse_address = True
|
||||||
|
|
||||||
|
|
||||||
class ChallengeHandler(WebChallengeHandler):
|
class HTTPServer6(HTTPServer):
|
||||||
|
address_family = socket.AF_INET6
|
||||||
|
|
||||||
|
|
||||||
|
class ChallengeHandler(AbstractChallengeHandler):
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
WebChallengeHandler.__init__(self, config)
|
AbstractChallengeHandler.__init__(self, config)
|
||||||
self._verify_challenge = False
|
bind_address = config.get("bind_address", "")
|
||||||
self.current_directory = os.getcwd()
|
port = int(config.get("port", 80))
|
||||||
if "port" in config:
|
|
||||||
port = int(config["port"])
|
self.challenges = {} # Initialize the challenge data dict
|
||||||
else:
|
_self = self
|
||||||
port = 80
|
|
||||||
|
# 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_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):
|
def start_challenge(self):
|
||||||
self.server_thread = threading.Thread(target=start_standalone, args=(self.server,))
|
def _():
|
||||||
os.chdir(self.challenge_directory)
|
self.server.serve_forever()
|
||||||
|
|
||||||
|
self.server_thread = threading.Thread(target=_)
|
||||||
self.server_thread.start()
|
self.server_thread.start()
|
||||||
|
|
||||||
def stop_challenge(self):
|
def stop_challenge(self):
|
||||||
self.server.shutdown()
|
self.server.shutdown()
|
||||||
self.server_thread.join()
|
self.server_thread.join()
|
||||||
os.chdir(self.current_directory)
|
|
||||||
|
Loading…
Reference in New Issue
Block a user