Edit: /opt/dedrads/autossl_sync (6267B)
#!/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()