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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2cac0b25f72c3190be77975f3a93dec7041cc3a0
4
- data.tar.gz: c02456232fa21321e1b6edda57f4ecd49e2e7973
3
+ metadata.gz: 84cc20b0f946f057dbb97dbc47775fb65e34841a
4
+ data.tar.gz: da5efa37f2ae9edb0c937d8b3badb0eab32ed147
5
5
  SHA512:
6
- metadata.gz: 1dbeb5d5b31f62654d02085d2e62423ea4b0c04c56a10514ffa2e6518ef3cd09f3279231825fe3b2c0a4101efa7fb7ab48a25c3d5bad298b327a0d76cb588186
7
- data.tar.gz: 07fe6c940ecb08a24937c8e34b18c720a16b28fe0151664a6832328b26db4580c47df48cace4a4c662d3f0fea5398a9820592e8013e57c74884aeeca7316f81b
6
+ metadata.gz: ee3808965a511242b527a62118c2d8f06a2b1b5bf6368aeb439973871d1b365dc751b7e27e2073315eda3ca30082728c484d24aa6eb3268d58cce9f511d582b8
7
+ data.tar.gz: 1ffcd1a23790403c2a372b2f36c8e80d7d3f121635093200b68feaad6d88dcbb61b18a524fa9e3712b7a36464597309e275464ea3803983579297e71ffe2e59d
data/.gitignore CHANGED
@@ -7,3 +7,4 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ /log/
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
- herokuapp: foobar
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
@@ -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
@@ -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/setup'
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| Setup.new(config).perform }
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
@@ -1,3 +1,3 @@
1
1
  module LetsencryptHeroku
2
- VERSION = "0.1.3"
2
+ VERSION = "0.2.3"
3
3
  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.1.3
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-07-17 00:00:00.000000000 Z
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/setup.rb
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