letsencrypt_heroku 0.1.3 → 0.2.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|