Merge branch 'refactor' into 'master'

Refactor

See merge request !2
This commit is contained in:
Joachim Lusiardi 2016-12-13 07:55:39 +01:00
commit 6a5d02cc41
5 changed files with 127 additions and 58 deletions

View File

@ -13,7 +13,7 @@ RUN pip3 install docker-py
ADD haproxy_ssl.conf /haproxy_ssl.conf ADD haproxy_ssl.conf /haproxy_ssl.conf
ADD haproxy.conf /haproxy.conf ADD haproxy.conf /haproxy.conf
ADD letencrypt.conf /letencrypt.conf ADD letsencrypt.conf /letsencrypt.conf
ADD start.py /start.py ADD start.py /start.py
ADD list_domains.py /list_domains.py ADD list_domains.py /list_domains.py

View File

@ -1,7 +1,8 @@
# SSL Termination using haproxy # SSL Termination using HAProxy
This image translates between plain http and https using haproxy. This image translates between plain http and https using HAProxy.
## How it works ## How it works
``` ```
+-------------+ +-------------+
| | | |

View File

@ -6,3 +6,7 @@ work-dir=/data/work
config-dir=/data/config config-dir=/data/config
email=letsencrypt@lusiardi.de email=letsencrypt@lusiardi.de
agree-tos=TRUE agree-tos=TRUE
expand=TRUE
force-renewal=TRUE
duplicate=TRUE
allow-subset-of-names=TRUE

View File

@ -9,12 +9,14 @@ import os
from socket import getaddrinfo from socket import getaddrinfo
import logging import logging
def get_if_available(dict, key, defValue): def get_if_available(dict, key, defValue):
if key in dict: if key in dict:
return dict[key] return dict[key]
else: else:
return defValue return defValue
def analyse_proxy_data(data): def analyse_proxy_data(data):
"""Extracts the data for the proxy configuration (envrionment variable """Extracts the data for the proxy configuration (envrionment variable
'PROXY_DATA' and converts it to a dictionary.""" 'PROXY_DATA' and converts it to a dictionary."""
@ -24,6 +26,7 @@ def analyse_proxy_data(data):
proxy_data[t[0]] = t[1] proxy_data[t[0]] = t[1]
return proxy_data return proxy_data
def analyse_env_vars(inspect_data): def analyse_env_vars(inspect_data):
"""Extracts the environment variables from the given result of an 'inspect """Extracts the environment variables from the given result of an 'inspect
container' call.""" container' call."""
@ -35,6 +38,7 @@ def analyse_env_vars(inspect_data):
env_data[t[0]] = t[1] env_data[t[0]] = t[1]
return env_data return env_data
def handle_container(docker_client, id): def handle_container(docker_client, id):
"""This function take a container's id and collects all data required """This function take a container's id and collects all data required
to create a proper proxy configuration. The configuration is then to create a proper proxy configuration. The configuration is then
@ -47,6 +51,7 @@ def handle_container(docker_client, id):
return names return names
return [] return []
def get_resolving_domains_from_containers(docker_client): def get_resolving_domains_from_containers(docker_client):
container_ids = docker_client.containers(quiet=True) container_ids = docker_client.containers(quiet=True)
@ -70,8 +75,8 @@ def get_resolving_domains_from_containers(docker_client):
return resolved_domains return resolved_domains
if __name__ == '__main__':
if __name__ == '__main__':
client = Client(base_url='unix://var/run/docker.sock', version='1.15') client = Client(base_url='unix://var/run/docker.sock', version='1.15')
resolved_domains = get_resolving_domains_from_containers(client) resolved_domains = get_resolving_domains_from_containers(client)

135
start.py
View File

@ -13,127 +13,186 @@ import list_domains
from docker import Client from docker import Client
cert_path = '/data/haproxy' cert_path = '/data/haproxy'
cert_file='/data/haproxy/cert.pem' cert_file = cert_path + '/cert.pem'
pid_file = '/haproxy.pid' pid_file = '/haproxy.pid'
delay = 10
def hash_cert_file(): def hash_cert_file():
"""Creates the sha256 hash of the certifcate file for haproxy. If the file """Creates the sha256 hash of the certificate file for HAProxy. If the file
does not exist, an empty string is returned. does not exist, an empty string is returned.
""" """
if not os.path.isfile(cert_file): if not os.path.isfile(cert_file):
return '' return ''
aFile = open(cert_file, 'rb') file_obj = open(cert_file, 'rb')
hasher = hashlib.sha256() hash_generator = hashlib.sha256()
buf = aFile.read(65536) buf = file_obj.read(65536)
while len(buf) > 0: while len(buf) > 0:
hasher.update(buf) hash_generator.update(buf)
buf = aFile.read(65536) buf = file_obj.read(65536)
return hasher.digest() return hash_generator.digest()
def setup_logging(): def setup_logging():
"""Sets up logging with a nice format""" """Sets up logging with a nice format"""
logging.basicConfig(format='%(asctime)s [%(levelname)s]: %(message)s', level=logging.INFO) logging.basicConfig(format='%(asctime)s [%(levelname)s]: %(message)s', level=logging.INFO)
def get_pid(): def get_pid():
"""This function reads the process id from the given file and returns as int.""" """This function reads the process id from the given file and returns as int."""
with open(pid_file, 'r') as file: with open(pid_file, 'r') as file:
return int(file.read()) return int(file.read())
def kill_haproxy(): def kill_haproxy():
"""Stops the currently running instance of haproxy by issueing a kill signal to its pid.""" """Stops the currently running instance of HAProxy by issuing a kill signal to its pid."""
logging.info('killing haproxy') logging.info('killing HAProxy')
try: try:
os.kill(get_pid(), signal.SIGKILL) os.kill(get_pid(), signal.SIGKILL)
except OSError: except OSError:
pass pass
def start_haproxy_ssl(): def start_haproxy_ssl():
logging.info('starting haproxy SSL') """Start HAProxy with SSL activated. Returns True, if HAProxy is running, False otherwise."""
os.system('/usr/sbin/haproxy -f /haproxy_ssl.conf -p /haproxy.pid') kill_haproxy()
logging.info('starting HAProxy with SSL')
os.system('/usr/sbin/haproxy -f /haproxy_ssl.conf -p ' + pid_file)
return is_haproxy_running() return is_haproxy_running()
def start_haproxy(): def start_haproxy():
logging.info('starting haproxy NON SSL') """Start HAProxy without SSL activated. Returns True, if HAProxy is running, False otherwise."""
os.system('/usr/sbin/haproxy -f /haproxy.conf -p /haproxy.pid') kill_haproxy()
return False 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(): def is_haproxy_running():
"""Check if HAProxy is running by sending a signal to its pid."""
try: try:
os.kill(get_pid(), 0) os.kill(get_pid(), 0)
return True return True
except OSError: except OSError:
return False return False
def ssl_possible(): def ssl_possible():
"""Check if a certificate is available.""" """Check if a certificate is available."""
if not os.path.isfile(cert_file): if not os.path.isfile(cert_file):
return False return False
else: else:
return True 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(): def create_haproxy_cert():
"""Combines the freshly created fullchain.pem and privkey.pem into /data/haproxy/cert.pem""" """Combines the freshly created fullchain.pem and privkey.pem into /data/haproxy/cert.pem"""
logging.info('updating %s', cert_file) logging.info('updating %s', cert_file)
os.system('DIR=`ls -td /data/config/live/*/ | head -1`; echo ${DIR}; mkdir -p /data/haproxy; cat ${DIR}/fullchain.pem ${DIR}/privkey.pem > /data/haproxy/cert.pem')
# 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
fullchain = read_file(youngest_directory + '/fullchain.pem')
privkey = read_file(youngest_directory + '/privkey.pem')
write_file(cert_file, fullchain + privkey)
logging.info('file written')
def create_cert_data_standalone(domains): def create_cert_data_standalone(domains):
domains = " -d ".join(domains) domains = " -d ".join(domains)
os.system('/letsencrypt/letsencrypt-auto --config letencrypt.conf certonly --expand --force-renewal --duplicate --allow-subset-of-names --standalone-supported-challenges http-01 --http-01-port 54321 -d ' + domains)
# we should use tls-sni-01 if ssl is already running!
os.system(
'/letsencrypt/letsencrypt-auto --config letsencrypt.conf certonly --standalone-supported-challenges http-01 --http-01-port 54321 -d ' + domains)
def cert_watcher(): def cert_watcher():
SSL_RUNNING=True ssl_active = ssl_possible() and is_haproxy_running()
cert_file_hash = hash_cert_file() cert_file_hash = hash_cert_file()
while True: while True:
logging.info('ping') logging.info('ping')
time.sleep(60) time.sleep(delay)
if ssl_possible() and not SSL_RUNNING:
if not ssl_active:
if ssl_possible():
# we should be able to start with SSL, but ...
kill_haproxy() kill_haproxy()
start_haproxy_ssl() start_haproxy_ssl()
if is_haproxy_running(): if is_haproxy_running():
# running with SSL succeeded
cert_file_hash = hash_cert_file() cert_file_hash = hash_cert_file()
logging.info('NON SSL -> SSL') logging.info('NON SSL -> SSL')
SSL_RUNNING=True ssl_active = True
else: else:
# something went wrong (maybe broken certificate) but without SSL we can run it
start_haproxy() start_haproxy()
SSL_RUNNING=False logging.info('NON SSL -> NON SSL')
if SSL_RUNNING and cert_file_hash != hash_cert_file(): 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') logging.info('cert has changed')
kill_haproxy() kill_haproxy()
start_haproxy_ssl() start_haproxy_ssl()
if is_haproxy_running(): if is_haproxy_running():
cert_file_hash = hash_cert_file() # restart with SSL succeeded, update hash
logging.info('SSL -> SSL') logging.info('SSL -> SSL')
SSL_RUNNING=True
else: else:
# restart with SSL failed, so we start without SSL again
start_haproxy() start_haproxy()
logging.info('SSL -> NON SSL') logging.info('SSL -> NON SSL')
SSL_RUNNING=False ssl_active = False
cert_file_hash = hash_cert_file()
if __name__ == '__main__': if __name__ == '__main__':
setup_logging() setup_logging()
logging.info('starting') logging.info('starting ssl endpoint')
if not os.path.exists(cert_path):
logging.info('creating cert_path path: %s', cert_path)
os.mkdir(cert_path)
client = Client(base_url='unix://var/run/docker.sock', version='1.15') client = Client(base_url='unix://var/run/docker.sock', version='1.15')
cert_file_hash = hash_cert_file()
# try to start in SSL mode, no problem if that fails # try to start in SSL mode, no problem if that fails
logging.info('try in SSL mode') logging.info('try in SSL mode')
SSL_RUNNING = start_haproxy_ssl() if not start_haproxy_ssl():
if not SSL_RUNNING:
logging.info('SSL mode failed') logging.info('SSL mode failed')
if not is_haproxy_running(): if not is_haproxy_running():
# tried to start haproxy and this failed, so we need to create a certificate and try again: # 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 # - start non ssl haproxy to be able to get a valid cert
logging.info('try in NON SSL mode') logging.info('try in NON SSL mode')
SSL_RUNNING = start_haproxy() start_haproxy()
# - get all domains # - get all domains
resolved_domains = list_domains.get_resolving_domains_from_containers(client) resolved_domains = list_domains.get_resolving_domains_from_containers(client)
# - create cert # - create cert
@ -152,10 +211,10 @@ if __name__ == '__main__':
line_str = line.decode("utf-8") line_str = line.decode("utf-8")
event = json.loads(line_str) event = json.loads(line_str)
if event['Action'] in ['start', 'destroy']: if event['Action'] in ['start', 'stop']:
# check if there is any domain name configured # check if there is any domain name configured
container_id = event['id'] container_id = event['id']
if len(list_domains.handle_container()) > 0: if len(list_domains.handle_container(client, container_id)) > 0:
resolved_domains = list_domains.get_resolving_domains_from_containers(client) resolved_domains = list_domains.get_resolving_domains_from_containers(client)
create_cert_data_standalone(resolved_domains) create_cert_data_standalone(resolved_domains)
create_haproxy_cert() create_haproxy_cert()