Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

sitegen.py 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  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_templates(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_sites(self):
  120. files = os.listdir(self.siteConfDir)
  121. files.sort()
  122. templates = []
  123. for file in files:
  124. if file.endswith(".conf"):
  125. templates.append(file[:-5])
  126. return templates
  127. def get_all_domains(self):
  128. domains = list(set(self.get_all_sites() + self.get_all_certs()))
  129. domains.sort()
  130. return domains
  131. def get_site_template_dir(self):
  132. return path.join(self.confDir, "templates")
  133. def get_site_conf_files(self, domain):
  134. return [
  135. path.join(self.siteConfDir, domain + ".conf"),
  136. path.join(self.siteConfDir, domain + ".include")
  137. ]
  138. def get_site_template_conf_files(self, template):
  139. return [
  140. path.join(self.get_site_template_dir(), template + ".conf"),
  141. path.join(self.get_site_template_dir(), template + ".include")
  142. ]
  143. def get_site_dir(self, domain):
  144. return path.abspath(path.join(self.siteDir, domain))
  145. def is_site_present(self, domain):
  146. for file in self.get_site_conf_files(domain):
  147. if path.isfile(file):
  148. return True
  149. return False
  150. def is_site_template_present(self, template):
  151. for file in self.get_site_template_conf_files(template):
  152. if path.isfile(file):
  153. return True
  154. return False
  155. def generate_site_conf_file(self, domain, template, outfile):
  156. with open(template) as f:
  157. content = f.read()
  158. content = content.replace("%%HOST%%", domain).replace("%%ROOT%%", self.get_site_dir(domain))
  159. with open(outfile, "w") as f:
  160. f.write(content)
  161. def cert_request(self, domain):
  162. cert_files = self.get_cert_files(domain)
  163. cert_files.insert(0, domain)
  164. self.execute_hooks("cert", "pre", cert_files)
  165. command = self.get_letsencrypt_command(domain)
  166. args = command['letsencryptArgs'].copy()
  167. args.append("-d")
  168. args.append(domain)
  169. res, out = self.execute(command['letsencryptCommand'], args, False)
  170. if res != 0:
  171. raise SiteGenException("Certificate request failed with code %i" % res, res)
  172. self.symlink_letsencrypt_file(domain, "cert.pem", domain + ".crt")
  173. self.symlink_letsencrypt_file(domain, "privkey.pem", domain + ".key")
  174. self.symlink_letsencrypt_file(domain, "chain.pem", domain + "-chain.crt")
  175. self.symlink_letsencrypt_file(domain, "fullchain.pem", domain + "-fullchain.crt")
  176. self.execute_hooks("cert", "post", cert_files)
  177. def certs_request(self, domains):
  178. for domain in domains:
  179. self.cert_request(domain)
  180. def cert_check(self, domain):
  181. if not self.is_cert_present(domain):
  182. raise SiteGenException("Certificate not present: %s" % domain, 1)
  183. if self.is_cert_gonna_expire(domain, self.certRenewTime):
  184. print("%s: %s" % (domain, self.get_cert_end_date(domain)))
  185. return True
  186. return False
  187. def certs_check(self, domains):
  188. for domain in domains:
  189. self.cert_check(domain)
  190. def cert_enddate(self, domain):
  191. if not self.is_cert_present(domain):
  192. raise SiteGenException("Certificate not present: %s" % domain, 1)
  193. print("%s: %s" % (domain, self.get_cert_end_date(domain)))
  194. def certs_enddate(self, domains):
  195. for domain in domains:
  196. self.cert_enddate(domain)
  197. def cert_renew(self, domain):
  198. if self.cert_check(domain):
  199. self.cert_request(domain)
  200. def certs_renew(self, domains):
  201. for domain in domains:
  202. self.cert_renew(domain)
  203. def site_create(self, domain, template):
  204. if self.is_site_present(domain):
  205. raise SiteGenException("Site is present", 1)
  206. if not self.is_site_template_present(template):
  207. raise SiteGenException("Template is not present", 1)
  208. args = [domain, self.get_site_dir(domain)]
  209. conf_files = self.get_site_template_conf_files(template)
  210. site_files = self.get_site_conf_files(domain)
  211. for f in conf_files:
  212. args.append(f)
  213. for f in site_files:
  214. args.append(f)
  215. self.execute_hooks("site", "pre", args)
  216. self.make_dirs_p(self.get_site_dir(domain))
  217. i = 0
  218. while i < len(conf_files):
  219. self.generate_site_conf_file(domain, conf_files[i], site_files[i])
  220. i += 1
  221. self.execute_hooks("site", "post", args)
  222. def hook_enable(self, hook_type, hook_name):
  223. if hook_type is None or not self.is_hook_present(hook_type, hook_name, False):
  224. raise SiteGenException("Hook is not present", 1)
  225. if self.is_hook_enabled(hook_type, hook_name):
  226. raise SiteGenException("Hook is already enabled", 0)
  227. print("Enabling %s %s" % (hook_type, hook_name))
  228. hook_dir = self.get_hook_dir(hook_type, hook_name)
  229. self.make_dirs_p(hook_dir)
  230. hook_file_available = self.get_hook_file(hook_type, hook_name, False)
  231. hook_file_enabled = self.get_hook_file(hook_type, hook_name, True)
  232. hook_relative_file = path.relpath(hook_file_available, self.get_hook_dir(hook_type, True))
  233. os.symlink(hook_relative_file, hook_file_enabled)
  234. def hook_disable(self, hook_type, hook_name):
  235. if hook_type is None or not self.is_hook_present(hook_type, hook_name, False):
  236. raise SiteGenException("Hook is not present", 1)
  237. if not self.is_hook_enabled(hook_type, hook_name):
  238. raise SiteGenException("Hook is not enabled", 0)
  239. print("Disabling %s %s" % (hook_type, hook_name))
  240. os.remove(self.get_hook_file(hook_type, hook_name, True))
  241. def parse_domain(domain):
  242. site_template = "default"
  243. if ":" in domain:
  244. split = domain.split(":")
  245. domain = split[0]
  246. site_template = split[1]
  247. return domain, site_template
  248. def parse_hook(hook):
  249. if "." in hook:
  250. split = hook.split(".")
  251. return split[0], split[1]
  252. return None, None
  253. def get_site_gen(prefix, **kwargs):
  254. with open(kwargs.get("parsed_args").config, "r") as f:
  255. config = json.load(f)
  256. return SiteGen(config)
  257. def cert_completer(prefix, **kwargs):
  258. site_gen = get_site_gen(prefix, **kwargs)
  259. return site_gen.get_all_certs()
  260. def domain_completer(prefix, **kwargs):
  261. site_gen = get_site_gen(prefix, **kwargs)
  262. return site_gen.get_all_domains()
  263. def site_template_completer(prefix, **kwargs):
  264. site_gen = get_site_gen(prefix, **kwargs)
  265. return site_gen.get_all_site_templates()
  266. def site_create_completer(prefix, **kwargs):
  267. if ":" in prefix:
  268. domain, template = parse_domain(prefix)
  269. templates = site_template_completer(prefix, **kwargs)
  270. return [domain + ":" + elt for elt in templates]
  271. return domain_completer(prefix, **kwargs)
  272. def hook_completer(prefix, **kwargs):
  273. site_gen = get_site_gen(prefix, **kwargs)
  274. return site_gen.get_all_certs()
  275. def main():
  276. parser = argparse.ArgumentParser(description='Manage apache websites and SSL certificates')
  277. parser.add_argument('--config', dest='config', default='/etc/sitegen/sitegen.json', help='Configuration file path')
  278. parser.add_argument('--cert-request', metavar='domain', const='', nargs='?',
  279. help='Request/renew a certificate. Request/renew all certificates if no domain is specified').completer = domain_completer
  280. parser.add_argument('--cert-check', metavar='domain', const='', nargs='?',
  281. help='Check if certificate needs to be renewed. Check all if no domain is specified').completer = cert_completer
  282. parser.add_argument('--cert-renew', metavar='domain', const='', nargs='?',
  283. help='Renew certificate if it needs to be. Renew all that needs to be if no domain is specified').completer = cert_completer
  284. parser.add_argument('--cert-enddate', metavar='enddate', const='', nargs='?',
  285. help='Print certificate enddate. Print all certificates enddate if no domain is specified').completer = cert_completer
  286. parser.add_argument('--site-create', help='Create a site configuration in the form domain[:template]',
  287. metavar='domain').completer = site_create_completer
  288. parser.add_argument('--hook-enable', help='Enable a site hook in the form (site|cert):hook_name',
  289. dest='hook_enable', metavar='hook').completer = hook_completer
  290. parser.add_argument('--hook-disable', help='Disable a site hook in the form (site|cert):hook_name',
  291. dest='hook_disable', metavar='hook').completer = hook_completer
  292. argcomplete.autocomplete(parser)
  293. args = parser.parse_args()
  294. with open(args.config, "r") as f:
  295. config = json.load(f)
  296. site_gen = SiteGen(config)
  297. site_gen.make_dirs()
  298. try:
  299. if args.cert_request is not None:
  300. if args.cert_request == "":
  301. site_gen.certs_request(site_gen.get_all_certs())
  302. else:
  303. site_gen.cert_request(args.cert_request)
  304. elif args.cert_check is not None:
  305. if args.cert_check == "":
  306. site_gen.certs_check(site_gen.get_all_certs())
  307. else:
  308. site_gen.cert_check(args.cert_check)
  309. elif args.cert_renew is not None:
  310. if args.cert_renew == "":
  311. site_gen.certs_renew(site_gen.get_all_certs())
  312. else:
  313. site_gen.cert_renew(args.cert_renew)
  314. elif args.cert_enddate is not None:
  315. if args.cert_enddate == "":
  316. site_gen.certs_enddate(site_gen.get_all_certs())
  317. else:
  318. site_gen.cert_enddate(args.cert_enddate)
  319. elif args.site_create is not None:
  320. domain, site_template = parse_domain(args.site_create)
  321. site_gen.site_create(domain, site_template)
  322. elif args.hook_enable is not None:
  323. hook_type, hook_name = parse_hook(args.hook_enable)
  324. site_gen.hook_enable(hook_type, hook_name)
  325. elif args.hook_disable is not None:
  326. hook_type, hook_name = parse_hook(args.hook_disable)
  327. site_gen.hook_disable(hook_type, hook_name)
  328. else:
  329. parser.print_help()
  330. except SiteGenException as e:
  331. print(e.error)
  332. exit(e.code)
  333. if __name__ == "__main__":
  334. main()