forked from bsyk/cf-letsencrypt
-
Notifications
You must be signed in to change notification settings - Fork 0
/
setup-app.py
166 lines (142 loc) · 6.29 KB
/
setup-app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
import requests
import yaml
from subprocess import call, check_call, Popen, PIPE
import sys
import time
# Define some helper functions
def domain_has_ssl(domain, full_host, print_info=False):
"""domain_has_ssl uses the two most-reliable ways to check for an SSL on
a domain name within bluemix. It calls the Bluemix CLI to ask for
certificate details, and it attempts to connect to that host with HTTPS.
If either succeeds, it returns True. Otherwise, it returns false. Note
that it is possible to get false negatives, but not false positives.
The print_info parameter can be used to dump the certificate information
from Bluemix to stdout.
"""
pipe = Popen("bx app domain-cert %s" % domain,
stdout=PIPE, shell=True)
output = pipe.stdout.read().decode("unicode_escape")
cert_exists = "OK" in output
if print_info and cert_exists:
print(output)
return cert_exists or check_ssl(full_host)
def get_cert(appname, domain, certname):
"""get_cert wraps the `bx cf ssh` command to retrieve the literal file
contents of the certificate that was requested.
It then writes the certificate to a file in the current working
directory with the same name that the certificate had on the server.
"""
command = "bx --quiet cf ssh %s -c \"cat ~/app/conf/live/%s/%s\"" % (appname, domain, certname)
print("Running: %s" % command)
certfile = open(certname,"w+")
return Popen(command, shell=True, stdout=certfile)
def check_ssl(full_host):
"""check_ssl makes an HTTPS request to a given full host name
and returns a boolean for whether the SSL on the host is present
and valid.
"""
try:
target = "https://%s" % full_host
print("Making GET request to %s" % target)
requests.get(target)
return True
except requests.exceptions.SSLError as err:
print(err)
return False
# Begin Script
with open('domains.json') as data_file:
settings = yaml.safe_load(data_file)
with open('manifest.yml') as manifest_file:
manifest = yaml.safe_load(manifest_file)
appname = manifest['applications'][0]['name']
#consider deleting the app if you've already pushed it with recent success
#otherwise the script can get confused by those success messages in the logs
#call(["bx", "cf", "delete", appname])
# Push the app, but don't start it yet
check_call(["bx", "app", "push", "--no-start"])
# For each domain, map a route for the specific letsencrypt check path
# '/.well-known/acme-challenge/'
for entry in settings['domains']:
domain = entry['domain']
for host in entry['hosts']:
if host == '.':
call(["bx", "cf", "map-route", appname, domain, "--path", ".well-known/acme-challenge/"])
else:
call(["bx", "cf", "map-route", appname, domain, "--hostname", host, "--path", ".well-known/acme-challenge/"])
# Now the app can be started
check_call(["bx", "app", "start", appname])
# Tail the application log
print("Parsing log files.")
end_token = "cf stop %s" % appname # Seeing this in the log means certs done
log_pipe = Popen("bx cf logs %s --recent" % appname, shell=True,
stdout=PIPE, stderr=PIPE)
log_lines = str(log_pipe.stdout.readlines())
print("Waiting for certs...")
seconds_waited = 0
MAX_WAIT_SECONDS = 60
while end_token not in ''.join(log_lines)\
and seconds_waited < MAX_WAIT_SECONDS:
# Keep checking the logs for cert readiness
print("Certs not ready yet, retrying in 5 seconds.")
time.sleep(5)
seconds_waited = seconds_waited + 5
log_pipe = Popen("bx cf logs %s --recent" % appname, shell=True,
stdout=PIPE, stderr=PIPE)
log_lines = str(log_pipe.stdout.readlines())
# If no certs in log after MAX_WAIT_SECONDS, exit and warn user
if seconds_waited >= MAX_WAIT_SECONDS:
print("\n\nIt has been %d minutes without seeing certificates issued"
% (MAX_WAIT_SECONDS/60)
+ " in the log. Something probably went wrong. Please check"
+ " the output of `bx cf logs %s --recent`" % appname
+ " for more information.\n\nExiting.")
sys.exit(1)
# Figure out which domain name to look for
primary_domain = settings['domains'][0]['domain']
domain_with_first_host = "%s.%s" % (settings['domains'][0]['hosts'][0],
primary_domain)
# Hostname is sometimes '.', which requires special handling
if domain_with_first_host.startswith('..'):
domain_with_first_host = domain_with_first_host[2:]
cert1Proc = get_cert(appname, domain_with_first_host, 'cert.pem')
cert2Proc = get_cert(appname, domain_with_first_host, 'chain.pem')
cert3Proc = get_cert(appname, domain_with_first_host, 'fullchain.pem')
cert4Proc = get_cert(appname, domain_with_first_host, 'privkey.pem')
# wait for get_cert subprocesses to finish
cert1Proc.wait()
cert2Proc.wait()
cert3Proc.wait()
cert4Proc.wait()
# Check if there is already an SSL in place
if domain_has_ssl(primary_domain, domain_with_first_host, True):
print("\n***IMPORTANT***")
print("This domain name already has an SSL certificate in bluemix."
+ " You must first remove the old SSL before adding a new one."
+ " This means that your application will have a window of time"
+ " without a certificate.\n")
print("If you wish to continue, run:\n"
+ ("bx app domain-cert-remove %s; " % primary_domain)
+ ("bx app domain-cert-add %s -c cert.pem -k privkey.pem -i chain.pem; "
% primary_domain)
+ ("bx app domain-cert %s\n" % primary_domain))
sys.exit(1)
# Kill the letsencrypt app now that its work is done
call(["bx", "app", "stop", appname])
failure = True
count = 0
while(failure and count < 3):
# Upload new cert
print("Attempting certificate upload...")
call("bx app domain-cert-add %s -c cert.pem -k privkey.pem -i chain.pem"
% primary_domain, shell=True)
failure = not domain_has_ssl(primary_domain, domain_with_first_host, True)
count = count + 1
time.sleep(5)
print("Warning: Please note that your SSL certificate, its corresponding"
+ " PRIVATE KEY, and its intermediate certificates have been downloaded"
+ " to the current working directory. If you need to remove them, use"
+ " `rm *.pem`")
if failure:
print("Unable to upload certificates")
sys.exit(1)
print("Upload Succeeded")