1#!/usr/bin/env python3 2 3from pathlib import Path 4from os import path 5import os 6import sys 7from subprocess import PIPE,call, check_call, CalledProcessError,Popen 8import argparse 9import shutil 10import re 11 12ASADMIN_PATH="./asadmin" 13WEB_ROOT_PATH=Path("/tmp/payara_le_war") 14LE_LIVE_PATH=Path("/etc/letsencrypt/live/") 15APP_NAME="le" 16FNULL = open(os.devnull, 'w') 17OKGREEN = '\033[92m' 18WARNING = '\033[93m' 19FAIL = '\033[91m' 20ENDC = '\033[0m' 21 22def kill_process(): 23 pass 24 25def make_output_dir(dir_path) -> Path: 26 dir_path.mkdir(exist_ok=True, parents=True) 27 28def create_le_war(): 29 make_output_dir(WEB_ROOT_PATH / "WEB-INF") 30 31def restart_listener(listener_name): 32 check_call([ASADMIN_PATH, "set", "server.network-config.network-listeners.network-listener.%s.enabled=false" % listener_name]) 33 check_call([ASADMIN_PATH, "set", "server.network-config.network-listeners.network-listener.%s.enabled=true" % listener_name]) 34 35def upload_keypair(key_path, cert_path, alias, gf_domain_name, gf_domain_dir=None): 36 print("Uploading keypair using asadmin: ", end='') 37 try: 38 if gf_domain_dir is None: 39 check_call([ASADMIN_PATH, "add-pkcs8", "--domain_name", gf_domain_name, "--destalias", alias, "--priv-key-path", key_path, "--cert-chain-path", cert_path], stdout=FNULL, stderr=FNULL) 40 else: 41 check_call([ASADMIN_PATH, "add-pkcs8", "--domain_name", gf_domain_name, "--domaindir", gf_domain_dir, "--destalias", alias, "--priv-key-path", key_path, "--cert-chain-path", cert_path], stdout=FNULL, stderr=FNULL) 42 except CalledProcessError: 43 print('[' + FAIL + "FAIL" + ENDC + ']' + "\n", sys.exc_info()[1]) 44 return 1 45 46 print('[' + OKGREEN + " OK " + ENDC + ']') 47 return 0 48 49def configure_listener_alias(listener_name, alias): 50 check_call([ASADMIN_PATH, "set", "configs.config.server-config.network-config.protocols.protocol.%s.ssl.cert-nickname=%s" % (listener_name, alias)]) 51 52def deploy_war(): 53 print("Attempting to deploy an EMPTY WAR to the ROOT context: ", end='') 54 try: 55 check_call([ASADMIN_PATH, "deploy", "--name", APP_NAME, "--force", "--contextroot", "/", WEB_ROOT_PATH], stdout=FNULL, stderr=FNULL) 56 print('[' + OKGREEN + " OK " + ENDC + ']') 57 except CalledProcessError: 58 print('[' + FAIL + "FAIL" + ENDC + ']' + " Is the server up and running?\n", sys.exc_info()[1]) 59 shutil.rmtree(WEB_ROOT_PATH) 60 return 1 61 62 return 0 63 64def undeploy_war(): 65 print("Undeploying WAR, doing cleanup: ", end='') 66 try: 67 check_call([ASADMIN_PATH, "undeploy", APP_NAME], stdout=FNULL, stderr=FNULL) 68 print('[' + OKGREEN + " OK " + ENDC + ']') 69 shutil.rmtree(WEB_ROOT_PATH) 70 except CalledProcessError: 71 print('[' + FAIL + "FAIL" + ENDC + ']' + " Is the server up and running?\n", sys.exc_info()[1]) 72 shutil.rmtree(WEB_ROOT_PATH) 73 return 1 74 75 return 0 76 77def invoke_certbot(domain_names): 78 certbot_call_args = ["certbot", "certonly", "--webroot", "-w", WEB_ROOT_PATH] 79 for d in domain_names: 80 certbot_call_args += ["-d", d] 81 82 try: 83 check_call(certbot_call_args) 84 print('Calling certbot: [' + OKGREEN + " OK " + ENDC + ']') 85 except CalledProcessError: 86 print(sys.exc_info()[1], '\nCalling certbot: [' + FAIL + "FAIL" + ENDC + ']') 87 return 1 88 89 return 0 90 91def check_http_port(): 92 proc = Popen([ASADMIN_PATH, "get", "server.network-config.network-listeners.network-listener.*.port"], stdout=PIPE) 93 ports = proc.stdout.read() 94 if not '.port=80\n'.encode() in ports: 95 print(WARNING + "WARNING: " + ENDC + "None of the listeners of Payara are running on the standard HTTP port (80).\nUnless there is a port mapping, " 96 "a reverse-proxy exposing port 80 or other solution making the deployed web-app visible through port 80, the following invocation of certbot will " 97 "likely fail (due to a failing 'HTTP Challenge' of the ACME protocol; see Chapter 8.3 on https://ietf-wg-acme.github.io/acme/draft-ietf-acme-acme.html). " 98 "Proceeding nevertheless.") 99 100 101if __name__=="__main__": 102 parser = argparse.ArgumentParser(description="Payara Let's Encrypt integration script. This script deploys an empty WAR, calls (and requires) certbot " 103 "to retrieve your certificate and uploads the certificate to the default keystore of the Payara domain. In addition, this script configures the listener " 104 "with the given alias and restarts it (the listener only, not the whole domain), so that the new certificate is effective. Afterwards the WAR is undeployed. " 105 "CertBot requires root, and as a consequence, so does this script. NOTE: In order for the web-challenge to be successful, the deployed application must be " 106 "visible through the standard HTTP port (80) of the provided certification domain name (see parameter --cert-domain below).") 107 parser.add_argument('-c','--cert-domain', action="append", help="The FQDN of the domain(s) the certificate will be bound too. You may use this arg multiple times.", required=True) 108 parser.add_argument('-n','--name', help="The name of the payara-domain where the certificate will be uploaded.", required=False, default='production') 109 parser.add_argument('-d','--domain-dir', '--domaindir', help="The directory where payara domains are defined. Necessary to provide only when the domains are in non-standard locations.", required=False) 110 parser.add_argument('-l','--listener', help="HTTP Listener's name. By default http-listener-2", required=False, default="http-listener-2") 111 parser.add_argument('-a','--alias', help="The alias that is used to import the keypar and the listener will be configured to use this alias. " 112 "The default is constructed like: 'le_{listener}'", required=False) 113 114 args = parser.parse_args() 115 alias = "le_" + args.cert_domain[0] if args.alias is None else args.alias 116 create_le_war() 117 if deploy_war() != 0: 118 exit(1) 119 120 check_http_port() 121 code = invoke_certbot(args.cert_domain) 122 undeploy_war() 123 124 if code != 0: 125 exit(code) 126 127 key_path = LE_LIVE_PATH / args.cert_domain[0] / "privkey.pem" 128 cert_path = LE_LIVE_PATH / args.cert_domain[0] / "fullchain.pem" 129 code = upload_keypair(key_path, cert_path, alias, args.name, args.domain_dir) 130 if code == 0: 131 code = configure_listener_alias(alias) 132 if code == 0: 133 restart_listener(args.listener) 134 135 exit(code) 136