letsencrypt_heroku 0.1.3 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/README.md +7 -1
- data/bin/letsencrypt_heroku +34 -0
- data/lib/letsencrypt_heroku.rb +18 -2
- data/lib/letsencrypt_heroku/config_builder.rb +14 -0
- data/lib/letsencrypt_heroku/process.rb +15 -0
- data/lib/letsencrypt_heroku/process/authorize_domains.rb +38 -0
- data/lib/letsencrypt_heroku/process/check_preconditions.rb +15 -0
- data/lib/letsencrypt_heroku/process/prepare_config.rb +25 -0
- data/lib/letsencrypt_heroku/process/setup_client.rb +24 -0
- data/lib/letsencrypt_heroku/process/update_certificates.rb +30 -0
- data/lib/letsencrypt_heroku/tools.rb +54 -0
- data/lib/letsencrypt_heroku/version.rb +1 -1
- metadata +10 -3
- data/lib/letsencrypt_heroku/setup.rb +0 -102
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 84cc20b0f946f057dbb97dbc47775fb65e34841a
|
4
|
+
data.tar.gz: da5efa37f2ae9edb0c937d8b3badb0eab32ed147
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ee3808965a511242b527a62118c2d8f06a2b1b5bf6368aeb439973871d1b365dc751b7e27e2073315eda3ca30082728c484d24aa6eb3268d58cce9f511d582b8
|
7
|
+
data.tar.gz: 1ffcd1a23790403c2a372b2f36c8e80d7d3f121635093200b68feaad6d88dcbb61b18a524fa9e3712b7a36464597309e275464ea3803983579297e71ffe2e59d
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -19,13 +19,19 @@ You'll need a `config/letsencrypt_heroku.yml`
|
|
19
19
|
|
20
20
|
- contact: contact@foobar.dev
|
21
21
|
domains: foobar.dev www.foobar.dev
|
22
|
-
|
22
|
+
heroku_app: foobar
|
23
23
|
|
24
24
|
And finally execute
|
25
25
|
|
26
26
|
$ letsencrypt_heroku
|
27
27
|
|
28
|
+
Please note that your application needs to be restarted for every individual domain in your config. The restart happens
|
29
|
+
automatically when the heroku challenge response gets set as environment variable.
|
30
|
+
|
28
31
|
## Contributing
|
29
32
|
|
30
33
|
Bug reports and pull requests are welcome on GitHub at https://github.com/xijo/letsencrypt_heroku.
|
31
34
|
|
35
|
+
## TODO
|
36
|
+
|
37
|
+
- configurable config file location
|
data/bin/letsencrypt_heroku
CHANGED
@@ -2,5 +2,39 @@
|
|
2
2
|
|
3
3
|
require 'bundler/setup'
|
4
4
|
require 'letsencrypt_heroku'
|
5
|
+
require 'optparse'
|
6
|
+
|
7
|
+
class Parser
|
8
|
+
def self.parse(options)
|
9
|
+
args = {}
|
10
|
+
|
11
|
+
opt_parser = OptionParser.new do |opts|
|
12
|
+
opts.banner = "Usage: letsencrypt_heroku [options]"
|
13
|
+
|
14
|
+
# opts.on("-rNAME", "--restriction=NAME", "Restrict process to config entries where at least one domain matches the given value") do |restriction|
|
15
|
+
# args[:restriction] = restriction
|
16
|
+
# end
|
17
|
+
|
18
|
+
# opts.on("-s", "--setup", "Generate config and put it into config/letsencrypt_heroku.yml") do |n|
|
19
|
+
# LetsencryptHeroku::ConfigBuilder.new.perform
|
20
|
+
# exit
|
21
|
+
# end
|
22
|
+
|
23
|
+
# opts.on("-c", "--check", "Parse config and print problems") do |n|
|
24
|
+
# puts "perform check"
|
25
|
+
# exit
|
26
|
+
# end
|
27
|
+
|
28
|
+
opts.on("-h", "--help", "Prints this help") do
|
29
|
+
puts opts
|
30
|
+
exit
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
opt_parser.parse!(options)
|
35
|
+
return args
|
36
|
+
end
|
37
|
+
end
|
38
|
+
options = Parser.parse ARGV
|
5
39
|
|
6
40
|
LetsencryptHeroku::CLI.run
|
data/lib/letsencrypt_heroku.rb
CHANGED
@@ -3,20 +3,36 @@ require 'tty-spinner'
|
|
3
3
|
require 'yaml'
|
4
4
|
require 'acme-client'
|
5
5
|
require 'openssl'
|
6
|
+
require 'logger'
|
7
|
+
require 'ostruct'
|
6
8
|
require 'letsencrypt_heroku/version'
|
7
|
-
require 'letsencrypt_heroku/
|
9
|
+
require 'letsencrypt_heroku/tools'
|
10
|
+
require 'letsencrypt_heroku/process'
|
11
|
+
require 'letsencrypt_heroku/config_builder'
|
12
|
+
require 'letsencrypt_heroku/process/prepare_config'
|
13
|
+
require 'letsencrypt_heroku/process/check_preconditions'
|
14
|
+
require 'letsencrypt_heroku/process/setup_client'
|
15
|
+
require 'letsencrypt_heroku/process/authorize_domains'
|
16
|
+
require 'letsencrypt_heroku/process/update_certificates'
|
8
17
|
|
9
18
|
module LetsencryptHeroku
|
10
19
|
class CLI
|
11
20
|
CONFIG_FILE = 'config/letsencrypt_heroku.yml'
|
12
21
|
|
22
|
+
# generate config?
|
23
|
+
|
24
|
+
# limit to domain, skip other configs
|
25
|
+
|
13
26
|
def self.run
|
14
27
|
if File.exist?(CONFIG_FILE)
|
15
28
|
configs = Array(YAML.load(File.read(CONFIG_FILE))).map { |c| OpenStruct.new(c) }
|
16
|
-
configs.each { |config|
|
29
|
+
configs.each { |config| Process.new(config).perform }
|
17
30
|
else
|
18
31
|
puts Rainbow("Missing config: #{CONFIG_FILE}").red
|
19
32
|
end
|
20
33
|
end
|
21
34
|
end
|
35
|
+
|
36
|
+
class TaskError < StandardError
|
37
|
+
end
|
22
38
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class LetsencryptHeroku::ConfigBuilder
|
2
|
+
include LetsencryptHeroku::Tools
|
3
|
+
|
4
|
+
# Create a letsencrypt_heroku config file under the given path.
|
5
|
+
#
|
6
|
+
def perform(path = 'config/letsencrypt_heroku.yml')
|
7
|
+
banner 'Generate:', "#{Dir.pwd}/#{path}"
|
8
|
+
|
9
|
+
output("write file") do
|
10
|
+
error('already exists') if File.exists?(path)
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module LetsencryptHeroku
|
2
|
+
class Process
|
3
|
+
attr_accessor :context
|
4
|
+
|
5
|
+
def initialize(config)
|
6
|
+
@context = OpenStruct.new(config: config)
|
7
|
+
end
|
8
|
+
|
9
|
+
def perform
|
10
|
+
[PrepareConfig, CheckPreconditions, SetupClient, AuthorizeDomains, UpdateCertificates].each do |klass|
|
11
|
+
klass.new.perform(context)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
class LetsencryptHeroku::Process
|
2
|
+
class AuthorizeDomains
|
3
|
+
include LetsencryptHeroku::Tools
|
4
|
+
|
5
|
+
def perform(context)
|
6
|
+
context.config.domains.each do |domain|
|
7
|
+
output "Authorize #{domain}" do
|
8
|
+
authorize(domain: domain, context: context)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def authorize(domain:, context:)
|
14
|
+
challenge = context.client.authorize(domain: domain).http01
|
15
|
+
|
16
|
+
execute "heroku config:set LETSENCRYPT_RESPONSE=#{challenge.file_content} --app #{context.config.heroku_challenge_app}"
|
17
|
+
|
18
|
+
test_response(domain: domain, challenge: challenge)
|
19
|
+
|
20
|
+
challenge.request_verification
|
21
|
+
sleep(1) while 'pending' == challenge.verify_status
|
22
|
+
challenge.verify_status == 'valid' or error('failed authorization')
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_response(domain:, challenge:)
|
26
|
+
url = "http://#{domain}/#{challenge.filename}"
|
27
|
+
fail_count = 0
|
28
|
+
answer = nil
|
29
|
+
|
30
|
+
while answer != challenge.file_content
|
31
|
+
error('failed test response') if fail_count > 30
|
32
|
+
fail_count += 1
|
33
|
+
sleep(1)
|
34
|
+
answer = `curl -sL #{url}`
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class LetsencryptHeroku::Process
|
2
|
+
class CheckPreconditions
|
3
|
+
include LetsencryptHeroku::Tools
|
4
|
+
|
5
|
+
def perform(context)
|
6
|
+
banner 'Running for:', context.config.domains.join(', ')
|
7
|
+
output 'Prepare SSL endpoint' do
|
8
|
+
execute "heroku labs:enable http-sni --app #{context.config.heroku_certificate_app}"
|
9
|
+
execute 'heroku plugins:install heroku-certs'
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class LetsencryptHeroku::Process
|
2
|
+
class PrepareConfig
|
3
|
+
include LetsencryptHeroku::Tools
|
4
|
+
|
5
|
+
def perform(context)
|
6
|
+
config = context.config
|
7
|
+
config.domains = config.domains.to_s.split
|
8
|
+
config.heroku_certificate_app ||= config.heroku_app
|
9
|
+
config.heroku_challenge_app ||= config.heroku_app
|
10
|
+
|
11
|
+
validate_config(config)
|
12
|
+
context
|
13
|
+
end
|
14
|
+
|
15
|
+
def validate_config(config)
|
16
|
+
if config.domains.empty?
|
17
|
+
error('Please provide `domains`')
|
18
|
+
end
|
19
|
+
|
20
|
+
unless config.heroku_certificate_app && config.heroku_challenge_app
|
21
|
+
error('Please provide `heroku_app`')
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class LetsencryptHeroku::Process
|
2
|
+
class SetupClient
|
3
|
+
include LetsencryptHeroku::Tools
|
4
|
+
|
5
|
+
PRODUCTION = 'https://acme-v01.api.letsencrypt.org/'
|
6
|
+
STAGING = 'https://acme-staging.api.letsencrypt.org/'
|
7
|
+
|
8
|
+
def perform(context)
|
9
|
+
output 'Contact letsencrypt server' do
|
10
|
+
context.client = build_client
|
11
|
+
context.client.register(contact: "mailto:#{context.config.contact}").agree_terms or error('registration failed')
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def build_client
|
16
|
+
Acme::Client.new(private_key: private_key, endpoint: PRODUCTION)
|
17
|
+
end
|
18
|
+
|
19
|
+
def private_key
|
20
|
+
OpenSSL::PKey::RSA.new(4096)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class LetsencryptHeroku::Process
|
2
|
+
class UpdateCertificates
|
3
|
+
include LetsencryptHeroku::Tools
|
4
|
+
|
5
|
+
def perform(context)
|
6
|
+
herokuapp = context.config.heroku_certificate_app
|
7
|
+
|
8
|
+
output 'Update certificates' do
|
9
|
+
csr = Acme::Client::CertificateRequest.new(names: context.config.domains)
|
10
|
+
certificate = context.client.new_certificate(csr)
|
11
|
+
File.write('privkey.pem', certificate.request.private_key.to_pem)
|
12
|
+
File.write('fullchain.pem', certificate.fullchain_to_pem)
|
13
|
+
|
14
|
+
if has_already_cert(herokuapp)
|
15
|
+
execute "heroku _certs:update fullchain.pem privkey.pem --confirm #{herokuapp} --app #{herokuapp}"
|
16
|
+
else
|
17
|
+
execute "heroku _certs:add fullchain.pem privkey.pem --app #{herokuapp}"
|
18
|
+
end
|
19
|
+
FileUtils.rm %w(privkey.pem fullchain.pem)
|
20
|
+
end
|
21
|
+
puts # finish the output with a nice newline ;)
|
22
|
+
end
|
23
|
+
|
24
|
+
def has_already_cert(herokuapp)
|
25
|
+
Open3.popen3("heroku _certs:info --app #{herokuapp}") do |stdin, stdout, stderr, wait_thr|
|
26
|
+
return wait_thr.value.success?
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module LetsencryptHeroku
|
2
|
+
module Tools
|
3
|
+
def banner(msg, values = nil)
|
4
|
+
puts "\n #{Rainbow(msg).blue} #{values.to_s}\n\n"
|
5
|
+
end
|
6
|
+
|
7
|
+
def output(name)
|
8
|
+
log name
|
9
|
+
@_spinner = build_spinner(name)
|
10
|
+
@_spinner.start
|
11
|
+
yield
|
12
|
+
@_spinner.success
|
13
|
+
rescue LetsencryptHeroku::TaskError
|
14
|
+
exit
|
15
|
+
end
|
16
|
+
|
17
|
+
def log(message, level: :info)
|
18
|
+
message.to_s.empty? and return
|
19
|
+
level == :info ? logger.info(message) : logger.error(message)
|
20
|
+
end
|
21
|
+
|
22
|
+
def error(reason = nil)
|
23
|
+
log reason, level: :error
|
24
|
+
@_spinner && @_spinner.error("(#{reason.strip})")
|
25
|
+
raise LetsencryptHeroku::TaskError, reason
|
26
|
+
end
|
27
|
+
|
28
|
+
def execute(command)
|
29
|
+
log command
|
30
|
+
Open3.popen3(command) do |stdin, stdout, stderr, wait_thr|
|
31
|
+
out, err = stdout.read, stderr.read
|
32
|
+
log out
|
33
|
+
log err
|
34
|
+
wait_thr.value.success? or error(err.force_encoding('utf-8').sub(' ▸ ', 'heroku: '))
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def logger
|
41
|
+
@logger ||= Logger.new(File.open('log/letsencrypt_heroku.log', File::WRONLY | File::APPEND | File::CREAT))
|
42
|
+
end
|
43
|
+
|
44
|
+
def build_spinner(name)
|
45
|
+
TTY::Spinner.new(" :spinner #{name}",
|
46
|
+
format: :dots,
|
47
|
+
interval: 20,
|
48
|
+
frames: [ "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" ].map { |s| Rainbow(s).yellow.bright },
|
49
|
+
success_mark: Rainbow('✔').green,
|
50
|
+
error_mark: Rainbow('✘').red
|
51
|
+
)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: letsencrypt_heroku
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Johannes Opper
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-08-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rainbow
|
@@ -128,7 +128,14 @@ files:
|
|
128
128
|
- bin/setup
|
129
129
|
- letsencrypt_heroku.gemspec
|
130
130
|
- lib/letsencrypt_heroku.rb
|
131
|
-
- lib/letsencrypt_heroku/
|
131
|
+
- lib/letsencrypt_heroku/config_builder.rb
|
132
|
+
- lib/letsencrypt_heroku/process.rb
|
133
|
+
- lib/letsencrypt_heroku/process/authorize_domains.rb
|
134
|
+
- lib/letsencrypt_heroku/process/check_preconditions.rb
|
135
|
+
- lib/letsencrypt_heroku/process/prepare_config.rb
|
136
|
+
- lib/letsencrypt_heroku/process/setup_client.rb
|
137
|
+
- lib/letsencrypt_heroku/process/update_certificates.rb
|
138
|
+
- lib/letsencrypt_heroku/tools.rb
|
132
139
|
- lib/letsencrypt_heroku/version.rb
|
133
140
|
homepage: https://github.com/xijo/letsencrypt_heroku
|
134
141
|
licenses: []
|
@@ -1,102 +0,0 @@
|
|
1
|
-
require 'open3'
|
2
|
-
|
3
|
-
module LetsencryptHeroku
|
4
|
-
class Setup
|
5
|
-
class SetupError < StandardError ; end
|
6
|
-
|
7
|
-
PRODUCTION = 'https://acme-v01.api.letsencrypt.org/'
|
8
|
-
STAGING = 'https://acme-staging.api.letsencrypt.org/'
|
9
|
-
|
10
|
-
attr_accessor :config
|
11
|
-
|
12
|
-
def initialize(config)
|
13
|
-
@config = config
|
14
|
-
@config.endpoint ||= PRODUCTION
|
15
|
-
@config.domains = config.domains.split
|
16
|
-
end
|
17
|
-
|
18
|
-
def perform
|
19
|
-
run_task "preflight" do
|
20
|
-
# heroku labs:enable http-sni
|
21
|
-
# heroku plugins:install heroku-certs
|
22
|
-
|
23
|
-
# check that ssl endpoint is on
|
24
|
-
# check heroku is there
|
25
|
-
# check that certs are there
|
26
|
-
end
|
27
|
-
|
28
|
-
run_task 'register with letsencrypt server' do
|
29
|
-
@private_key = OpenSSL::PKey::RSA.new(4096)
|
30
|
-
@client = Acme::Client.new(private_key: @private_key, endpoint: config.endpoint)
|
31
|
-
@client.register(contact: "mailto:#{config.contact}").agree_terms or fail_task('failed resiger')
|
32
|
-
end
|
33
|
-
|
34
|
-
config.domains.each do |domain|
|
35
|
-
run_task "authorize #{domain}" do
|
36
|
-
@challenge = @client.authorize(domain: domain).http01
|
37
|
-
|
38
|
-
execute "heroku config:set LETSENCRYPT_RESPONSE=#{@challenge.file_content}"
|
39
|
-
|
40
|
-
test_response(domain: domain, challenge: @challenge)
|
41
|
-
|
42
|
-
@challenge.request_verification
|
43
|
-
sleep(1) while 'pending' == @challenge.verify_status
|
44
|
-
@challenge.verify_status == 'valid' or fail_task("failed authorization")
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
# if has cert: update cert, else add cert
|
49
|
-
|
50
|
-
run_task "update certificates" do
|
51
|
-
csr = Acme::Client::CertificateRequest.new(names: config.domains)
|
52
|
-
certificate = @client.new_certificate(csr)
|
53
|
-
File.write('privkey.pem', certificate.request.private_key.to_pem)
|
54
|
-
File.write('fullchain.pem', certificate.fullchain_to_pem)
|
55
|
-
|
56
|
-
execute "heroku _certs:update fullchain.pem privkey.pem --confirm #{config.herokuapp}"
|
57
|
-
FileUtils.rm %w(privkey.pem fullchain.pem)
|
58
|
-
end
|
59
|
-
rescue SetupError => e
|
60
|
-
exit
|
61
|
-
end
|
62
|
-
|
63
|
-
def test_response(domain:, challenge:)
|
64
|
-
url = "http://#{domain}/#{challenge.filename}"
|
65
|
-
fail_count = 0
|
66
|
-
while fail_count < 30
|
67
|
-
answer = `curl -sL #{url}`
|
68
|
-
if answer != challenge.file_content
|
69
|
-
fail_count += 1
|
70
|
-
sleep(1)
|
71
|
-
else
|
72
|
-
return
|
73
|
-
end
|
74
|
-
end
|
75
|
-
fail_task('failed test response')
|
76
|
-
end
|
77
|
-
|
78
|
-
def execute(command)
|
79
|
-
Open3.popen3(command) do |stdin, stdout, stderr, wait_thr|
|
80
|
-
wait_thr.value.success? or fail_task(stderr.read.force_encoding('utf-8').sub(' ▸ ', 'heroku: '))
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
|
-
def run_task(name)
|
85
|
-
@_spinner = TTY::Spinner.new(" :spinner #{name}",
|
86
|
-
format: :dots,
|
87
|
-
interval: 20,
|
88
|
-
frames: [ "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" ].map { |s| Rainbow(s).yellow.bright },
|
89
|
-
success_mark: Rainbow('✔').green,
|
90
|
-
error_mark: Rainbow('✘').red
|
91
|
-
)
|
92
|
-
@_spinner.start
|
93
|
-
yield
|
94
|
-
@_spinner.success
|
95
|
-
end
|
96
|
-
|
97
|
-
def fail_task(reason = nil)
|
98
|
-
@_spinner.error("(#{reason.strip})")
|
99
|
-
raise SetupError, reason
|
100
|
-
end
|
101
|
-
end
|
102
|
-
end
|