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.

157 lines
5.7 KiB

  1. # -*- coding: utf-8 -*-
  2. # © 2016 Therp BV <http://therp.nl>
  3. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
  4. import os
  5. import logging
  6. import urllib2
  7. import urlparse
  8. import subprocess
  9. import tempfile
  10. from openerp import _, api, models, exceptions
  11. from openerp.tools.config import _get_default_datadir
  12. from openerp.tools.misc import file_open
  13. from .acme_tiny import get_crt, DEFAULT_CA
  14. DEFAULT_KEY_LENGTH = 4096
  15. _logger = logging.getLogger(__name__)
  16. class Letsencrypt(models.AbstractModel):
  17. _name = 'letsencrypt'
  18. _description = 'Abstract model providing functions for letsencrypt'
  19. @api.model
  20. def get_data_dir(self):
  21. return os.path.join(_get_default_datadir(), 'letsencrypt')
  22. @api.model
  23. def call_cmdline(self, cmdline, loglevel=logging.INFO,
  24. raise_on_result=True):
  25. process = subprocess.Popen(
  26. cmdline, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  27. stdout, stderr = process.communicate()
  28. if stderr:
  29. _logger.log(loglevel, stderr)
  30. if stdout:
  31. _logger.log(loglevel, stdout)
  32. if process.returncode:
  33. raise exceptions.Warning(
  34. _('Error calling %s: %d') % (cmdline[0], process.returncode),
  35. ' '.join(cmdline),
  36. )
  37. return process.returncode
  38. @api.model
  39. def generate_account_key(self):
  40. data_dir = self.get_data_dir()
  41. if not os.path.isdir(data_dir):
  42. os.makedirs(data_dir)
  43. account_key = os.path.join(data_dir, 'account.key')
  44. if not os.path.isfile(account_key):
  45. _logger.info('generating rsa account key')
  46. self.call_cmdline([
  47. 'openssl', 'genrsa', '-out', account_key,
  48. str(DEFAULT_KEY_LENGTH),
  49. ])
  50. assert os.path.isfile(account_key), 'failed to create rsa key'
  51. return account_key
  52. @api.model
  53. def generate_domain_key(self, domain):
  54. domain_key = os.path.join(self.get_data_dir(), '%s.key' % domain)
  55. if not os.path.isfile(domain_key):
  56. _logger.info('generating rsa domain key for %s', domain)
  57. self.call_cmdline([
  58. 'openssl', 'genrsa', '-out', domain_key,
  59. str(DEFAULT_KEY_LENGTH),
  60. ])
  61. return domain_key
  62. @api.model
  63. def validate_domain(self, domain):
  64. if domain in ['localhost', '127.0.0.1'] or\
  65. domain.startswith('10.') or\
  66. domain.startswith('172.16.') or\
  67. domain.startswith('192.168.'):
  68. raise exceptions.Warning(
  69. _("Let's encrypt doesn't work with private addresses!"))
  70. @api.model
  71. def generate_csr(self, domain):
  72. domains = [domain]
  73. i = 0
  74. while self.env['ir.config_parameter'].get_param(
  75. 'letsencrypt.altname.%d' % i):
  76. domains.append(
  77. self.env['ir.config_parameter']
  78. .get_param('letsencrypt.altname.%d' % i)
  79. )
  80. i += 1
  81. _logger.info('generating csr for %s', domain)
  82. if len(domains) > 1:
  83. _logger.info('with alternative subjects %s', ','.join(domains[1:]))
  84. config = self.env['ir.config_parameter'].get_param(
  85. 'letsencrypt.openssl.cnf', '/etc/ssl/openssl.cnf')
  86. csr = os.path.join(self.get_data_dir(), '%s.csr' % domain)
  87. with tempfile.NamedTemporaryFile() as cfg:
  88. cfg.write(open(config).read())
  89. if len(domains) > 1:
  90. cfg.write(
  91. '\n[SAN]\nsubjectAltName=' +
  92. ','.join(map(lambda x: 'DNS:%s' % x, domains)) + '\n')
  93. cfg.file.flush()
  94. cmdline = [
  95. 'openssl', 'req', '-new',
  96. self.env['ir.config_parameter'].get_param(
  97. 'letsencrypt.openssl.digest', '-sha256'),
  98. '-key', self.generate_domain_key(domain),
  99. '-subj', '/CN=%s' % domain, '-config', cfg.name,
  100. '-out', csr,
  101. ]
  102. if len(domains) > 1:
  103. cmdline.extend([
  104. '-reqexts', 'SAN',
  105. ])
  106. self.call_cmdline(cmdline)
  107. return csr
  108. @api.model
  109. def cron(self):
  110. domain = urlparse.urlparse(
  111. self.env['ir.config_parameter'].get_param(
  112. 'web.base.url', 'localhost')).netloc
  113. self.validate_domain(domain)
  114. account_key = self.generate_account_key()
  115. csr = self.generate_csr(domain)
  116. manifest, manifest_path = file_open(
  117. 'letsencrypt/__openerp__.py', pathinfo=True)
  118. manifest.close()
  119. acme_challenge = os.path.join(
  120. os.path.dirname(manifest_path),
  121. 'static', 'acme-challenge')
  122. if not os.path.isdir(acme_challenge):
  123. os.makedirs(acme_challenge)
  124. if not self.env.context.get('letsencrypt_fake_cert'):
  125. crt_text = get_crt(
  126. account_key, csr, acme_challenge, log=_logger, CA=DEFAULT_CA)
  127. else:
  128. crt_text = 'I\'m a test text'
  129. with open(os.path.join(self.get_data_dir(), '%s.crt' % domain), 'w')\
  130. as crt:
  131. crt.write(crt_text)
  132. chain_cert = urllib2.urlopen(
  133. self.env['ir.config_parameter'].get_param(
  134. 'letsencrypt.chain_certificate_address',
  135. 'https://letsencrypt.org/certs/'
  136. 'lets-encrypt-x1-cross-signed.pem')
  137. )
  138. crt.write(chain_cert.read())
  139. chain_cert.close()
  140. _logger.info('wrote %s', crt.name)
  141. self.call_cmdline([
  142. 'sh', '-c', self.env['ir.config_parameter'].get_param(
  143. 'letsencrypt.reload_command', ''),
  144. ])