2016-04-06 20:26:08 +02:00
|
|
|
#!/usr/bin/python3.4
|
|
|
|
|
|
|
|
import os
|
2016-04-10 09:36:03 +02:00
|
|
|
import json
|
2016-04-08 06:46:00 +02:00
|
|
|
import sys
|
2016-04-06 20:26:08 +02:00
|
|
|
import signal
|
|
|
|
import logging
|
|
|
|
import time
|
|
|
|
import hashlib
|
2016-04-10 09:36:03 +02:00
|
|
|
import threading
|
2016-04-06 20:26:08 +02:00
|
|
|
|
2016-04-08 06:46:00 +02:00
|
|
|
import list_domains
|
|
|
|
from docker import Client
|
|
|
|
|
2016-04-10 12:36:43 +02:00
|
|
|
cert_path = '/data/haproxy'
|
2016-04-12 07:05:24 +02:00
|
|
|
cert_file = cert_path + '/cert.pem'
|
2016-04-10 12:36:43 +02:00
|
|
|
pid_file = '/haproxy.pid'
|
|
|
|
delay = 10
|
2016-04-06 20:26:08 +02:00
|
|
|
|
2016-04-12 07:05:24 +02:00
|
|
|
|
2016-04-06 20:26:08 +02:00
|
|
|
def hash_cert_file():
|
2016-04-12 07:05:24 +02:00
|
|
|
"""Creates the sha256 hash of the certificate file for HAProxy. If the file
|
2016-04-07 08:03:13 +02:00
|
|
|
does not exist, an empty string is returned.
|
|
|
|
"""
|
2016-04-06 20:26:08 +02:00
|
|
|
if not os.path.isfile(cert_file):
|
|
|
|
return ''
|
2016-04-10 12:36:43 +02:00
|
|
|
file_obj = open(cert_file, 'rb')
|
|
|
|
hash_generator = hashlib.sha256()
|
|
|
|
buf = file_obj.read(65536)
|
2016-04-06 20:26:08 +02:00
|
|
|
while len(buf) > 0:
|
2016-04-10 12:36:43 +02:00
|
|
|
hash_generator.update(buf)
|
|
|
|
buf = file_obj.read(65536)
|
|
|
|
return hash_generator.digest()
|
|
|
|
|
2016-04-06 20:26:08 +02:00
|
|
|
|
|
|
|
def setup_logging():
|
|
|
|
"""Sets up logging with a nice format"""
|
|
|
|
logging.basicConfig(format='%(asctime)s [%(levelname)s]: %(message)s', level=logging.INFO)
|
|
|
|
|
2016-04-10 12:36:43 +02:00
|
|
|
|
2016-04-06 20:26:08 +02:00
|
|
|
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())
|
|
|
|
|
2016-04-10 12:36:43 +02:00
|
|
|
|
2016-04-06 20:26:08 +02:00
|
|
|
def kill_haproxy():
|
2016-04-12 07:05:24 +02:00
|
|
|
"""Stops the currently running instance of HAProxy by issuing a kill signal to its pid."""
|
|
|
|
logging.info('killing HAProxy')
|
2016-04-06 20:26:08 +02:00
|
|
|
try:
|
|
|
|
os.kill(get_pid(), signal.SIGKILL)
|
|
|
|
except OSError:
|
|
|
|
pass
|
|
|
|
|
2016-04-10 12:36:43 +02:00
|
|
|
|
2016-04-06 20:26:08 +02:00
|
|
|
def start_haproxy_ssl():
|
2016-04-12 07:05:24 +02:00
|
|
|
"""Start HAProxy with SSL activated. Returns True, if HAProxy is running, False otherwise."""
|
|
|
|
kill_haproxy()
|
|
|
|
logging.info('starting HAProxy with SSL')
|
2016-04-10 12:36:43 +02:00
|
|
|
os.system('/usr/sbin/haproxy -f /haproxy_ssl.conf -p ' + pid_file)
|
2016-04-08 06:46:00 +02:00
|
|
|
return is_haproxy_running()
|
2016-04-06 20:26:08 +02:00
|
|
|
|
2016-04-10 12:36:43 +02:00
|
|
|
|
2016-04-06 20:26:08 +02:00
|
|
|
def start_haproxy():
|
2016-04-12 07:05:24 +02:00
|
|
|
"""Start HAProxy without SSL activated. Returns True, if HAProxy is running, False otherwise."""
|
|
|
|
kill_haproxy()
|
|
|
|
logging.info('starting HAProxy without SSL')
|
2016-04-10 12:36:43 +02:00
|
|
|
os.system('/usr/sbin/haproxy -f /haproxy.conf -p ' + pid_file)
|
2016-04-12 07:05:24 +02:00
|
|
|
return is_haproxy_running()
|
2016-04-06 20:26:08 +02:00
|
|
|
|
2016-04-10 12:36:43 +02:00
|
|
|
|
2016-04-06 20:26:08 +02:00
|
|
|
def is_haproxy_running():
|
2016-04-12 07:05:24 +02:00
|
|
|
"""Check if HAProxy is running by sending a signal to its pid."""
|
2016-04-06 20:26:08 +02:00
|
|
|
try:
|
|
|
|
os.kill(get_pid(), 0)
|
|
|
|
return True
|
|
|
|
except OSError:
|
|
|
|
return False
|
|
|
|
|
2016-04-10 12:36:43 +02:00
|
|
|
|
2016-04-06 20:26:08 +02:00
|
|
|
def ssl_possible():
|
|
|
|
"""Check if a certificate is available."""
|
|
|
|
if not os.path.isfile(cert_file):
|
|
|
|
return False
|
|
|
|
else:
|
|
|
|
return True
|
|
|
|
|
2016-04-10 12:36:43 +02:00
|
|
|
|
2016-04-12 07:05:24 +02:00
|
|
|
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()
|
|
|
|
|
|
|
|
|
2016-04-08 06:46:00 +02:00
|
|
|
def create_haproxy_cert():
|
|
|
|
"""Combines the freshly created fullchain.pem and privkey.pem into /data/haproxy/cert.pem"""
|
2016-04-10 09:36:03 +02:00
|
|
|
logging.info('updating %s', cert_file)
|
2016-04-12 07:05:24 +02:00
|
|
|
|
|
|
|
# make sure the path exists...
|
2016-04-10 12:36:43 +02:00
|
|
|
if not os.path.exists(cert_path):
|
|
|
|
logging.info('creating cert_path path: %s', cert_path)
|
|
|
|
os.mkdir(cert_path)
|
2016-04-12 07:05:24 +02:00
|
|
|
|
|
|
|
# find the youngest directory
|
|
|
|
youngest_modify_time = 0
|
|
|
|
youngest_directory = ''
|
|
|
|
for root, directories, files in os.walk('/data/config/live'):
|
|
|
|
for directory in directories:
|
2016-04-12 07:29:39 +02:00
|
|
|
modify_time = os.stat('/data/config/live/' + directory).st_mtime
|
2016-04-12 07:05:24 +02:00
|
|
|
if modify_time > youngest_modify_time:
|
|
|
|
youngest_modify_time = modify_time
|
|
|
|
youngest_directory = directory
|
2016-04-12 07:29:39 +02:00
|
|
|
youngest_directory = '/data/config/live/' + youngest_directory
|
2016-04-12 07:05:24 +02:00
|
|
|
|
|
|
|
logging.info('using %s as base dir', youngest_directory)
|
|
|
|
|
|
|
|
# read fullchain.pem and privkey.pem
|
|
|
|
fullchain = read_file(youngest_directory + '/fullchain.pem')
|
|
|
|
privkey = read_file(youngest_directory + '/privkey.pem')
|
|
|
|
write_file(cert_file, fullchain + privkey)
|
2016-04-12 07:29:39 +02:00
|
|
|
logging.info('file written')
|
2016-04-08 06:46:00 +02:00
|
|
|
|
|
|
|
def create_cert_data_standalone(domains):
|
|
|
|
domains = " -d ".join(domains)
|
2016-04-12 07:05:24 +02:00
|
|
|
|
|
|
|
# we should use tls-sni-01 if ssl is already running!
|
2016-04-10 12:36:43 +02:00
|
|
|
os.system(
|
2016-04-12 07:05:24 +02:00
|
|
|
'/letsencrypt/letsencrypt-auto --config letsencrypt.conf certonly --standalone-supported-challenges http-01 --http-01-port 54321 -d ' + domains)
|
2016-04-10 12:36:43 +02:00
|
|
|
|
2016-04-10 09:36:03 +02:00
|
|
|
|
|
|
|
def cert_watcher():
|
2016-04-10 12:36:43 +02:00
|
|
|
ssl_active = ssl_possible() and is_haproxy_running()
|
2016-04-10 09:36:03 +02:00
|
|
|
cert_file_hash = hash_cert_file()
|
|
|
|
while True:
|
|
|
|
logging.info('ping')
|
2016-04-10 12:36:43 +02:00
|
|
|
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')
|
2016-04-10 09:36:03 +02:00
|
|
|
else:
|
2016-04-10 12:36:43 +02:00
|
|
|
# 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
|
2016-04-10 09:36:03 +02:00
|
|
|
cert_file_hash = hash_cert_file()
|
2016-04-10 12:36:43 +02:00
|
|
|
|
2016-04-08 06:46:00 +02:00
|
|
|
|
2016-04-06 20:26:08 +02:00
|
|
|
if __name__ == '__main__':
|
|
|
|
setup_logging()
|
|
|
|
|
2016-04-10 12:36:43 +02:00
|
|
|
logging.info('starting ssl endpoint')
|
2016-04-06 20:26:08 +02:00
|
|
|
|
2016-04-10 09:36:03 +02:00
|
|
|
client = Client(base_url='unix://var/run/docker.sock', version='1.15')
|
|
|
|
|
2016-04-08 06:46:00 +02:00
|
|
|
# try to start in SSL mode, no problem if that fails
|
|
|
|
logging.info('try in SSL mode')
|
2016-04-10 12:36:43 +02:00
|
|
|
if not start_haproxy_ssl():
|
2016-04-08 06:46:00 +02:00
|
|
|
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
|
2016-04-06 20:26:08 +02:00
|
|
|
logging.info('try in NON SSL mode')
|
2016-04-10 12:36:43 +02:00
|
|
|
start_haproxy()
|
2016-04-08 06:46:00 +02:00
|
|
|
# - get all domains
|
|
|
|
resolved_domains = list_domains.get_resolving_domains_from_containers(client)
|
|
|
|
# - create cert
|
|
|
|
create_cert_data_standalone(resolved_domains)
|
|
|
|
create_haproxy_cert()
|
|
|
|
|
|
|
|
# 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)
|
2016-04-06 20:26:08 +02:00
|
|
|
|
2016-04-10 09:36:03 +02:00
|
|
|
t = threading.Thread(target=cert_watcher)
|
|
|
|
t.start()
|
|
|
|
|
|
|
|
for line in client.events():
|
|
|
|
line_str = line.decode("utf-8")
|
|
|
|
event = json.loads(line_str)
|
|
|
|
|
2016-04-10 14:02:56 +02:00
|
|
|
if event['Action'] in ['start', 'stop']:
|
2016-04-10 11:54:27 +02:00
|
|
|
# check if there is any domain name configured
|
|
|
|
container_id = event['id']
|
2016-04-10 12:47:18 +02:00
|
|
|
if len(list_domains.handle_container(client, container_id)) > 0:
|
2016-04-10 11:54:27 +02:00
|
|
|
resolved_domains = list_domains.get_resolving_domains_from_containers(client)
|
|
|
|
create_cert_data_standalone(resolved_domains)
|
|
|
|
create_haproxy_cert()
|