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.conf /haproxy.conf
ADD letencrypt.conf /letencrypt.conf
ADD letsencrypt.conf /letsencrypt.conf
ADD start.py /start.py
ADD list_domains.py /list_domains.py

View File

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

View File

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

View File

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

165
start.py
View File

@ -12,128 +12,187 @@ import threading
import list_domains
from docker import Client
cert_path='/data/haproxy'
cert_file='/data/haproxy/cert.pem'
pid_file='/haproxy.pid'
cert_path = '/data/haproxy'
cert_file = cert_path + '/cert.pem'
pid_file = '/haproxy.pid'
delay = 10
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.
"""
if not os.path.isfile(cert_file):
return ''
aFile = open(cert_file, 'rb')
hasher = hashlib.sha256()
buf = aFile.read(65536)
file_obj = open(cert_file, 'rb')
hash_generator = hashlib.sha256()
buf = file_obj.read(65536)
while len(buf) > 0:
hasher.update(buf)
buf = aFile.read(65536)
return hasher.digest()
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 issueing a kill signal to its pid."""
logging.info('killing 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():
logging.info('starting haproxy SSL')
os.system('/usr/sbin/haproxy -f /haproxy_ssl.conf -p /haproxy.pid')
"""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():
logging.info('starting haproxy NON SSL')
os.system('/usr/sbin/haproxy -f /haproxy.conf -p /haproxy.pid')
return False
"""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)
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):
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():
SSL_RUNNING=True
ssl_active = ssl_possible() and is_haproxy_running()
cert_file_hash = hash_cert_file()
while True:
logging.info('ping')
time.sleep(60)
if ssl_possible() and not SSL_RUNNING:
kill_haproxy()
start_haproxy_ssl()
if is_haproxy_running():
cert_file_hash = hash_cert_file()
logging.info('NON SSL -> SSL')
SSL_RUNNING=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:
start_haproxy()
SSL_RUNNING=False
if SSL_RUNNING and cert_file_hash != hash_cert_file():
logging.info('cert has changed')
kill_haproxy()
start_haproxy_ssl()
if is_haproxy_running():
# 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()
logging.info('SSL -> SSL')
SSL_RUNNING=True
else:
start_haproxy()
logging.info('SSL -> NON SSL')
SSL_RUNNING=False
if __name__ == '__main__':
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')
cert_file_hash = hash_cert_file()
# try to start in SSL mode, no problem if that fails
logging.info('try in SSL mode')
SSL_RUNNING = start_haproxy_ssl()
if not SSL_RUNNING:
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')
SSL_RUNNING = start_haproxy()
start_haproxy()
# - get all domains
resolved_domains = list_domains.get_resolving_domains_from_containers(client)
# - create cert
@ -152,10 +211,10 @@ if __name__ == '__main__':
line_str = line.decode("utf-8")
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
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)
create_cert_data_standalone(resolved_domains)
create_haproxy_cert()