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