#!/usr/bin/env python3 import os import json import sys import signal import logging import time import hashlib import threading import list_domains from docker import Client cert_path = '/data/haproxy' cert_file = cert_path + '/cert.pem' pid_file = '/haproxy.pid' max_age = 24 * 60 * 60 delay = 10 def hash_cert_file(): """Creates the sha256 hash of the certificate file for HAProxy. If the file does not exist, an empty string is returned. """ if not os.path.isfile(cert_file): return '' file_obj = open(cert_file, 'rb') hash_generator = hashlib.sha256() buf = file_obj.read(65536) while len(buf) > 0: hash_generator.update(buf) buf = file_obj.read(65536) return hash_generator.digest() def setup_logging(): """Sets up logging with a nice format""" logging.basicConfig(format='%(asctime)s [%(levelname)s]: %(message)s', level=logging.INFO) def get_pid(): """This function reads the process id from the given file and returns as int.""" with open(pid_file, 'r') as file: return int(file.read()) def kill_haproxy(): """Stops the currently running instance of HAProxy by issuing a kill signal to its pid.""" logging.info('killing HAProxy') try: os.kill(get_pid(), signal.SIGKILL) except OSError: pass def start_haproxy_ssl(): """Start HAProxy with SSL activated. Returns True, if HAProxy is running, False otherwise.""" kill_haproxy() logging.info('starting HAProxy with SSL') os.system('/usr/sbin/haproxy -f /haproxy_ssl.conf -p ' + pid_file) return is_haproxy_running() def start_haproxy(): """Start HAProxy without SSL activated. Returns True, if HAProxy is running, False otherwise.""" kill_haproxy() logging.info('starting HAProxy without SSL') os.system('/usr/sbin/haproxy -f /haproxy.conf -p ' + pid_file) return is_haproxy_running() def is_haproxy_running(): """Check if HAProxy is running by sending a signal to its pid.""" try: os.kill(get_pid(), 0) return True except OSError: return False def ssl_possible(): """Check if a certificate is available.""" if not os.path.isfile(cert_file): return False else: return True def read_file(filename): file=open(filename, 'r') read_data = file.read() file.close() return read_data def write_file(filename, content): file=open(filename, 'w') file.write(content) file.close() def create_haproxy_cert(): """Combines the freshly created fullchain.pem and privkey.pem into /data/haproxy/cert.pem""" logging.info('updating %s', cert_file) # make sure the path exists... if not os.path.exists(cert_path): logging.info('creating cert_path path: %s', cert_path) os.mkdir(cert_path) # find the youngest directory youngest_modify_time = 0 youngest_directory = '' for root, directories, files in os.walk('/data/config/live'): for directory in directories: modify_time = os.stat('/data/config/live/' + directory).st_mtime if modify_time > youngest_modify_time: youngest_modify_time = modify_time youngest_directory = directory youngest_directory = '/data/config/live/' + youngest_directory logging.info('using %s as base dir', youngest_directory) # read fullchain.pem and privkey.pem if not os.path.exists(youngest_directory + '/fullchain.pem') or not os.path.exists(youngest_directory + '/privkey.pem'): logging.info('either fullchain.pem or privkey.pem is missing.') return fullchain = read_file(youngest_directory + '/fullchain.pem') privkey = read_file(youngest_directory + '/privkey.pem') return fullchain + privkey def create_cert_data_standalone(domains): if len(domains) == 0: logging.info('no domains for SSL found.') return domains = " -d ".join(domains) # we should use tls-sni-01 if ssl is already running! os.system( '/letsencrypt/letsencrypt-auto --config letsencrypt.conf certonly --http-01-port 54321 -d ' + domains) def cert_watcher(): ssl_active = ssl_possible() and is_haproxy_running() cert_file_hash = hash_cert_file() while True: time.sleep(delay) if not ssl_active: if ssl_possible(): # we should be able to start with SSL, but ... kill_haproxy() start_haproxy_ssl() if is_haproxy_running(): # running with SSL succeeded cert_file_hash = hash_cert_file() logging.info('NON SSL -> SSL') ssl_active = True else: # something went wrong (maybe broken certificate) but without SSL we can run it start_haproxy() logging.info('NON SSL -> NON SSL') else: # currently not running with SSL but also no cert, so we do not attempt to start with SSL pass else: if cert_file_hash != hash_cert_file(): # we are running with SSL and the certificate has changed so we need to restart haproxy logging.info('cert has changed') kill_haproxy() start_haproxy_ssl() if is_haproxy_running(): # restart with SSL succeeded, update hash logging.info('SSL -> SSL') else: # restart with SSL failed, so we start without SSL again start_haproxy() logging.info('SSL -> NON SSL') ssl_active = False cert_file_hash = hash_cert_file() if __name__ == '__main__': setup_logging() logging.info('starting ssl endpoint') client = Client(base_url='unix://var/run/docker.sock', version='1.15') # try to start in SSL mode, no problem if that fails logging.info('try in SSL mode') if not start_haproxy_ssl(): logging.info('SSL mode failed') if not is_haproxy_running(): # tried to start haproxy and this failed, so we need to create a certificate and try again: # - start non ssl haproxy to be able to get a valid cert logging.info('try in NON SSL mode') start_haproxy() # - get all domains resolved_domains = list_domains.get_resolving_domains_from_containers(client) # - create cert create_cert_data_standalone(resolved_domains) create_haproxy_cert() start_haproxy_ssl() # now we should have it up and running or something weird happened. if not is_haproxy_running(): logging.error('could not start after generating cert. See output above.') sys.exit(1) t = threading.Thread(target=cert_watcher) t.start() cert_cache = {} for line in client.events(): line_str = line.decode("utf-8") event = json.loads(line_str) if event['Action'] in ['create']: # check if there is any domain name configured container_id = event['id'] if len(list_domains.handle_container(client, container_id)) > 0: resolved_domains = list_domains.get_resolving_domains_from_containers(client) resolved_domains = sorted(resolved_domains) if resolved_domains in cert_cache: cached_data = cert_cache[resolved_domains] if time.time() - cached_data['time'] <= max_age: logging.info('get cached cert: %s s old', time.time() - cached_data['time']) write_file(cert_file, cached_data['data']) logging.info('file written') continue # no previous data or to old logging.info('create new cert') create_cert_data_standalone(resolved_domains) cert_data = create_haproxy_cert() cert_cache[resolved_domains] = { 'time': time.time(), 'data': cert_data } write_file(cert_file, cert_data) logging.info('file written')