2019-10-01 15:22:03 +02:00
|
|
|
import csv
|
2019-09-22 19:30:39 +02:00
|
|
|
import os
|
|
|
|
|
|
|
|
|
|
import requests
|
|
|
|
|
|
|
|
|
|
from wo.core.fileutils import WOFileUtils
|
|
|
|
|
from wo.core.git import WOGit
|
|
|
|
|
from wo.core.logging import Log
|
2019-10-28 10:35:26 +01:00
|
|
|
from wo.core.shellexec import WOShellExec, CommandExecutionError
|
2019-10-02 13:13:32 +02:00
|
|
|
from wo.core.variables import WOVar
|
2023-08-13 10:24:29 +02:00
|
|
|
from wo.core.template import WOTemplate
|
2019-09-22 19:30:39 +02:00
|
|
|
|
|
|
|
|
|
2019-10-01 17:29:29 +02:00
|
|
|
class WOAcme:
|
2019-09-22 19:30:39 +02:00
|
|
|
"""Acme.sh utilities for WordOps"""
|
|
|
|
|
|
2019-10-01 15:22:03 +02:00
|
|
|
wo_acme_exec = ("/etc/letsencrypt/acme.sh --config-home "
|
|
|
|
|
"'/etc/letsencrypt/config'")
|
|
|
|
|
|
2019-12-03 19:48:18 +01:00
|
|
|
def check_acme(self):
|
|
|
|
|
"""
|
|
|
|
|
Check if acme.sh is properly installed,
|
|
|
|
|
and install it if required
|
|
|
|
|
"""
|
|
|
|
|
if not os.path.exists('/etc/letsencrypt/acme.sh'):
|
|
|
|
|
if os.path.exists('/opt/acme.sh'):
|
|
|
|
|
WOFileUtils.rm(self, '/opt/acme.sh')
|
|
|
|
|
WOGit.clone(
|
|
|
|
|
self, 'https://github.com/Neilpang/acme.sh.git',
|
|
|
|
|
'/opt/acme.sh', branch='master')
|
|
|
|
|
WOFileUtils.mkdir(self, '/etc/letsencrypt/config')
|
|
|
|
|
WOFileUtils.mkdir(self, '/etc/letsencrypt/renewal')
|
|
|
|
|
WOFileUtils.mkdir(self, '/etc/letsencrypt/live')
|
|
|
|
|
try:
|
|
|
|
|
WOFileUtils.chdir(self, '/opt/acme.sh')
|
|
|
|
|
WOShellExec.cmd_exec(
|
|
|
|
|
self, './acme.sh --install --home /etc/letsencrypt'
|
|
|
|
|
'--config-home /etc/letsencrypt/config'
|
|
|
|
|
'--cert-home /etc/letsencrypt/renewal'
|
|
|
|
|
)
|
|
|
|
|
WOShellExec.cmd_exec(
|
|
|
|
|
self, "{0} --upgrade --auto-upgrade"
|
|
|
|
|
.format(WOAcme.wo_acme_exec)
|
|
|
|
|
)
|
|
|
|
|
except CommandExecutionError as e:
|
|
|
|
|
Log.debug(self, str(e))
|
|
|
|
|
Log.error(self, "acme.sh installation failed")
|
|
|
|
|
if not os.path.exists('/etc/letsencrypt/acme.sh'):
|
|
|
|
|
Log.error(self, 'acme.sh ')
|
|
|
|
|
|
2019-10-01 15:22:03 +02:00
|
|
|
def export_cert(self):
|
|
|
|
|
"""Export acme.sh csv certificate list"""
|
2019-12-03 19:48:18 +01:00
|
|
|
# check acme.sh is installed
|
|
|
|
|
WOAcme.check_acme(self)
|
2020-10-14 18:17:06 +02:00
|
|
|
acme_list = WOShellExec.cmd_exec_stdout(
|
|
|
|
|
self, "{0} ".format(WOAcme.wo_acme_exec) +
|
|
|
|
|
"--list --listraw")
|
|
|
|
|
if acme_list:
|
|
|
|
|
WOFileUtils.textwrite(self, '/var/lib/wo/cert.csv', acme_list)
|
|
|
|
|
WOFileUtils.chmod(self, '/var/lib/wo/cert.csv', 0o600)
|
|
|
|
|
else:
|
2019-10-01 15:22:03 +02:00
|
|
|
Log.error(self, "Unable to export certs list")
|
|
|
|
|
|
2019-09-23 01:02:44 +02:00
|
|
|
def setupletsencrypt(self, acme_domains, acmedata):
|
2019-09-30 12:38:28 +02:00
|
|
|
"""Issue SSL certificates with acme.sh"""
|
2019-12-03 19:48:18 +01:00
|
|
|
# check acme.sh is installed
|
|
|
|
|
WOAcme.check_acme(self)
|
|
|
|
|
# define variables
|
2019-09-23 01:02:44 +02:00
|
|
|
all_domains = '\' -d \''.join(acme_domains)
|
2019-09-22 19:30:39 +02:00
|
|
|
wo_acme_dns = acmedata['acme_dns']
|
2019-09-30 15:05:07 +02:00
|
|
|
keylenght = acmedata['keylength']
|
2019-09-22 19:30:39 +02:00
|
|
|
if acmedata['dns'] is True:
|
|
|
|
|
acme_mode = "--dns {0}".format(wo_acme_dns)
|
|
|
|
|
validation_mode = "DNS mode with {0}".format(wo_acme_dns)
|
2019-09-24 02:36:46 +02:00
|
|
|
if acmedata['dnsalias'] is True:
|
|
|
|
|
acme_mode = acme_mode + \
|
|
|
|
|
" --challenge-alias {0}".format(acmedata['acme_alias'])
|
2019-09-22 19:30:39 +02:00
|
|
|
else:
|
|
|
|
|
acme_mode = "-w /var/www/html"
|
|
|
|
|
validation_mode = "Webroot challenge"
|
|
|
|
|
Log.debug(self, "Validation : Webroot mode")
|
2019-10-25 23:58:08 +02:00
|
|
|
if not os.path.isdir('/var/www/html/.well-known/acme-challenge'):
|
|
|
|
|
WOFileUtils.mkdir(
|
|
|
|
|
self, '/var/www/html/.well-known/acme-challenge')
|
|
|
|
|
WOFileUtils.chown(
|
|
|
|
|
self, '/var/www/html/.well-known', 'www-data', 'www-data',
|
|
|
|
|
recursive=True)
|
|
|
|
|
WOFileUtils.chmod(self, '/var/www/html/.well-known', 0o750,
|
|
|
|
|
recursive=True)
|
2019-09-22 19:30:39 +02:00
|
|
|
|
|
|
|
|
Log.info(self, "Validation mode : {0}".format(validation_mode))
|
|
|
|
|
Log.wait(self, "Issuing SSL cert with acme.sh")
|
2019-09-23 01:16:52 +02:00
|
|
|
if not WOShellExec.cmd_exec(
|
2019-10-01 17:29:29 +02:00
|
|
|
self, "{0} ".format(WOAcme.wo_acme_exec) +
|
2019-09-22 19:30:39 +02:00
|
|
|
"--issue -d '{0}' {1} -k {2} -f"
|
2019-09-23 01:16:52 +02:00
|
|
|
.format(all_domains, acme_mode, keylenght)):
|
2019-09-22 19:30:39 +02:00
|
|
|
Log.failed(self, "Issuing SSL cert with acme.sh")
|
|
|
|
|
if acmedata['dns'] is True:
|
2019-10-18 10:49:06 +02:00
|
|
|
Log.error(
|
2022-10-15 16:42:17 +02:00
|
|
|
self, "Please make sure you properly "
|
2019-11-11 19:06:11 +01:00
|
|
|
"set your DNS API credentials for acme.sh\n"
|
|
|
|
|
"If you are using sudo, use \"sudo -E wo\"")
|
2019-10-18 10:49:06 +02:00
|
|
|
return False
|
2019-09-22 19:30:39 +02:00
|
|
|
else:
|
2019-09-24 01:59:49 +02:00
|
|
|
Log.error(
|
|
|
|
|
self, "Your domain is properly configured "
|
|
|
|
|
"but acme.sh was unable to issue certificate.\n"
|
|
|
|
|
"You can find more informations in "
|
2019-10-18 10:49:06 +02:00
|
|
|
"/var/log/wo/wordops.log")
|
|
|
|
|
return False
|
2019-09-23 01:14:59 +02:00
|
|
|
else:
|
|
|
|
|
Log.valide(self, "Issuing SSL cert with acme.sh")
|
|
|
|
|
return True
|
2019-09-22 19:30:39 +02:00
|
|
|
|
|
|
|
|
def deploycert(self, wo_domain_name):
|
2019-10-26 19:28:56 +02:00
|
|
|
"""Deploy Let's Encrypt certificates with acme.sh"""
|
2019-12-03 19:48:18 +01:00
|
|
|
# check acme.sh is installed
|
|
|
|
|
WOAcme.check_acme(self)
|
2019-09-23 01:00:58 +02:00
|
|
|
if not os.path.isfile('/etc/letsencrypt/renewal/{0}_ecc/fullchain.cer'
|
2019-09-22 19:30:39 +02:00
|
|
|
.format(wo_domain_name)):
|
|
|
|
|
Log.error(self, 'Certificate not found. Deployment canceled')
|
|
|
|
|
|
|
|
|
|
Log.debug(self, "Cert deployment for domain: {0}"
|
|
|
|
|
.format(wo_domain_name))
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
Log.wait(self, "Deploying SSL cert")
|
|
|
|
|
if WOShellExec.cmd_exec(
|
|
|
|
|
self, "mkdir -p {0}/{1} && {2} --install-cert -d {1} --ecc "
|
|
|
|
|
"--cert-file {0}/{1}/cert.pem --key-file {0}/{1}/key.pem "
|
|
|
|
|
"--fullchain-file {0}/{1}/fullchain.pem "
|
|
|
|
|
"--ca-file {0}/{1}/ca.pem --reloadcmd \"nginx -t && "
|
|
|
|
|
"service nginx restart\" "
|
2019-10-02 13:13:32 +02:00
|
|
|
.format(WOVar.wo_ssl_live,
|
2019-10-01 17:29:29 +02:00
|
|
|
wo_domain_name, WOAcme.wo_acme_exec)):
|
2019-09-22 19:30:39 +02:00
|
|
|
Log.valide(self, "Deploying SSL cert")
|
|
|
|
|
else:
|
|
|
|
|
Log.failed(self, "Deploying SSL cert")
|
|
|
|
|
Log.error(self, "Unable to deploy certificate")
|
|
|
|
|
|
|
|
|
|
if os.path.isdir('/var/www/{0}/conf/nginx'
|
|
|
|
|
.format(wo_domain_name)):
|
|
|
|
|
|
2023-08-13 10:24:29 +02:00
|
|
|
data = dict(ssl_live_path=WOVar.wo_ssl_live,
|
|
|
|
|
domain=wo_domain_name)
|
|
|
|
|
WOTemplate.deploy(self,
|
|
|
|
|
'/var/www/{0}/conf/nginx/ssl.conf'
|
|
|
|
|
.format(wo_domain_name),
|
|
|
|
|
'ssl.mustache', data, overwrite=False)
|
2019-09-22 19:30:39 +02:00
|
|
|
|
|
|
|
|
if not WOFileUtils.grep(self, '/var/www/22222/conf/nginx/ssl.conf',
|
|
|
|
|
'/etc/letsencrypt'):
|
2019-09-30 12:38:28 +02:00
|
|
|
Log.info(self, "Securing WordOps backend with current cert")
|
2023-08-13 10:24:29 +02:00
|
|
|
data = dict(ssl_live_path=WOVar.wo_ssl_live,
|
|
|
|
|
domain=wo_domain_name)
|
|
|
|
|
WOTemplate.deploy(self,
|
|
|
|
|
'/var/www/22222/conf/nginx/ssl.conf',
|
|
|
|
|
'ssl.mustache', data, overwrite=False)
|
2019-09-22 19:30:39 +02:00
|
|
|
|
|
|
|
|
WOGit.add(self, ["/etc/letsencrypt"],
|
|
|
|
|
msg="Adding letsencrypt folder")
|
|
|
|
|
|
|
|
|
|
except IOError as e:
|
|
|
|
|
Log.debug(self, str(e))
|
|
|
|
|
Log.debug(self, "Error occured while generating "
|
|
|
|
|
"ssl.conf")
|
2019-10-01 04:08:54 +02:00
|
|
|
return 0
|
2019-09-24 01:59:49 +02:00
|
|
|
|
2019-10-29 18:47:52 +01:00
|
|
|
def renew(self, domain):
|
|
|
|
|
"""Renew letsencrypt certificate with acme.sh"""
|
2019-12-03 19:48:18 +01:00
|
|
|
# check acme.sh is installed
|
|
|
|
|
WOAcme.check_acme(self)
|
2019-10-29 18:47:52 +01:00
|
|
|
try:
|
|
|
|
|
WOShellExec.cmd_exec(
|
|
|
|
|
self, "{0} ".format(WOAcme.wo_acme_exec) +
|
|
|
|
|
"--renew -d {0} --ecc --force".format(domain))
|
|
|
|
|
except CommandExecutionError as e:
|
|
|
|
|
Log.debug(self, str(e))
|
|
|
|
|
Log.error(self, 'Unable to renew certificate')
|
|
|
|
|
return True
|
|
|
|
|
|
2019-09-24 01:59:49 +02:00
|
|
|
def check_dns(self, acme_domains):
|
|
|
|
|
"""Check if a list of domains point to the server IP"""
|
2019-11-11 19:06:11 +01:00
|
|
|
server_ip = requests.get('https://v4.wordops.eu/').text
|
2019-09-24 01:59:49 +02:00
|
|
|
for domain in acme_domains:
|
2019-12-10 00:24:01 +01:00
|
|
|
domain_ip = requests.get('http://v4.wordops.eu/dns/{0}/'
|
|
|
|
|
.format(domain)).text
|
2023-08-13 10:24:29 +02:00
|
|
|
if (not domain_ip == server_ip):
|
2019-09-24 01:59:49 +02:00
|
|
|
Log.warn(
|
2019-10-26 19:28:56 +02:00
|
|
|
self, "{0}".format(domain) +
|
|
|
|
|
" point to the IP {0}".format(domain_ip) +
|
|
|
|
|
" but your server IP is {0}.".format(server_ip) +
|
|
|
|
|
"\nUse the flag --force to bypass this check.")
|
2019-09-24 01:59:49 +02:00
|
|
|
Log.error(
|
2019-10-26 19:28:56 +02:00
|
|
|
self, "You have to set the "
|
|
|
|
|
"proper DNS record for your domain", False)
|
2019-09-24 01:59:49 +02:00
|
|
|
return False
|
2019-10-28 09:22:04 +01:00
|
|
|
Log.debug(self, "DNS record are properly set")
|
|
|
|
|
return True
|
2019-10-01 15:22:03 +02:00
|
|
|
|
|
|
|
|
def cert_check(self, wo_domain_name):
|
|
|
|
|
"""Check certificate existance with acme.sh and return Boolean"""
|
2019-10-01 17:29:29 +02:00
|
|
|
WOAcme.export_cert(self)
|
2020-01-17 11:50:31 +01:00
|
|
|
# set variable acme_cert
|
|
|
|
|
acme_cert = False
|
2019-10-01 15:22:03 +02:00
|
|
|
# define new csv dialect
|
|
|
|
|
csv.register_dialect('acmeconf', delimiter='|')
|
|
|
|
|
# open file
|
2020-10-14 15:29:06 +02:00
|
|
|
certfile = open('/var/lib/wo/cert.csv',
|
|
|
|
|
mode='r', encoding='utf-8')
|
2019-10-01 15:22:03 +02:00
|
|
|
reader = csv.reader(certfile, 'acmeconf')
|
|
|
|
|
for row in reader:
|
|
|
|
|
# check if domain exist
|
2019-11-05 16:11:43 +01:00
|
|
|
if wo_domain_name == row[0]:
|
2019-10-01 15:22:03 +02:00
|
|
|
# check if cert expiration exist
|
|
|
|
|
if not row[3] == '':
|
2020-01-14 16:30:08 +01:00
|
|
|
acme_cert = True
|
2019-10-01 15:22:03 +02:00
|
|
|
certfile.close()
|
2020-01-14 16:30:08 +01:00
|
|
|
if acme_cert is True:
|
|
|
|
|
if os.path.exists(
|
|
|
|
|
'/etc/letsencrypt/live/{0}/fullchain.pem'
|
|
|
|
|
.format(wo_domain_name)):
|
|
|
|
|
return True
|
2019-10-26 19:28:56 +02:00
|
|
|
return False
|
2019-10-28 10:35:26 +01:00
|
|
|
|
|
|
|
|
def removeconf(self, domain):
|
|
|
|
|
sslconf = ("/var/www/{0}/conf/nginx/ssl.conf"
|
|
|
|
|
.format(domain))
|
|
|
|
|
sslforce = ("/etc/nginx/conf.d/force-ssl-{0}.conf"
|
|
|
|
|
.format(domain))
|
2019-10-28 11:15:53 +01:00
|
|
|
acmedir = [
|
|
|
|
|
'{0}'.format(sslforce), '{0}'.format(sslconf),
|
|
|
|
|
'{0}/{1}_ecc'.format(WOVar.wo_ssl_archive, domain),
|
|
|
|
|
'{0}.disabled'.format(sslconf), '{0}.disabled'
|
|
|
|
|
.format(sslforce), '{0}/{1}'
|
|
|
|
|
.format(WOVar.wo_ssl_live, domain),
|
|
|
|
|
'/etc/letsencrypt/shared/{0}.conf'.format(domain)]
|
2019-10-28 10:35:26 +01:00
|
|
|
wo_domain = domain
|
2019-12-03 19:48:18 +01:00
|
|
|
# check acme.sh is installed
|
|
|
|
|
WOAcme.check_acme(self)
|
2019-10-28 10:35:26 +01:00
|
|
|
if WOAcme.cert_check(self, wo_domain):
|
|
|
|
|
Log.info(self, "Removing Acme configuration")
|
|
|
|
|
Log.debug(self, "Removing Acme configuration")
|
|
|
|
|
try:
|
|
|
|
|
WOShellExec.cmd_exec(
|
|
|
|
|
self, "{0} ".format(WOAcme.wo_acme_exec) +
|
|
|
|
|
"--remove -d {0} --ecc".format(domain))
|
|
|
|
|
except CommandExecutionError as e:
|
|
|
|
|
Log.debug(self, "{0}".format(e))
|
|
|
|
|
Log.error(self, "Cert removal failed")
|
2019-10-28 11:15:53 +01:00
|
|
|
# remove all files and directories
|
|
|
|
|
for dir in acmedir:
|
|
|
|
|
if os.path.exists('{0}'.format(dir)):
|
|
|
|
|
WOFileUtils.rm(self, '{0}'.format(dir))
|
2019-10-28 10:35:26 +01:00
|
|
|
# find all broken symlinks
|
|
|
|
|
WOFileUtils.findBrokenSymlink(self, "/var/www")
|
|
|
|
|
else:
|
|
|
|
|
if os.path.islink("{0}".format(sslconf)):
|
|
|
|
|
WOFileUtils.remove_symlink(self, "{0}".format(sslconf))
|
|
|
|
|
WOFileUtils.rm(self, '{0}'.format(sslforce))
|
|
|
|
|
|
|
|
|
|
if WOFileUtils.grepcheck(self, '/var/www/22222/conf/nginx/ssl.conf',
|
|
|
|
|
'{0}'.format(domain)):
|
|
|
|
|
Log.info(
|
|
|
|
|
self, "Setting back default certificate for WordOps backend")
|
|
|
|
|
with open("/var/www/22222/conf/nginx/"
|
|
|
|
|
"ssl.conf", "w") as ssl_conf_file:
|
|
|
|
|
ssl_conf_file.write("ssl_certificate "
|
|
|
|
|
"/var/www/22222/cert/22222.crt;\n"
|
|
|
|
|
"ssl_certificate_key "
|
2022-06-15 11:32:31 +02:00
|
|
|
"/var/www/22222/cert/22222.key;\n"
|
|
|
|
|
"ssl_stapling off;\n")
|