#!/usr/lib/rads/venv/bin/python3 """ AutoSSL Sync Created by Jeff Sh. Copy the SSLs files for a given domain to the provided folder so that it can be used by 3rd party applications and update as AutoSSL updates them. TODO: Only run if the ssl is newer than the current one, or otherwise invalid TODO: Check if self-signed, and prevent replacing DCV with self-signed """ # Imports import sys import os import argparse import errno import logging import logging.handlers import cpapis class MyVars: """ Main/Global variables can be defined here. """ VERSION = "1.0.1" LOG_FILENAME = '/var/log/autossl_sync.log' # Make sure the backup directory exists. If not, abort. def check_dir(dir_path, logger): """ Check to make sure the backup directory exists. If not, create it. Abort if there's a file that exists, but it's not a directory. """ if not os.path.isfile(dir_path): if not os.path.exists(dir_path): try: os.makedirs(dir_path) except OSError as error: if error.errno != errno.EEXIST: logger.error( f"Failed to create directory {dir_path}: {error}" ) else: logger.error( f"A failure occured while trying to create the \ directory {dir_path}." ) logger.error(f"Reported error: {error}") sys.exit(1) else: if not os.path.isdir(dir_path): logger.error( f"{dir_path} already exists, but it's not a directory." ) logger.error("ABORTING.") sys.exit(1) def copy_ssls(domain, folder, pem, logger): """ Given a domain name, we need to get the data to a point that we can use it. The Python JSON module imports the data as a dict, then it's a list, which then puts it into a dict again. So, we have to drill down to get to what we need. """ try: ssl_data = cpapis.whmapi1( "fetch_ssl_certificates_for_fqdns", {"domains": f"{domain}"} ) except IndexError as error: logger.error(f"Unable to load domain data for {domain} from whmapi1.") logger.error("Is the domain a valid domain on this server?") logger.error(f"Reported error was: {error}") sys.exit(1) # ssl_data keys are all located at data -> payload -> index 0 and then: # crt = Certificate # key = Private Key # cab = CA Bundle check_dir(folder, logger) with open(f"{folder}/{domain}.crt", "w", encoding='utf-8') as crt_file: crt_file.write(ssl_data['data']['payload'][0]['crt']) with open(f"{folder}/{domain}.key", "w", encoding='utf-8') as key_file: key_file.write(ssl_data['data']['payload'][0]['key']) with open(f"{folder}/{domain}.cab", "w", encoding='utf-8') as cab_file: cab_file.write(ssl_data['data']['payload'][0]['cab']) logger.info("Files written to the following paths:") logger.info(f"Certificate: {folder}/{domain}.crt") logger.info(f"Private Key: {folder}/{domain}.key") logger.info(f"CA Bundle : {folder}/{domain}.cab") if pem: convert_pem(domain, folder, logger) def convert_pem(domain, folder, logger): """ The PEM file is the CRT, KEY and CA Bundle in one file. Nothing more since the files in WHM are already X.509 formatted. """ cmd = f"cat {folder}/{domain}.crt > {folder}/{domain}.pem &&" cmd = f"cat {folder}/{domain}.key >> {folder}/{domain}.pem &&" cmd = f"cat {folder}/{domain}.cab >> {folder}/{domain}.pem " os.system(cmd) logger.info("PEM file format written to the following location:") logger.info(f"Privacy-Enhanced Mail (PEM): {folder}/{domain}.pem") def main(): """ We need to require the domain and the path to save the files to. No need to allow file names, we'll just use .crt, .key and .cab Set up parsing for the CLI arguments that can be passed and help text. """ # Optional Arguments parser = argparse.ArgumentParser( description=f"AutoSSL Sync v{MyVars.VERSION}" ) parser.add_argument( "--cron", action="store_true", dest="cron", default=False, help="Suppress all output, excluding errors, for CRON.", ) parser.add_argument( "-P", "--pem", action="store_true", dest="pem", default=False, help="Convert the files to PEM format after creation.", ) # Required Arguments required_arg = parser.add_argument_group('**REQUIRED arguments') required_arg.add_argument( "-d", "--domain", action='store', dest='domain', required=True, help="The FQDN domain to obtain SSL information on \ and sync. Subdomains can be used.", ) required_arg.add_argument( "-f", "--folder", action='store', dest='folder', required=True, help="The full path to the folder where the AutoSSL \ files should be copied to. Do not include a trailing slash!", ) # If no argument is passed, display help and exit. if len(sys.argv) == 1: parser.print_help(sys.stderr) print(__doc__) sys.exit(1) args = parser.parse_args() # Setup Logging - Rotate logs when 10MB, keep 6 copies and the current one. logging.basicConfig( level=logging.INFO, format="[%(asctime)s] - %(levelname)s - %(message)s", handlers=[ logging.handlers.RotatingFileHandler( MyVars.LOG_FILENAME, "a", 10485760, 6 ) ], ) logger = logging.getLogger(f'AutoSSL Sync v{MyVars.VERSION}') # Don't print to stdout when running with CRON argument, still send to log. cron_handler = logging.StreamHandler(sys.stdout) if not args.cron: logger.addHandler(cron_handler) else: logger.info("Running CRON Process for AutoSSL Sync v%s", MyVars.VERSION) copy_ssls(args.domain, args.folder, args.pem, logger) sys.exit(0) if __name__ == '__main__': main()