#!/usr/bin/python
# -*- coding: utf-8 -*-

# (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.

ANSIBLE_METADATA = {'metadata_version': '1.0',
                    'status': ['preview'],
                    'supported_by': 'community'}


DOCUMENTATION = '''
---
module: letsencrypt
author: "Michael Gruener (@mgruener)"
version_added: "2.2"
short_description: Create SSL certificates with Let's Encrypt
description:
   - "Create and renew SSL certificates with Let's Encrypt. Let’s Encrypt is a
      free, automated, and open certificate authority (CA), run for the
      public’s benefit. For details see U(https://letsencrypt.org). The current
      implementation supports the http-01, tls-sni-02 and dns-01 challenges."
   - "To use this module, it has to be executed at least twice. Either as two
      different tasks in the same run or during multiple runs."
   - "Between these two tasks you have to fulfill the required steps for the
      chosen challenge by whatever means necessary. For http-01 that means
      creating the necessary challenge file on the destination webserver. For
      dns-01 the necessary dns record has to be created. tls-sni-02 requires
      you to create a SSL certificate with the appropriate subjectAlternativeNames.
      It is I(not) the responsibility of this module to perform these steps."
   - "For details on how to fulfill these challenges, you might have to read through
      U(https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-7)"
   - "Although the defaults are chosen so that the module can be used with
      the Let's Encrypt CA, the module can be used with any service using the ACME
      protocol."
requirements:
  - "python >= 2.6"
  - openssl
options:
  account_key:
    description:
      - "File containing the the Let's Encrypt account RSA key."
      - "Can be created with C(openssl rsa ...)."
    required: true
  account_email:
    description:
      - "The email address associated with this account."
      - "It will be used for certificate expiration warnings."
    required: false
    default: null
  acme_directory:
    description:
      - "The ACME directory to use. This is the entry point URL to access
         CA server API."
      - "For safety reasons the default is set to the Let's Encrypt staging server.
         This will create technically correct, but untrusted certificates."
    required: false
    default: https://acme-staging.api.letsencrypt.org/directory
  agreement:
    description:
      - "URI to a terms of service document you agree to when using the
         ACME service at C(acme_directory)."
    required: false
    default: 'https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf'
  challenge:
    description: The challenge to be performed.
    required: false
    choices: [ 'http-01', 'dns-01', 'tls-sni-02']
    default: 'http-01'
  csr:
    description:
      - "File containing the CSR for the new certificate."
      - "Can be created with C(openssl csr ...)."
      - "The CSR may contain multiple Subject Alternate Names, but each one
         will lead to an individual challenge that must be fulfilled for the
         CSR to be signed."
    required: true
    aliases: ['src']
  data:
    description:
      - "The data to validate ongoing challenges."
      - "The value that must be used here will be provided by a previous use
         of this module.
"
    required: false
    default: null
  dest:
    description: The destination file for the certificate.
    required: true
    aliases: ['cert']
  remaining_days:
    description:
      - "The number of days the certificate must have left being valid.
         If C(cert_days < remaining_days), then it will be renewed.
         If the certificate is not renewed, module return values will not
         include C(challenge_data)."
    required: false
    default: 10
'''

EXAMPLES = '''
- letsencrypt:

    account_key: /etc/pki/cert/private/account.key
    csr: /etc/pki/cert/csr/sample.com.csr
    dest: /etc/httpd/ssl/sample.com.crt
  register: sample_com_challenge

# perform the necessary steps to fulfill the challenge
# for example:
#
# - copy:
#     dest: /var/www/html/{{ sample_com_challenge['challenge_data']['sample.com']['http-01']['resource'] }}
#     content: "{{ sample_com_challenge['challenge_data']['sample.com']['http-01']['resource_value'] }}"
#     when: sample_com_challenge|changed

- letsencrypt:
    account_key: /etc/pki/cert/private/account.key
    csr: /etc/pki/cert/csr/sample.com.csr
    dest: /etc/httpd/ssl/sample.com.crt
    data: "{{ sample_com_challenge }}"
'''

RETURN = '''
cert_days:
  description: the number of days the certificate remains valid.
  returned: success
  type: int
challenge_data:
  description: per domain / challenge type challenge data
  returned: changed
  type: complex
  contains:
    resource:
      description: the challenge resource that must be created for validation
      returned: changed
      type: string
      sample: .well-known/acme-challenge/evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA
    resource_value:
      description: the value the resource has to produce for the validation
      returned: changed
      type: string
      sample: IlirfxKKXA...17Dt3juxGJ-PCt92wr-oA
authorizations:
  description: ACME authorization data.
  returned: changed
  type: complex
  contains:
      authorization:
        description: ACME authorization object. See https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.1.2
        returned: success
        type: dict
'''

import binascii
import copy
import locale
import textwrap
from datetime import datetime


def nopad_b64(data):
    return base64.urlsafe_b64encode(data).decode('utf8').replace("=", "")


def simple_get(module, url):
    resp, info = fetch_url(module, url, method='GET')

    result = None
    try:
        content = resp.read()
    except AttributeError:
        if info['body']:
            content = info['body']

    if content:
        if info['content-type'].startswith('application/json'):
            try:
                result = module.from_json(content.decode('utf8'))
            except ValueError:
                module.fail_json(msg="Failed to parse the ACME response: {0} {1}".format(url, content))
        else:
            result = content

    if info['status'] >= 400:
        module.fail_json(msg="ACME request failed: CODE: {0} RESULT: {1}".format(info['status'], result))
    return result


def get_cert_days(module, cert_file):
    '''
    Return the days the certificate in cert_file remains valid and -1
    if the file was not found.
    '''
    if not os.path.exists(cert_file):
        return -1

    openssl_bin = module.get_bin_path('openssl', True)
    openssl_cert_cmd = [openssl_bin, "x509", "-in", cert_file, "-noout", "-text"]
    _, out, _ = module.run_command(openssl_cert_cmd, check_rc=True)
    try:
        not_after_str = re.search(r"\s+Not After\s*:\s+(.*)", out.decode('utf8')).group(1)
        not_after = datetime.datetime.fromtimestamp(time.mktime(time.strptime(not_after_str, '%b %d %H:%M:%S %Y %Z')))
    except AttributeError:
        module.fail_json(msg="No 'Not after' date found in {0}".format(cert_file))
    except ValueError:
        module.fail_json(msg="Failed to parse 'Not after' date of {0}".format(cert_file))
    now = datetime.datetime.utcnow()
    return (not_after - now).days


# function source: network/basics/uri.py
def write_file(module, dest, content):
    '''
    Write content to destination file dest, only 