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 15KB

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