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
2018-08-04 19:28:41 +02:00
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 ) :
2018-08-04 23:26:46 +02:00
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 ) :
2018-08-04 23:26:46 +02:00
print ( ' restarting container ' + c . id )
2018-08-05 01:05:20 +02:00
if not args . dry :
c . restart ( )
2018-08-04 19:28:41 +02:00
2018-08-05 00:48:19 +02:00
def createCerts ( args ) :
2018-08-04 19:28:41 +02:00
# Read JSON file
2018-08-05 00:48:19 +02:00
data = json . loads ( open ( args . certificate ) . read ( ) )
2018-08-04 19:28:41 +02:00
# 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
2018-08-04 23:26:46 +02:00
names = [ ]
2018-08-04 19:28:41 +02:00
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
2018-08-04 19:28:41 +02:00
# 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 )
2018-08-04 23:26:46 +02:00
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-04 23:26:46 +02:00
2018-08-05 01:05:20 +02:00
with ( directory / ' cert.pem ' ) . open ( ' w ' ) as f :
f . write ( cert )
2018-08-04 23:26:46 +02:00
2018-08-05 01:05:20 +02:00
with ( directory / ' chain.pem ' ) . open ( ' w ' ) as f :
f . write ( chain )
2018-08-04 23:26:46 +02:00
2018-08-05 01:05:20 +02:00
with ( directory / ' fullchain.pem ' ) . open ( ' w ' ) as f :
f . write ( fullchain )
2018-08-04 19:28:41 +02:00
2018-08-04 22:35:22 +02:00
print ( ' Extracted certificate for: ' + name +
( ' , ' + ' , ' . join ( sans ) if sans else ' ' ) )
2018-08-04 23:26:46 +02:00
names . append ( name )
return names
2018-08-04 19:28:41 +02:00
2017-06-27 14:09:51 +02:00
class Handler ( FileSystemEventHandler ) :
2018-08-04 19:28:41 +02:00
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 ( )
2018-08-04 19:28:41 +02:00
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 ' )
2018-08-04 23:26:46 +02:00
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
2018-08-04 19:28:41 +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 ' )
2018-08-04 23:26:46 +02:00
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 ( )