You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

sitegen.py 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. #! /usr/bin/env python3
  2. import fnmatch
  3. import json
  4. import argparse
  5. import os
  6. import subprocess
  7. import argcomplete
  8. from os import path
  9. class SiteGenException(Exception):
  10. error = None
  11. code = None
  12. def __init__(self, error, code):
  13. self.error = error
  14. self.code = code
  15. class SiteGen:
  16. siteConfDir = ""
  17. siteDir = ""
  18. confDir = ""
  19. hooksEnabledDir = ""
  20. hooksAvailableDir = ""
  21. templatesDir = ""
  22. certRenewTime = ""
  23. letsencryptCommands = ""
  24. letsencryptDir = ""
  25. certDir = ""
  26. def __init__(self, config):
  27. self.siteConfDir = config["siteConfDir"]
  28. self.siteDir = config["siteDir"]
  29. self.confDir = config["confDir"]
  30. self.hooksEnabledDir = path.join(self.confDir, "hooks-enabled")
  31. self.hooksAvailableDir = path.join(self.confDir, "hooks-available")
  32. self.templatesDir = path.join(self.confDir, "templates")
  33. self.certRenewTime = config["certRenewTime"]
  34. self.letsencryptCommands = config["letsencryptCommands"]
  35. self.letsencryptDir = config["letsencryptDir"]
  36. self.certDir = config["certDir"]
  37. def make_dirs_p(self, folder):
  38. if not path.isdir(folder):
  39. os.makedirs(folder)
  40. def make_dirs(self):
  41. self.make_dirs_p(self.certDir)
  42. self.make_dirs_p(self.siteConfDir)
  43. self.make_dirs_p(self.siteDir)
  44. def get_hook_dir(self, hook_type, is_enabled):
  45. return path.join(self.hooksEnabledDir if is_enabled else self.hooksAvailableDir, hook_type)
  46. def get_hook_file(self, hook_type, hook_name, is_enabled):
  47. return path.join(self.get_hook_dir(hook_type, is_enabled), hook_name)
  48. def get_hook_files(self, hook_type, is_enabled):
  49. hook_dir = self.get_hook_dir(hook_type, is_enabled)
  50. files = os.listdir(hook_dir)
  51. files.sort()
  52. return files
  53. def is_hook_present(self, hook_type, hook_name, is_enabled):
  54. return path.isfile(self.get_hook_file(hook_type, hook_name, is_enabled))
  55. def is_hook_enabled(self, hook_type, hook_name):
  56. return self.is_hook_present(hook_type, hook_name, True)
  57. def get_letsencrypt_dir(self, domain):
  58. return path.join(self.letsencryptDir, domain)
  59. def get_letsencrypt_command(self, domain):
  60. for d in self.letsencryptCommands:
  61. patterns = d['patterns'] if isinstance(d['patterns'], list) else [d['patterns']]
  62. for pattern in patterns:
  63. if fnmatch.fnmatch(domain, pattern):
  64. return d['command']
  65. return None
  66. def symlink_letsencrypt_file(self, domain, file, outfile):
  67. letsencrypt_cert_file = path.abspath(self.get_letsencrypt_dir(domain))
  68. my_cert_file = path.join(self.certDir, outfile)
  69. if path.lexists(my_cert_file):
  70. os.remove(my_cert_file)
  71. os.symlink(path.join(letsencrypt_cert_file, file), my_cert_file)
  72. def get_cert_files(self, domain):
  73. return [path.abspath(path.join(self.certDir, domain + ".crt")),
  74. path.abspath(path.join(self.certDir, domain + ".key")),
  75. path.abspath(path.join(self.certDir, domain + "-chain.crt"))]
  76. def execute(self, exe, args, get_output):
  77. args = args.copy()
  78. args.insert(0, exe)
  79. proc = subprocess.Popen(args, stdout=(subprocess.PIPE if get_output else None))
  80. out = proc.communicate()
  81. return proc.returncode, out[0]
  82. def execute_hooks(self, hook_type, hook_event, args):
  83. args = args.copy()
  84. args.insert(0, hook_event)
  85. for hook_name in self.get_hook_files(hook_type, True):
  86. self.execute(self.get_hook_file(hook_type, hook_name, True), args, False)
  87. def is_cert_present(self, domain):
  88. cert_files = self.get_cert_files(domain)
  89. for cert_file in cert_files:
  90. if not path.isfile(cert_file):
  91. return False
  92. return True
  93. def get_cert_end_date(self, domain):
  94. cert_files = self.get_cert_files(domain)
  95. res, out = self.execute("openssl", ["x509", "-noout", "-in", cert_files[0], "-enddate"], True)
  96. if res == 0:
  97. return out.decode("UTF-8").split("=")[1][:-1]
  98. return None
  99. def is_cert_gonna_expire(self, domain, checkend):
  100. cert_files = self.get_cert_files(domain)
  101. res, out = self.execute("openssl", ["x509", "-noout", "-in", cert_files[0], "-checkend", str(checkend)], True)
  102. return res == 1
  103. def get_all_certs(self):
  104. files = os.listdir(self.certDir)
  105. files.sort()
  106. domains = []
  107. for file in files:
  108. if file.endswith(".crt") and self.is_cert_present(file[:-4]):
  109. domains.append(file[:-4])
  110. return domains
  111. def get_all_site_confs(self):
  112. files = os.listdir(self.get_site_template_dir())
  113. files.sort()
  114. templates = []
  115. for file in files:
  116. if file.endswith(".conf"):
  117. templates.append(file[:-5])
  118. return templates
  119. def get_all_site_incs(self):
  120. files = os.listdir(self.get_site_template_dir())
  121. files.sort()
  122. templates = []
  123. for file in files:
  124. if file.endswith(".include"):
  125. templates.append(file[:-8])
  126. return templates
  127. def get_all_sites(self):
  128. files = os.listdir(self.siteConfDir)
  129. files.sort()
  130. templates = []
  131. for file in files:
  132. if file.endswith(".conf"):
  133. templates.append(file[:-5])
  134. return templates
  135. def get_all_domains(self):
  136. domains = list(set(self.get_all_sites() + self.get_all_certs()))
  137. domains.sort()
  138. return domains
  139. def get_site_template_dir(self):
  140. return path.join(self.confDir, "templates")
  141. def get_site_conf_files(self, domain):
  142. return [
  143. path.join(self.siteConfDir, domain + ".conf"),
  144. path.join(self.siteConfDir, domain + ".include")
  145. ]
  146. def get_site_template_conf_files(self, inc, conf):
  147. return [
  148. path.join(self.get_site_template_dir(), conf + ".conf"),
  149. path.join(self.get_site_template_dir(), inc + ".include")
  150. ]
  151. def get_site_dir(self, domain):
  152. return path.abspath(path.join(self.siteDir, domain))
  153. def is_site_present(self, domain):
  154. for file in self.get_site_conf_files(domain):
  155. if path.isfile(file):
  156. return True
  157. return False
  158. def is_site_template_present(self, inc, conf):
  159. for file in self.get_site_template_conf_files(inc, conf):
  160. if path.isfile(file):
  161. return True
  162. return False
  163. def generate_site_conf_file(self, domain, template, outfile):
  164. with open(template) as f:
  165. content = f.read()
  166. content = content.replace("%%HOST%%", domain).replace("%%ROOT%%", self.get_site_dir(domain))
  167. with open(outfile, "w") as f:
  168. f.write(content)
  169. def cert_request(self, domain):
  170. cert_files = self.get_cert_files(domain)
  171. cert_files.insert(0, domain)
  172. self.execute_hooks("cert", "pre", cert_files)
  173. command = self.get_letsencrypt_command(domain)
  174. args = command['letsencryptArgs'].copy()
  175. args.append("-d")
  176. args.append(domain)
  177. res, out = self.execute(command['letsencryptCommand'], args, False)
  178. if res != 0:
  179. raise SiteGenException("Certificate request failed with code %i" % res, res)
  180. self.symlink_letsencrypt_file(domain, "cert.pem", domain + ".crt")
  181. self.symlink_letsencrypt_file(domain, "privkey.pem", domain + ".key")
  182. self.symlink_letsencrypt_file(domain, "chain.pem", domain + "-chain.crt")
  183. self.symlink_letsencrypt_file(domain, "fullchain.pem", domain + "-fullchain.crt")
  184. self.execute_hooks("cert", "post", cert_files)
  185. def certs_request(self, domains):
  186. for domain in domains:
  187. self.cert_request(domain)
  188. def cert_check(self, domain):
  189. if not self.is_cert_present(domain):
  190. raise SiteGenException("Certificate not present: %s" % domain, 1)
  191. if self.is_cert_gonna_expire(domain, self.certRenewTime):
  192. print("%s: %s" % (domain, self.get_cert_end_date(domain)))
  193. return True
  194. return False
  195. def certs_check(self, domains):
  196. for domain in domains:
  197. self.cert_check(domain)
  198. def cert_enddate(self, domain):
  199. if not self.is_cert_present(domain):
  200. raise SiteGenException("Certificate not present: %s" % domain, 1)
  201. print("%s: %s" % (domain, self.get_cert_end_date(domain)))
  202. def certs_enddate(self, domains):
  203. for domain in domains:
  204. self.cert_enddate(domain)
  205. def cert_renew(self, domain):
  206. if self.cert_check(domain):
  207. self.cert_request(domain)
  208. def certs_renew(self, domains):
  209. for domain in domains:
  210. self.cert_renew(domain)
  211. def site_create(self, domain, inc, conf):
  212. if self.is_site_present(domain):
  213. raise SiteGenException("Site is present", 1)
  214. if not self.is_site_template_present(inc, conf):
  215. raise SiteGenException("Template is not present", 1)
  216. args = [domain, self.get_site_dir(domain)]
  217. conf_files = self.get_site_template_conf_files(inc, conf)
  218. site_files = self.get_site_conf_files(domain)
  219. for f in conf_files:
  220. args.append(f)
  221. for f in site_files:
  222. args.append(f)
  223. self.execute_hooks("site", "pre", args)
  224. self.make_dirs_p(self.get_site_dir(domain))
  225. i = 0
  226. while i < len(conf_files):
  227. self.generate_site_conf_file(domain, conf_files[i], site_files[i])
  228. i += 1
  229. self.execute_hooks("site", "post", args)
  230. def hook_enable(self, hook_type, hook_name):
  231. if hook_type is None or not self.is_hook_present(hook_type, hook_name, False):
  232. raise SiteGenException("Hook is not present", 1)
  233. if self.is_hook_enabled(hook_type, hook_name):
  234. raise SiteGenException("Hook is already enabled", 0)
  235. print("Enabling %s %s" % (hook_type, hook_name))
  236. hook_dir = self.get_hook_dir(hook_type, hook_name)
  237. self.make_dirs_p(hook_dir)
  238. hook_file_available = self.get_hook_file(hook_type, hook_name, False)
  239. hook_file_enabled = self.get_hook_file(hook_type, hook_name, True)
  240. hook_relative_file = path.relpath(hook_file_available, self.get_hook_dir(hook_type, True))
  241. os.symlink(hook_relative_file, hook_file_enabled)
  242. def hook_disable(self, hook_type, hook_name):
  243. if hook_type is None or not self.is_hook_present(hook_type, hook_name, False):
  244. raise SiteGenException("Hook is not present", 1)
  245. if not self.is_hook_enabled(hook_type, hook_name):
  246. raise SiteGenException("Hook is not enabled", 0)
  247. print("Disabling %s %s" % (hook_type, hook_name))
  248. os.remove(self.get_hook_file(hook_type, hook_name, True))
  249. def parse_domain(domain, default_inc="default", default_conf="https"):
  250. inc = default_inc
  251. conf = default_conf
  252. if ":" in domain:
  253. split = domain.split(":")
  254. domain = split[0]
  255. inc = split[1]
  256. if "." in inc:
  257. split = inc.split(".")
  258. inc = split[0]
  259. conf = split[1]
  260. return domain, inc, conf
  261. def parse_hook(hook):
  262. if "." in hook:
  263. split = hook.split(".")
  264. return split[0], split[1]
  265. return None, None
  266. def get_site_gen(prefix, **kwargs):
  267. with open(kwargs.get("parsed_args").config, "r") as f:
  268. config = json.load(f)
  269. return SiteGen(config)
  270. def cert_completer(prefix, **kwargs):
  271. site_gen = get_site_gen(prefix, **kwargs)
  272. return site_gen.get_all_certs()
  273. def domain_completer(prefix, **kwargs):
  274. site_gen = get_site_gen(prefix, **kwargs)
  275. return site_gen.get_all_domains()
  276. def site_conf_completer(prefix, **kwargs):
  277. site_gen = get_site_gen(prefix, **kwargs)
  278. return site_gen.get_all_site_confs()
  279. def site_inc_completer(prefix, **kwargs):
  280. site_gen = get_site_gen(prefix, **kwargs)
  281. return site_gen.get_all_site_incs()
  282. def site_create_completer(prefix, **kwargs):
  283. domain, inc, conf = parse_domain(prefix, None, None)
  284. if inc is not None:
  285. if conf is not None:
  286. templates = site_conf_completer(prefix, **kwargs)
  287. return [domain + ":" + inc + "." + elt for elt in templates]
  288. else:
  289. templates = site_inc_completer(prefix, **kwargs)
  290. return [domain + ":" + elt for elt in templates]
  291. return domain_completer(prefix, **kwargs)
  292. def hook_completer(prefix, **kwargs):
  293. site_gen = get_site_gen(prefix, **kwargs)
  294. return site_gen.get_all_certs()
  295. def main():
  296. parser = argparse.ArgumentParser(description='Manage apache websites and SSL certificates')
  297. parser.add_argument('--config', dest='config', default='/etc/sitegen/sitegen.json', help='Configuration file path')
  298. parser.add_argument('--cert-request', metavar='domain', const='', nargs='?',
  299. help='Request/renew a certificate. Request/renew all certificates if no domain is specified').completer = domain_completer
  300. parser.add_argument('--cert-check', metavar='domain', const='', nargs='?',
  301. help='Check if certificate needs to be renewed. Check all if no domain is specified').completer = cert_completer
  302. parser.add_argument('--cert-renew', metavar='domain', const='', nargs='?',
  303. help='Renew certificate if it needs to be. Renew all that needs to be if no domain is specified').completer = cert_completer
  304. parser.add_argument('--cert-enddate', metavar='enddate', const='', nargs='?',
  305. help='Print certificate enddate. Print all certificates enddate if no domain is specified').completer = cert_completer
  306. parser.add_argument('--site-create', help='Create a site configuration in the form domain[:template]',
  307. metavar='domain').completer = site_create_completer
  308. parser.add_argument('--hook-enable', help='Enable a site hook in the form (site|cert):hook_name',
  309. dest='hook_enable', metavar='hook').completer = hook_completer
  310. parser.add_argument('--hook-disable', help='Disable a site hook in the form (site|cert):hook_name',
  311. dest='hook_disable', metavar='hook').completer = hook_completer
  312. argcomplete.autocomplete(parser)
  313. args = parser.parse_args()
  314. with open(args.config, "r") as f:
  315. config = json.load(f)
  316. site_gen = SiteGen(config)
  317. site_gen.make_dirs()
  318. try:
  319. if args.cert_request is not None:
  320. if args.cert_request == "":
  321. site_gen.certs_request(site_gen.get_all_certs())
  322. else:
  323. site_gen.cert_request(args.cert_request)
  324. elif args.cert_check is not None:
  325. if args.cert_check == "":
  326. site_gen.certs_check(site_gen.get_all_certs())
  327. else:
  328. site_gen.cert_check(args.cert_check)
  329. elif args.cert_renew is not None:
  330. if args.cert_renew == "":
  331. site_gen.certs_renew(site_gen.get_all_certs())
  332. else:
  333. site_gen.cert_renew(args.cert_renew)
  334. elif args.cert_enddate is not None:
  335. if args.cert_enddate == "":
  336. site_gen.certs_enddate(site_gen.get_all_certs())
  337. else:
  338. site_gen.cert_enddate(args.cert_enddate)
  339. elif args.site_create is not None:
  340. domain, inc, conf = parse_domain(args.site_create)
  341. site_gen.site_create(domain, inc, conf)
  342. elif args.hook_enable is not None:
  343. hook_type, hook_name = parse_hook(args.hook_enable)
  344. site_gen.hook_enable(hook_type, hook_name)
  345. elif args.hook_disable is not None:
  346. hook_type, hook_name = parse_hook(args.hook_disable)
  347. site_gen.hook_disable(hook_type, hook_name)
  348. else:
  349. parser.print_help()
  350. except SiteGenException as e:
  351. print(e.error)
  352. exit(e.code)
  353. if __name__ == "__main__":
  354. main()