#!/usr/bin/env python3

import sys
import os
import yaml
import re

# Add /usr/share/python-stanford-incommoncert/lib to the python path so we can
# find the incommon.py module
sys.path.append('/usr/share/python3-stanford-incommoncert/lib')

from incommon import Certificate, InCommonWS, InCommonReporting

#################################################################

VERSION = "0.15"
script_name = os.path.basename(os.path.abspath(__file__))

#################################################################
def make_incommonws(arg_results):
  incommonws = InCommonWS()

  # Set "test" mode
  incommonws.setTestMode(arg_results.test)

  # If a credentials file was passed, use that to
  # set username and password. Otherwise, look for the username and
  # password arguments.
  if (not(arg_results.credentials_file is None)):
    cred_fh = open(arg_results.credentials_file, 'r')
    dataMap = yaml.load(cred_fh, Loader=yaml.SafeLoader)
    cred_fh.close()

    incommonws.password  = dataMap['password']
    incommonws.username  = dataMap['username']
    incommonws.secretKey = dataMap['secretkey']
  else:
    incommonws.username = arg_results.username
    if arg_results.password is None:
      import getpass
      incommonws.password = getpass.getpass()
    else:
      incommonws.password = arg_results.password

  return incommonws
#################################################################
def get_common_name_from_csr(csr_string):
  import OpenSSL.crypto

  x509req = OpenSSL.crypto.load_certificate_request(
      OpenSSL.crypto.FILETYPE_PEM, csr_string)

  components = (x509req.get_subject()).get_components()
  for pair in components:
    field = pair[0].decode('utf-8')
    value = pair[1].decode('utf-8')
    if (field == 'CN'):
      return value

  # If we get here, then we did not find CN
  raise Exception('no common name found')

#################################################################
def make_certificate(args):
  incommonws = make_incommonws(args)

  certificate = Certificate()

  if (incommonws.test_mode):
    certificate.set_test()
  else:
    certificate.set_prod()

  certificate.username  = incommonws.username
  certificate.password  = incommonws.password
  certificate.secretKey = incommonws.secretKey

  return certificate

#################################################################
def request(args):

  # 1. Create an Incommon Certificate object
  certificate = make_certificate(args)

  # 2. Add the Subject Alternative Names. The passed in subject alternative
  # names argument should be a comma-delimited list of fully-qualified
  # domains names.
  #
  # Question: can you have a certificate that is a wildcard cert AND has
  # SANs? Answer: No. Currently, InCommon does not allow this. The way
  # around this is to put the non-wildcard name in the CN and the wildcard
  # as a SAN.
  names = args.subject_alternative_names.split(',')
  for name in names:
    name = name.strip()
    if (name != ""):
      # Check that name is a valid hostname. We allow the name to start
      # with '*.'.
      match = re.search(r'^(\*\.)?([\w\-]+\.)*([\w\-]+)$', name)
      if match:
        # print 'name ' + name + ' is valid'
        certificate.add_subjAltName(name) # Implicitly sets cert type.
      else:
        raise Exception('subject alternative name \'' + name +
                        ' is not valid')

  # 3. If the wildcard option is set, change the certificate type.
  if (args.wildcard):
    certificate.localType = 'wildcard'

  # 4. If --ecc is set, set the uses_ecc attribute to true.
  if (args.ecc):
    certificate.uses_ecc = True

  # 5. Set the server type
  certificate.serverType = certificate.SERVER_TYPES[args.server_type]

  # 6. Set the term (duration)
  certificate.term = args.term

  # 7. Set the signature hash type. We currently only recognize two
  # signature hash types: sha1 and sha256.
  sigHashType = args.sig_hash.lower()

  if (sigHashType == 'sha256'):
    certificate.setSignatureHashType('sha256')
  elif (sigHashType == 'sha2'):
    certificate.setSignatureHashType('sha256')
  elif (sigHashType == 'sha1'):
    certificate.setSignatureHashType('sha1')
  else:
    raise Exception("unrecognized signature hash type " + sigHashType)

  # 8. Add the csr string to the certificate object.
  csr_file_string = args.csr_file
  csr_string = None

  if (csr_file_string is None):
    raise Exception("no CSR file found")
  else:
    csr_file = open(csr_file_string, 'r')
    csr_string = csr_file.read()

  certificate.csr = csr_string
  # print certificate.csr
  # print certificate.to_s()



  # 9. If we can extract a common name from the CSR, then the common name
  # has an asterisk ('*') in it if, and only if, the certificate type is
  # set to wildcard.
  common_name = None
  try:
    common_name = get_common_name_from_csr(csr_string)
  except:
    # We don't care if we get an exception.
    print("warning: no common name extracted from CSR")
    pass

  if (common_name is not None):
    cn_contains_asterisk = re.match(r'\*', common_name)
    if (cn_contains_asterisk and (not args.wildcard)):
      raise Exception('the common name contains a wildcard but the ' +
                      '--wildcard flag was not set!')
    elif ((not cn_contains_asterisk) and args.wildcard):
      raise Exception('the common name does not contain an asterisk but the ' +
                      '--wildcard flag was set!')

  # 10. Login to the InCommon web service and populate the certificate types
  certificate.login()

  # 11. Enroll
  certificate.enroll()

  # 12. Get the order number
  print(certificate.order_number)

  return
#################################################################
# Returns:
#   * ready (when certificate is ready to be downloaded)
#   * not ready: <reason>
#     where <reason> is one of
#     - submitted but not yet approved
#     - approved but not yet fullfilled
def status(args):
  # 1. Create an Incommon Certificate object
  certificate = make_certificate(args)

  # 2. Add order number
  certificate.order_number = args.order_number

  # 3. Login to the InCommon web service and populate the certificate types
  certificate.login()

  # 4. Get status
  status = certificate.status()

  if (status == "new"):
    print("not ready: submitted but not yet approved")
  elif (status == "approved"):
    print("not ready: approved but not yet fulfilled")
  elif (status == "declined"):
    print("not ready: declined")
  elif (status == "fulfilled"):
    print("ready")
  else:
    raise Exception("unknown status: " + status)

#################################################################
def server_types(args):
  # Fake some of the arguments to avoid errors.
  args.username = "fake"
  args.password = "fake"
  args.credentials_file = None
  args.test = True

  # 1. Create an Incommon Certificate object
  certificate = make_certificate(args)

  server_type_names = sorted(certificate.SERVER_TYPES.keys(), key=str.lower)

  # 2. Display the server types
  for server_type in server_type_names:
    print(server_type)

  return

#################################################################
def cert_types(args):
  # 1. Create an Incommon Certificate object
  certificate = make_certificate(args)

  # 2. Login to the InCommon web service and populate the certificate types
  certificate.login()

  # 3. Display the certificate types (no particular order)
  for cert_type in certificate.cert_types:
    print(cert_type.name)

  return
#################################################################
def availibility(arg_results):
  # 1. Get the order number
  order_number = arg_results.order_number
  if (order_number is None):
    raise Exception("cannot check availibilty unless you supply an order number")

  # 1. Create an Incommon Certificate object
  certificate = make_certificate(arg_results)
  certificate.order_number = order_number

  # 2. Login to the InCommon web service and populate the certificate types
  certificate.login()

  # 3. Check availibility
  result = certificate.isAvailable()

  if (result == 1):
    return True
  elif (result == 0):
    return False
  else:
    raise Exception("invalid response from isAvailable: " + str(result))
#################################################################
def collect(arg_results):
  # 1. Get the order number
  order_number = arg_results.order_number
  if (order_number is None):
    raise Exception("cannot collect unless you supply an order number")

  # 2. Get the format type; verify it is a valid type
  format = arg_results.format

  formatTypes = {
           'x509_bundle':            0,
           'x509_cert_only':         1,
           'x509_intermediate_only': 2,
           'pkcs7_bundle_pem':       3,
           'pkcs7_bundle_der':       4,
  }

  formatTypeId = None
  if (not (format in formatTypes)):
    raise Exception("unrecognized format: " + format)
  else:
    formatTypeId = formatTypes[format]

  # 3. Create an Incommon Certificate object
  certificate = make_certificate(arg_results)
  certificate.order_number = order_number

  # 2. Login to the InCommon web service and populate the certificate types
  certificate.login()

  # 3. Collect
  result_SSL = certificate.collect(formatTypeId)

  certificates_string = result_SSL.certificate

  print(certificates_string)
  print("renewID: " + result_SSL.renewID)

#################################################################
def revoke(arg_results):
  # 1. Get the order number
  order_number = arg_results.order_number
  if (order_number is None):
    raise Exception("cannot revoke unless you supply an order number")

  # 2. Get the reason
  reason = arg_results.reason

  # 3. Create an Incommon Certificate object
  certificate = make_certificate(arg_results)
  certificate.order_number = order_number

  # 4. Login to the InCommon web service.
  certificate.login()

  # 5. Revoke
  result_revoke = certificate.revoke(reason)

  if (result_revoke == 0):
    print("revocation succeeded")
  else:
    print("revocation failed")

#################################################################
def report(arg_results):
  # Make an incommonws object (gets us username and password)
  incommonws = make_incommonws(args)

  certificate = make_certificate(args)

  # Create an Incommon Report object
  report = InCommonReporting()

  report.username = certificate.username
  report.password = certificate.password
  report.URI      = certificate.URI

  # 2. Login to the InCommon Reporting service
  report.login()

  # 3.
  start_date = arg_results.start_date
  end_date   = arg_results.end_date

  report.getSSLReport(start_date, end_date)


#################################################################
def help_(args):

  # ## ###  # ## ###  # ## ###  # ## ###  # ## ###
  help_string_overview = """
This script helps with certificate lifecycle management.

1. Request (enroll)
2. Collect
3. Renew
4. Revoke (if needed)

For more help, run the help command.
"""

  # ## ###  # ## ###  # ## ###  # ## ###  # ## ###
  help_string_credentials = """
Credentials must be passed in one of the following ways:
  --username joeuser --password secret
or
  --credentials-file <path_to_file>
(If you use both methods --credentials_file takes precedence.)

If you use the --credentials-file option, the file format should be this:
username: joeuser
password: secret
"""

  # ## ###  # ## ###  # ## ###  # ## ###  # ## ###
  help_string_help = """
Display help on an action.
Usage:
  %s help <action>
where <action> is one of
  - help
  - request
  - status
  - collect
  - server-types
  - cert-types
  - renew
  - revoke
  - report

Examples:
  %s help help
  %s help request
  %s help         (same as 'help help')
""" % (script_name, script_name, script_name, script_name)

  # ## ###  # ## ###  # ## ###  # ## ###  # ## ###
  help_string_request = """
Purpose: request an InCommon/Comodo certificate
Usage:   %s request <credentials> <csr_file>
Options: --test (send request to InCommon test certificate server)
         --server-type <server-type> (web server type (default='Apache/ModSSL'))
           to get a list of all server types, run 'incommon-mgr server-types'
         --subject-alternative-names <comma-delimited list>
         --wildcard
         --ecc
         --sig-hash [sha1|sha256] (default=sha256)
         --term <expiration_in_years> (number of years before expiration; must be 1 or 2 (default=2))
Returns: order certificate number (e.g., 123456)
""" % (script_name) + help_string_credentials

  # ## ###  # ## ###  # ## ###  # ## ###  # ## ###
  help_string_status = """
Purpose: Get the status of an InCommon/Comodo certificate request
Usage:   %s status <order_number> <credentials>
Options: --test (send request to InCommon test certificate server)
Returns: one of
 'ready' (certificate ready for downloading)
 'not ready: approved but not yet fulfilled'
 'not ready: submitted but not yet approved'
 'not ready: declined'
""" % (script_name) + help_string_credentials

  # ## ###  # ## ###  # ## ###  # ## ###  # ## ###
  help_string_collect = """
Purpose: Collect (download) an InCommon/Comodo certificate
Usage:   %s collect <order_number> <credentials>
Options: --test (collect from the InCommon test certificate server)
         --format (format of downloaded certificates) (default: x509_bundle)
Returns: the certificate to stdout

The format option can take one of these values:
  x509_bundle              certificate and intermediate certs (default)
  x509_cert_only           certificate only
  x509_intermediate_only   intermediate certs only
  pkcs7_bundle_pem         PKCS#7 PEM Bundle (?)
  pkcs7_bundle_der         PKCS#7 DER Bundle
""" % (script_name) + help_string_credentials

  # ## ###  # ## ###  # ## ###  # ## ###  # ## ###
  help_string_revoke = """
Purpose: Revoke an InCommon/Comodo certificate
Usage:   %s revoke <order_number> <reason> <credentials>
Options: --test (collect from the InCommon test certificate server)
Returns: Nothing

The <reason> should be a string of not more than 250 characters.
""" % (script_name) + help_string_credentials

  # ## ###  # ## ###  # ## ###  # ## ###  # ## ###
  help_string_report = """
Purpose: Generate a report on all InCommon certificate objects
Usage:   %s report <start_date> <finish_date> <credentials>
Options: --test (collect from the InCommon test certificate server)
         --format (format of downloaded certificates) (default: x509_bundle)
         --start-date
         --end-date
         download all certs enrolled after --start-date but before --end-date
Returns: a JSON string

The start and end dates should have the format YYYY-MM-DD
""" % (script_name) + help_string_credentials

  # ## ###  # ## ###  # ## ###  # ## ###  # ## ###
  help_string_server_types = """
Usage: %s server-types
Returns: list of valid server types
""" % (script_name)


  # ## ###  # ## ###  # ## ###  # ## ###  # ## ###
  help_string_cert_types = """
Usage: %s cert-types
Returns: list of valid certificate types (makes InCommon API call)
Options: --test (collect from the InCommon test certificate server)
""" % (script_name) + help_string_credentials

  help_action_to_string = {
    'help':          help_string_help,
    'request':       help_string_request,
    'status':        help_string_status,
    'collect':       help_string_collect,
    'server-types':  help_string_server_types,
    'cert-types':    help_string_cert_types,
    'report':        help_string_report,
    'revoke':        help_string_revoke,
  }

  print(help_action_to_string[args.action])


#################################################################

# actions:
#   - request
#   - collect
#   - status
#   - revoke
#   - renew  (later)
#   - report (later)

# Define and parse the command-line parameters
import argparse
epilog = """
Request an InCommon SSL certificate.
%s <action> [options]
action is one of:
  * request
  * collect
  * revoke
  * status

help action
request csr_file
collect order_number
status order_number
revoke order_number reason
renew renewal_id

Examples:
%s request --username joeuser --password secret --csr host.csr
%s collect --username joeuser --password secret --order-number 123456

""" % (script_name, script_name, script_name)

# create the top-level parser
parser = argparse.ArgumentParser()

subparsers = parser.add_subparsers()

# The help command subparse
parser_help = subparsers.add_parser('help')
parser_help.add_argument('action', nargs='?', default='help')
parser_help.set_defaults(func=help_)


# The request command subparse
parser_request = subparsers.add_parser('request')
parser_request.add_argument('csr_file')
parser_request.add_argument('--test', action='store_true', help='make calls against test instance')
parser_request.add_argument('--username', action='store', dest='username',
                    help='InCommon username')
parser_request.add_argument('--password', action='store', dest='password',
                    help='InCommon password')
parser_request.add_argument('--credentials-file', action='store', dest='credentials_file',
                    help='credentials file')
parser_request.add_argument('--server-type', action='store', dest='server_type',
                    default='Apache/ModSSL', help='server type')
parser_request.add_argument('--subject-alternative-names', action='store',
                    dest='subject_alternative_names',
                    default='', help='subject alternative names (comma-delimited)')
parser_request.add_argument('--wildcard', action='store_true',
                    help='this is a request for a wildcard certificate')
parser_request.add_argument('--ecc', action='store_true',
                    help='this is a request for an ECC certificate')
parser_request.add_argument('--term', action='store', type=int, dest='term',
                    default=2, help='certificate term')
parser_request.add_argument('--sig-hash', action='store', dest='sig_hash',
                    default='sha256', help='certificate signature hash')
parser_request.set_defaults(func=request)

# The status command subparse
parser_status = subparsers.add_parser('status')
parser_status.add_argument('order_number')
parser_status.add_argument('--test', action='store_true', help='make calls against test instance')
parser_status.add_argument('--username', action='store', dest='username',
                    help='InCommon username')
parser_status.add_argument('--password', action='store', dest='password',
                    help='InCommon password')
parser_status.add_argument('--credentials-file', action='store', dest='credentials_file',
                    help='credentials file')
parser_status.set_defaults(func=status)

# The server-types command subparse
parser_server_types = subparsers.add_parser('server-types')
parser_server_types.set_defaults(func=server_types)

# The cert-types command subparse
parser_cert_types = subparsers.add_parser('cert-types')
parser_cert_types.add_argument('--test', action='store_true', help='make calls against test instance')
parser_cert_types.add_argument('--username', action='store', dest='username',
                    help='InCommon username')
parser_cert_types.add_argument('--password', action='store', dest='password',
                    help='InCommon password')
parser_cert_types.add_argument('--credentials-file', action='store', dest='credentials_file',
                    help='credentials file')
parser_cert_types.set_defaults(func=cert_types)


# The collect command subparse
parser_collect = subparsers.add_parser('collect')
parser_collect.add_argument('order_number')
parser_collect.add_argument('--test', action='store_true', help='make calls against test instance')
parser_collect.add_argument('--username', action='store', dest='username',
                    help='InCommon username')
parser_collect.add_argument('--password', action='store', dest='password',
                    help='InCommon password')
parser_collect.add_argument('--credentials-file', action='store', dest='credentials_file',
                    help='credentials file')
parser_collect.add_argument('--format', action='store', dest='format',
                            default='x509_bundle', help='certificate format')
parser_collect.set_defaults(func=collect)

# The revoke command subparse
parser_revoke = subparsers.add_parser('revoke')
parser_revoke.add_argument('order_number')
parser_revoke.add_argument('reason')
parser_revoke.add_argument('--test', action='store_true', help='make calls against test instance')
parser_revoke.add_argument('--username', action='store', dest='username',
                    help='InCommon username')
parser_revoke.add_argument('--password', action='store', dest='password',
                    help='InCommon password')
parser_revoke.add_argument('--credentials-file', action='store', dest='credentials_file',
                    help='credentials file')
parser_revoke.set_defaults(func=revoke)

# The report command subparse
parser_cert_report = subparsers.add_parser('report')
parser_cert_report.add_argument('--test', action='store_true', help='make calls against test instance')
parser_cert_report.add_argument('--start-date', action='store', dest='start_date',
                    help='certificate request start date')
parser_cert_report.add_argument('--end-date', action='store', dest='end_date',
                    help='certificate request end date')
parser_cert_report.add_argument('--username', action='store', dest='username',
                    help='InCommon username')
parser_cert_report.add_argument('--password', action='store', dest='password',
                    help='InCommon password')
parser_cert_report.add_argument('--credentials-file', action='store', dest='credentials_file',
                    help='credentials file')
parser_cert_report.set_defaults(func=report)


test_mode  = None
try:
  args = parser.parse_args()
except IOError as msg:
  parser.error(str(msg))

args.func(args)

sys.exit()
