-
Notifications
You must be signed in to change notification settings - Fork 33
/
letsencrypt.rake
208 lines (169 loc) · 7.53 KB
/
letsencrypt.rake
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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
require 'open-uri'
require 'openssl'
require 'acme-client'
require 'platform-api'
namespace :letsencrypt do
desc 'Renew your LetsEncrypt certificate'
task :renew do
# Check configuration looks OK
abort "letsencrypt-rails-heroku is configured incorrectly. Are you missing an environment variable or other configuration? You should have heroku_token, heroku_app, acme_email and acme_terms_agreed configured either via a `Letsencrypt.configure` block in an initializer or as environment variables." unless Letsencrypt.configuration.valid?
# Set up Heroku client
heroku = PlatformAPI.connect_oauth Letsencrypt.configuration.heroku_token
heroku_app = Letsencrypt.configuration.heroku_app
if Letsencrypt.registered?
puts "Using existing registration details"
private_key = OpenSSL::PKey::RSA.new(Letsencrypt.configuration.acme_private_key)
key_id = Letsencrypt.configuration.acme_key_id
else
# Create a private key
print "Creating account key..."
private_key = OpenSSL::PKey::RSA.new(4096)
puts "Done!"
client = Acme::Client.new(private_key: private_key,
directory: Letsencrypt.configuration.acme_directory,
connection_options: {
request: {
open_timeout: 5,
timeout: 5
}
})
print "Registering with LetsEncrypt..."
account = client.new_account(contact: "mailto:#{Letsencrypt.configuration.acme_email}",
terms_of_service_agreed: true)
key_id = account.kid
puts "Done!"
print "Saving account details as configuration variables..."
heroku.config_var.update(heroku_app,
'ACME_PRIVATE_KEY' => private_key.to_pem,
'ACME_KEY_ID' => account.kid)
puts "Done!"
end
# Make a new Acme::Client with whichever private_key & key_id we ended up with.
client = Acme::Client.new(private_key: private_key,
directory: Letsencrypt.configuration.acme_directory,
kid: key_id)
domains = []
if Letsencrypt.configuration.acme_domain
puts "Using ACME_DOMAIN configuration variable..."
domains = Letsencrypt.configuration.acme_domain.split(',').map(&:strip)
else
domains = heroku.domain.list(heroku_app).map{|domain| domain['hostname']}
puts "Using #{domains.length} configured Heroku domain(s) for this app..."
end
order = client.new_order(identifiers: domains)
order.authorizations.each do |authorization|
puts "Performing verification for #{authorization.domain}:"
challenge = authorization.http
raise Letsencrypt::Error::NoHTTPChallengeError, "No HTTP challenge was given by Let's Encrypt for #{authorization.domain}, and letsencrypt-rails-heroku does not currently support other challenge types." unless challenge
print "Setting config vars on Heroku..."
heroku.config_var.update(heroku_app, {
'ACME_CHALLENGE_FILENAME' => challenge.filename,
'ACME_CHALLENGE_FILE_CONTENT' => challenge.file_content
})
puts "Done!"
# Wait for app to come up
print "Testing filename works (to bring up app)..."
# Get the domain name from Heroku
hostname = heroku.domain.list(heroku_app).first['hostname']
# Wait at least a little bit, otherwise the first request will almost always fail.
sleep(2)
start_time = Time.now
begin
open("http://#{hostname}/#{challenge.filename}").read
rescue OpenSSL::SSL::SSLError, OpenURI::HTTPError, RuntimeError => e
raise e if e.is_a?(RuntimeError) && !e.message.include?("redirection forbidden")
if Time.now - start_time <= 60
puts "Error fetching challenge, retrying... #{e.message}"
sleep(5)
retry
else
failure_message = "Error waiting for response from http://#{hostname}/#{challenge.filename}, Error: #{e.message}"
raise Letsencrypt::Error::ChallengeUrlError, failure_message
end
end
puts "Done!"
print "Giving LetsEncrypt some time to verify..."
# Once you are ready to serve the confirmation request you can proceed.
challenge.request_validation
start_time = Time.now
while challenge.status == 'pending'
if Time.now - start_time >= 30
failure_message = "Failed - timed out waiting for challenge verification."
raise Letsencrypt::Error::VerificationTimeoutError, failure_message
end
sleep(2)
challenge.reload
end
puts "Done!"
unless challenge.status == 'valid'
puts "Problem verifying challenge."
failure_message = "Status: #{challenge.status}, Error: #{challenge.error}"
raise Letsencrypt::Error::VerificationError, failure_message
end
puts ""
end
# Unset temporary config vars. We don't care about waiting for this to
# restart
heroku.config_var.update(heroku_app, {
'ACME_CHALLENGE_FILENAME' => nil,
'ACME_CHALLENGE_FILE_CONTENT' => nil
})
# Create CSR
csr_private_key = OpenSSL::PKey::RSA.new 4096
csr = Acme::Client::CertificateRequest.new(names: domains,
private_key: csr_private_key)
print "Asking LetsEncrypt to finalize our certificate order..."
# Get certificate
order.finalize(csr: csr)
# Wait for order to process
start_time = Time.now
while order.status == 'processing'
if Time.now - start_time >= 30
failure_message = "Failed - timed out waiting for order finalization"
raise Letsencrypt::Error::FinalizationTimeoutError, failure_message
end
sleep(2)
order.reload
end
puts "Done!"
unless order.status == 'valid'
failure_message = "Problem finalizing order - status: #{order.status}"
raise Letsencrypt::Error::FinalizationError, failure_message
end
certificate = order.certificate # => PEM-formatted certificate
# Send certificates to Heroku via API
endpoint = case Letsencrypt.configuration.ssl_type
when 'sni'
heroku.sni_endpoint
when 'endpoint'
heroku.ssl_endpoint
end
certificate_info = {
certificate_chain: certificate,
private_key: csr_private_key.to_pem
}
# Fetch existing certificate from Heroku (if any). We just use the first
# one; if someone has more than one, they're probably not actually using
# this gem. Could also be an error?
existing_certificate = endpoint.list(heroku_app)[0]
begin
if existing_certificate
print "Updating existing certificate #{existing_certificate['name']}..."
endpoint.update(heroku_app, existing_certificate['name'], certificate_info)
puts "Done!"
else
print "Adding new certificate..."
endpoint.create(heroku_app, certificate_info)
puts "Done!"
end
rescue Excon::Error::UnprocessableEntity => e
warn "Error adding certificate to Heroku. Response from Heroku’s API follows:"
raise Letsencrypt::Error::HerokuCertificateError, e.response.body
rescue Excon::Error::Forbidden => e
warn "Error adding certificate to Heroku, expected an OK response status, got a '403 Forbidden'. Response follows:"
puts e.response.body
# Re-raise for now.
raise e
end
end
end