247 lines
9.1 KiB
Python
Raw Permalink Normal View History

2017-06-27 14:09:51 +02:00
import sys
import os
import errno
import time
import json
2018-08-05 00:00:19 +02:00
import docker
2018-08-05 00:27:22 +02:00
import threading
2018-08-04 21:52:49 +02:00
import argparse
from argparse import ArgumentTypeError as err
2017-06-27 14:09:51 +02:00
from base64 import b64decode
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from pathlib import Path
2018-08-04 22:35:22 +02:00
2018-08-04 21:52:49 +02:00
class PathType(object):
def __init__(self, exists=True, type='file', dash_ok=True):
'''exists:
True: a path that does exist
False: a path that does not exist, in a valid parent directory
None: don't care
type: file, dir, symlink, None, or a function returning True for valid paths
None: don't care
dash_ok: whether to allow "-" as stdin/stdout'''
assert exists in (True, False, None)
2018-08-04 22:35:22 +02:00
assert type in ('file', 'dir', 'symlink',
None) or hasattr(type, '__call__')
2018-08-04 21:52:49 +02:00
self._exists = exists
self._type = type
self._dash_ok = dash_ok
def __call__(self, string):
2018-08-04 22:35:22 +02:00
if string == '-':
2018-08-04 21:52:49 +02:00
# the special argument "-" means sys.std{in,out}
if self._type == 'dir':
2018-08-04 22:35:22 +02:00
raise err(
'standard input/output (-) not allowed as directory path')
2018-08-04 21:52:49 +02:00
elif self._type == 'symlink':
2018-08-04 22:35:22 +02:00
raise err(
'standard input/output (-) not allowed as symlink path')
2018-08-04 21:52:49 +02:00
elif not self._dash_ok:
raise err('standard input/output (-) not allowed')
else:
e = os.path.exists(string)
2018-08-04 22:35:22 +02:00
if self._exists == True:
2018-08-04 21:52:49 +02:00
if not e:
raise err("path does not exist: '%s'" % string)
if self._type is None:
pass
2018-08-04 22:35:22 +02:00
elif self._type == 'file':
2018-08-04 21:52:49 +02:00
if not os.path.isfile(string):
raise err("path is not a file: '%s'" % string)
2018-08-04 22:35:22 +02:00
elif self._type == 'symlink':
2018-08-04 21:52:49 +02:00
if not os.path.symlink(string):
raise err("path is not a symlink: '%s'" % string)
2018-08-04 22:35:22 +02:00
elif self._type == 'dir':
2018-08-04 21:52:49 +02:00
if not os.path.isdir(string):
raise err("path is not a directory: '%s'" % string)
elif not self._type(string):
raise err("path not valid: '%s'" % string)
else:
2018-08-04 22:35:22 +02:00
if self._exists == False and e:
2018-08-04 21:52:49 +02:00
raise err("path exists: '%s'" % string)
p = os.path.dirname(os.path.normpath(string)) or '.'
if not os.path.isdir(p):
raise err("parent path is not a directory: '%s'" % p)
elif not os.path.exists(p):
raise err("parent directory does not exist: '%s'" % p)
return string
2018-08-05 00:00:19 +02:00
def restartContainerWithDomains(domains):
client = docker.from_env()
container = client.containers.list(filters = {"label" : "com.github.SnowMB.traefik-certificate-extractor.restart_domain"})
for c in container:
restartDomains = str.split(c.labels["com.github.SnowMB.traefik-certificate-extractor.restart_domain"], ',')
2018-08-05 00:00:19 +02:00
if not set(domains).isdisjoint(restartDomains):
print('restarting container ' + c.id)
2018-08-05 01:05:20 +02:00
if not args.dry:
c.restart()
2018-08-05 00:48:19 +02:00
def createCerts(args):
# Read JSON file
2018-08-05 00:48:19 +02:00
data = json.loads(open(args.certificate).read())
# Determine ACME version
acme_version = 2 if 'acme-v02' in data['Account']['Registration']['uri'] else 1
# Find certificates
if acme_version == 1:
certs = data['DomainsCertificate']['Certs']
elif acme_version == 2:
certs = data['Certificates']
# Loop over all certificates
names = []
for c in certs:
if acme_version == 1:
name = c['Certificate']['Domain']
privatekey = c['Certificate']['PrivateKey']
fullchain = c['Certificate']['Certificate']
sans = c['Domains']['SANs']
elif acme_version == 2:
name = c['Domain']['Main']
privatekey = c['Key']
fullchain = c['Certificate']
sans = c['Domain']['SANs']
2018-08-05 00:58:32 +02:00
if (args.include and name not in args.include) or (args.exclude and name in args.exclude):
2018-08-05 00:48:19 +02:00
continue
# Decode private key, certificate and chain
privatekey = b64decode(privatekey).decode('utf-8')
fullchain = b64decode(fullchain).decode('utf-8')
start = fullchain.find('-----BEGIN CERTIFICATE-----', 1)
cert = fullchain[0:start]
chain = fullchain[start:]
2018-08-05 01:05:20 +02:00
if not args.dry:
# Create domain directory if it doesn't exist
directory = Path(args.directory)
if not directory.exists():
directory.mkdir()
2018-08-05 01:05:20 +02:00
if args.flat:
# Write private key, certificate and chain to flat files
with (directory / name + '.key').open('w') as f:
f.write(privatekey)
with (directory / name + '.crt').open('w') as f:
f.write(fullchain)
with (directory / name + '.chain.pem').open('w') as f:
f.write(chain)
if sans:
for name in sans:
with (directory / name + '.key').open('w') as f:
f.write(privatekey)
with (directory / name + '.crt').open('w') as f:
f.write(fullchain)
with (directory / name + '.chain.pem').open('w') as f:
f.write(chain)
else:
directory = directory / name
if not directory.exists():
directory.mkdir()
# Write private key, certificate and chain to file
with (directory / 'privkey.pem').open('w') as f:
f.write(privatekey)
2018-08-05 01:05:20 +02:00
with (directory / 'cert.pem').open('w') as f:
f.write(cert)
2018-08-05 01:05:20 +02:00
with (directory / 'chain.pem').open('w') as f:
f.write(chain)
2018-08-05 01:05:20 +02:00
with (directory / 'fullchain.pem').open('w') as f:
f.write(fullchain)
2018-08-04 22:35:22 +02:00
print('Extracted certificate for: ' + name +
(', ' + ', '.join(sans) if sans else ''))
names.append(name)
return names
2017-06-27 14:09:51 +02:00
class Handler(FileSystemEventHandler):
2018-08-04 21:52:49 +02:00
def __init__(self, args):
self.args = args
2018-08-05 00:27:22 +02:00
self.isWaiting = False
self.timer = threading.Timer(0.5, self.doTheWork)
self.lock = threading.Lock()
2017-06-27 14:09:51 +02:00
def on_created(self, event):
self.handle(event)
def on_modified(self, event):
self.handle(event)
def handle(self, event):
# Check if it's a JSON file
2018-08-04 22:35:22 +02:00
print('DEBUG : event fired')
if not event.is_directory and event.src_path.endswith(str(self.args.certificate)):
2017-06-27 14:09:51 +02:00
print('Certificates changed')
2018-08-05 00:27:22 +02:00
with self.lock:
if not self.isWaiting:
self.isWaiting = True #trigger the work just once (multiple events get fired)
2018-08-05 00:48:19 +02:00
self.timer = threading.Timer(2, self.doTheWork)
2018-08-05 00:27:22 +02:00
self.timer.start()
2018-08-05 01:16:20 +02:00
2018-08-05 00:27:22 +02:00
def doTheWork(self):
2018-08-05 00:48:19 +02:00
print('DEBUG : starting the work')
domains = createCerts(self.args)
2018-08-05 00:27:22 +02:00
if (self.args.restart_container):
restartContainerWithDomains(domains)
2018-08-05 00:48:19 +02:00
2018-08-05 00:27:22 +02:00
with self.lock:
self.isWaiting = False
2018-08-05 01:16:20 +02:00
print('DEBUG : finished')
2017-06-27 14:09:51 +02:00
if __name__ == "__main__":
2018-08-04 22:35:22 +02:00
parser = argparse.ArgumentParser(
description='Extract traefik letsencrypt certificates.')
2018-08-04 22:39:19 +02:00
parser.add_argument('-c', '--certificate', default='acme.json', type=PathType(
2018-08-04 22:35:22 +02:00
exists=True), help='file that contains the traefik certificates (default acme.json)')
2018-08-04 22:39:19 +02:00
parser.add_argument('-d', '--directory', default='.',
2018-08-04 22:35:22 +02:00
type=PathType(type='dir'), help='output folder')
parser.add_argument('-f', '--flat', action='store_true',
help='outputs all certificates into one folder')
parser.add_argument('-r', '--restart_container', action='store_true',
2018-08-05 19:22:54 +02:00
help="uses the docker API to restart containers that are labeled with 'com.github.SnowMB.traefik-certificate-extractor.restart_domain=<DOMAIN>' if the domain name of a generated certificates matches. Multiple domains can be seperated by ','")
2018-08-05 01:05:20 +02:00
parser.add_argument('--dry-run', action='store_true', dest='dry',
help="Don't write files and do not start docker containers.")
2018-08-05 00:48:19 +02:00
group = parser.add_mutually_exclusive_group()
group.add_argument('--include', nargs='*')
group.add_argument('--exclude', nargs='*')
2018-08-04 21:52:49 +02:00
args = parser.parse_args()
2017-06-27 14:09:51 +02:00
2018-08-04 22:48:28 +02:00
print('DEBUG: watching path: ' + str(args.certificate))
print('DEBUG: output path: ' + str(args.directory))
2017-06-27 14:09:51 +02:00
# Create event handler and observer
2018-08-04 21:52:49 +02:00
event_handler = Handler(args)
2017-06-27 14:09:51 +02:00
observer = Observer()
# Register the directory to watch
2018-08-04 22:48:28 +02:00
observer.schedule(event_handler, str(Path(args.certificate).parent))
2017-06-27 14:09:51 +02:00
# Main loop to watch the directory
observer.start()
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
observer.stop()
observer.join()