acme_plugin 0.0.13
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 +7 -0
- data/MIT-LICENSE +20 -0
- data/Rakefile +33 -0
- data/app/controllers/acme_plugin/application_controller.rb +34 -0
- data/app/models/acme_plugin/challenge.rb +17 -0
- data/app/models/acme_plugin/setting.rb +10 -0
- data/config/routes.rb +3 -0
- data/db/migrate/20151206135029_create_acme_plugin_challenges.rb +9 -0
- data/db/migrate/20160412195212_create_acme_plugin_settings.rb +9 -0
- data/lib/acme_plugin.rb +146 -0
- data/lib/acme_plugin/certificate_output.rb +17 -0
- data/lib/acme_plugin/challenge_store.rb +18 -0
- data/lib/acme_plugin/configuration.rb +27 -0
- data/lib/acme_plugin/database_store.rb +11 -0
- data/lib/acme_plugin/engine.rb +5 -0
- data/lib/acme_plugin/file_output.rb +18 -0
- data/lib/acme_plugin/file_store.rb +16 -0
- data/lib/acme_plugin/heroku_output.rb +19 -0
- data/lib/acme_plugin/private_key_store.rb +24 -0
- data/lib/acme_plugin/version.rb +5 -0
- data/lib/tasks/acme_plugin_tasks.rake +15 -0
- data/test/acme_plugin_test.rb +351 -0
- data/test/controllers/application_controller_test.rb +26 -0
- data/test/dummy/README.rdoc +28 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/javascripts/application.js +13 -0
- data/test/dummy/app/assets/stylesheets/application.css +15 -0
- data/test/dummy/app/controllers/application_controller.rb +5 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/bin/setup +29 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/acme_plugin.yml +19 -0
- data/test/dummy/config/application.rb +23 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +41 -0
- data/test/dummy/config/environments/production.rb +79 -0
- data/test/dummy/config/environments/test.rb +45 -0
- data/test/dummy/config/initializers/assets.rb +11 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +4 -0
- data/test/dummy/config/initializers/session_store.rb +3 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +23 -0
- data/test/dummy/config/routes.rb +3 -0
- data/test/dummy/config/secrets.yml +22 -0
- data/test/dummy/db/schema.rb +27 -0
- data/test/dummy/db/test.sqlite3 +0 -0
- data/test/dummy/key/test_keyfile_1024.pem +15 -0
- data/test/dummy/key/test_keyfile_2048.pem +27 -0
- data/test/dummy/key/test_keyfile_4096.pem +51 -0
- data/test/dummy/key/test_keyfile_8192.pem +99 -0
- data/test/dummy/log/development.log +0 -0
- data/test/dummy/log/test.log +25582 -0
- data/test/dummy/public/404.html +67 -0
- data/test/dummy/public/422.html +67 -0
- data/test/dummy/public/500.html +66 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/fixtures/acme_plugin/challenges.yml +7 -0
- data/test/fixtures/acme_plugin/settings.yml +56 -0
- data/test/lib/acme_plugin/configuration_test.rb +13 -0
- data/test/lib/acme_plugin/private_key_store_test.rb +30 -0
- data/test/models/acme_plugin/challenge_test.rb +6 -0
- data/test/test_helper.rb +23 -0
- metadata +309 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 6666fcd67062f236c49381267588d414edddd669c4f8520abdff1b918df2b48c
|
4
|
+
data.tar.gz: aa9045732686e8149e4fde70f98303f5ba93250af488904b9ab3ca8e007a7314
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2ecad8e88f8138d7ef00129396d8ea4908fe89dbb1831378124c856309b4d7fba8016a313acbc92663fd928aa40d749c065a60e8dc87edfd276e6bbc90ca9610
|
7
|
+
data.tar.gz: db3cfe8b2ce24e2f56bfa6f11d91acd42cf9017c6d5d13423a28ddc4d4cd58fd12c92b4f50c1cb7fc34b20455476c749ba7887cdc6e2d2cf2384779ef2571101
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2015-2018 Lukasz Gromanowski <lgromanowski@gmail.com>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'AcmePlugin'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.rdoc')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
APP_RAKEFILE = File.expand_path('../test/dummy/Rakefile', __FILE__)
|
18
|
+
load 'rails/tasks/engine.rake'
|
19
|
+
|
20
|
+
load 'rails/tasks/statistics.rake'
|
21
|
+
|
22
|
+
Bundler::GemHelper.install_tasks
|
23
|
+
|
24
|
+
require 'rake/testtask'
|
25
|
+
|
26
|
+
Rake::TestTask.new(:test) do |t|
|
27
|
+
t.libs << 'lib'
|
28
|
+
t.libs << 'test'
|
29
|
+
t.pattern = 'test/**/*_test.rb'
|
30
|
+
t.verbose = false
|
31
|
+
end
|
32
|
+
|
33
|
+
task default: :test
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module AcmePlugin
|
2
|
+
class ApplicationController < ActionController::Base
|
3
|
+
before_action :challenge_response, only: [:index]
|
4
|
+
before_action :validate_length, only: [:index]
|
5
|
+
protect_from_forgery with: :exception
|
6
|
+
|
7
|
+
def index
|
8
|
+
# There is only one item in DB with challenge response from our task
|
9
|
+
# we will use it to render plain text response
|
10
|
+
render plain: @response.response, status: :ok
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def validate_length
|
16
|
+
# Challenge request should have at least 128bit
|
17
|
+
challenge_failed('Challenge failed - Request has invalid length!') if invalid_length
|
18
|
+
end
|
19
|
+
|
20
|
+
def challenge_response
|
21
|
+
@response = AcmePlugin.config.challenge_dir_name.present? ? Challenge.new : Challenge.first
|
22
|
+
challenge_failed('Challenge failed - Can not get response from database!') if @response.nil?
|
23
|
+
end
|
24
|
+
|
25
|
+
def challenge_failed(msg)
|
26
|
+
Rails.logger.error(msg)
|
27
|
+
render plain: msg, status: :bad_request
|
28
|
+
end
|
29
|
+
|
30
|
+
def invalid_length
|
31
|
+
params[:challenge].nil? || params[:challenge].length < 16 || params[:challenge].length > 256
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module AcmePlugin
|
2
|
+
# if the project doesn't use ActiveRecord, we assume the challenge will
|
3
|
+
# be stored in the filesystem
|
4
|
+
if AcmePlugin.config.challenge_dir_name.blank? && defined?(ActiveRecord::Base) == 'constant' && ActiveRecord::Base.class == Class
|
5
|
+
class Challenge < ActiveRecord::Base
|
6
|
+
end
|
7
|
+
else
|
8
|
+
class Challenge
|
9
|
+
attr_accessor :response
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
full_challenge_dir = File.join(Rails.root, AcmePlugin.config.challenge_dir_name, 'challenge')
|
13
|
+
@response = IO.read(full_challenge_dir)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module AcmePlugin
|
2
|
+
if AcmePlugin.config.challenge_dir_name.blank? && defined?(ActiveRecord::Base) == 'constant' && ActiveRecord::Base.class == Class
|
3
|
+
class Setting < ActiveRecord::Base
|
4
|
+
end
|
5
|
+
else
|
6
|
+
class Setting
|
7
|
+
attr_accessor :private_key
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
data/config/routes.rb
ADDED
data/lib/acme_plugin.rb
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
require 'acme_plugin/engine'
|
2
|
+
require 'acme_plugin/file_output'
|
3
|
+
require 'acme_plugin/heroku_output'
|
4
|
+
require 'acme_plugin/file_store'
|
5
|
+
require 'acme_plugin/database_store'
|
6
|
+
require 'acme_plugin/configuration'
|
7
|
+
require 'acme_plugin/private_key_store'
|
8
|
+
require 'acme-client'
|
9
|
+
|
10
|
+
module AcmePlugin
|
11
|
+
def self.config
|
12
|
+
# load files on demand
|
13
|
+
@config ||= Configuration.load_file
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.config=(config)
|
17
|
+
@config = config
|
18
|
+
end
|
19
|
+
|
20
|
+
class CertGenerator
|
21
|
+
attr_reader :options, :cert, :client
|
22
|
+
|
23
|
+
def initialize(options = {})
|
24
|
+
@options = options
|
25
|
+
@options.freeze
|
26
|
+
end
|
27
|
+
|
28
|
+
def generate_certificate
|
29
|
+
register
|
30
|
+
domains = @options[:domain].split(' ')
|
31
|
+
return unless authorize_and_handle_challenge(domains)
|
32
|
+
# We can now request a certificate
|
33
|
+
Rails.logger.info('Creating CSR...')
|
34
|
+
@cert = @client.new_certificate(Acme::Client::CertificateRequest.new(names: domains))
|
35
|
+
save_certificate(@cert)
|
36
|
+
Rails.logger.info('Certificate has been generated.')
|
37
|
+
end
|
38
|
+
|
39
|
+
def authorize_and_handle_challenge(domains)
|
40
|
+
result = false
|
41
|
+
domains.each do |domain|
|
42
|
+
authorize(domain)
|
43
|
+
handle_challenge
|
44
|
+
request_challenge_verification
|
45
|
+
result = valid_verification_status
|
46
|
+
break unless result
|
47
|
+
end
|
48
|
+
result
|
49
|
+
end
|
50
|
+
|
51
|
+
def client
|
52
|
+
@client ||= Acme::Client.new(private_key: private_key, endpoint: @options[:endpoint])
|
53
|
+
end
|
54
|
+
|
55
|
+
def private_key
|
56
|
+
store ||= PrivateKeyStore.new(private_key_from_db) if @options.fetch(:private_key_in_db, false)
|
57
|
+
|
58
|
+
pk_id = @options.fetch(:private_key, nil)
|
59
|
+
|
60
|
+
raise 'Private key is not set, please check your config/acme_plugin.yml file!' if pk_id.nil? || pk_id.empty?
|
61
|
+
|
62
|
+
store ||= PrivateKeyStore.new(private_key_from_file(private_key_path(pk_id))) if File.file?(private_key_path(pk_id))
|
63
|
+
|
64
|
+
raise "Can not open private key: #{private_key_path(pk_id)}" if File.directory?(private_key_path(pk_id))
|
65
|
+
|
66
|
+
store ||= PrivateKeyStore.new(pk_id)
|
67
|
+
store.retrieve
|
68
|
+
end
|
69
|
+
|
70
|
+
def private_key_path(private_key_file)
|
71
|
+
Rails.root.join(private_key_file)
|
72
|
+
end
|
73
|
+
|
74
|
+
def private_key_from_db
|
75
|
+
settings = AcmePlugin::Setting.first
|
76
|
+
raise 'Empty private_key field in settings table!' if settings.private_key.nil?
|
77
|
+
settings.private_key
|
78
|
+
end
|
79
|
+
|
80
|
+
def private_key_from_file(filepath)
|
81
|
+
File.read(filepath)
|
82
|
+
end
|
83
|
+
|
84
|
+
def register
|
85
|
+
Rails.logger.info('Trying to register at Let\'s Encrypt service...')
|
86
|
+
registration = client.register(contact: "mailto:#{@options[:email]}")
|
87
|
+
registration.agree_terms
|
88
|
+
Rails.logger.info('Registration succeed.')
|
89
|
+
rescue => e
|
90
|
+
Rails.logger.info("#{e.class} - #{e.message}. Already registered.")
|
91
|
+
end
|
92
|
+
|
93
|
+
def common_domain_name
|
94
|
+
@domain ||= @options[:cert_name] || @options[:domain].split(' ').first.to_s
|
95
|
+
end
|
96
|
+
|
97
|
+
def authorize(domain = common_domain_name)
|
98
|
+
Rails.logger.info("Sending authorization request for: #{domain}...")
|
99
|
+
@authorization = client.authorize(domain: domain)
|
100
|
+
end
|
101
|
+
|
102
|
+
def store_challenge(challenge)
|
103
|
+
if @options[:challenge_dir_name].nil? || @options[:challenge_dir_name].empty?
|
104
|
+
DatabaseStore.new(challenge.file_content).store
|
105
|
+
else
|
106
|
+
FileStore.new(challenge.file_content, @options[:challenge_dir_name]).store
|
107
|
+
end
|
108
|
+
sleep(2)
|
109
|
+
end
|
110
|
+
|
111
|
+
def handle_challenge
|
112
|
+
@challenge = @authorization.http01
|
113
|
+
store_challenge(@challenge)
|
114
|
+
end
|
115
|
+
|
116
|
+
def request_challenge_verification
|
117
|
+
@challenge.request_verification
|
118
|
+
end
|
119
|
+
|
120
|
+
def wait_for_status(challenge)
|
121
|
+
Rails.logger.info('Waiting for challenge status...')
|
122
|
+
counter = 0
|
123
|
+
while challenge.verify_status == 'pending' && counter < 10
|
124
|
+
sleep(1)
|
125
|
+
counter += 1
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def valid_verification_status
|
130
|
+
wait_for_status(@challenge)
|
131
|
+
return true if @challenge.verify_status == 'valid'
|
132
|
+
Rails.logger.error('Challenge verification failed! ' \
|
133
|
+
"Error: #{@challenge.error['type']}: #{@challenge.error['detail']}")
|
134
|
+
false
|
135
|
+
end
|
136
|
+
|
137
|
+
# Save the certificate and key
|
138
|
+
def save_certificate(certificate)
|
139
|
+
return unless certificate
|
140
|
+
return HerokuOutput.new(common_domain_name, certificate).output unless ENV['DYNO'].nil?
|
141
|
+
output_dir = File.join(Rails.root, @options[:output_cert_dir])
|
142
|
+
return FileOutput.new(common_domain_name, certificate, output_dir).output if File.directory?(output_dir)
|
143
|
+
Rails.logger.error("Output directory: '#{output_dir}' does not exist!")
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module AcmePlugin
|
2
|
+
class CertificateOutput
|
3
|
+
def initialize(domain, cert)
|
4
|
+
@certificate = cert
|
5
|
+
@domain = domain
|
6
|
+
end
|
7
|
+
|
8
|
+
def output
|
9
|
+
display_info
|
10
|
+
|
11
|
+
output_cert('cert.pem', @certificate.to_pem)
|
12
|
+
output_cert('key.pem', @certificate.request.private_key.to_pem)
|
13
|
+
output_cert('chain.pem', @certificate.chain_to_pem)
|
14
|
+
output_cert('fullchain.pem', @certificate.fullchain_to_pem)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module AcmePlugin
|
2
|
+
class ChallengeStore
|
3
|
+
def initialize(challenge_content)
|
4
|
+
@content = challenge_content
|
5
|
+
end
|
6
|
+
|
7
|
+
def store
|
8
|
+
display_info
|
9
|
+
store_content
|
10
|
+
end
|
11
|
+
|
12
|
+
protected
|
13
|
+
|
14
|
+
def display_info
|
15
|
+
Rails.logger.info('Storing challenge information...')
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module AcmePlugin
|
2
|
+
Config = Class.new(OpenStruct)
|
3
|
+
|
4
|
+
# This is a class whose responsibility is to load the lets_encrypt configuration file
|
5
|
+
module Configuration
|
6
|
+
def self.load_file(filename = Rails.root.join('config', 'acme_plugin.yml'))
|
7
|
+
config_data = parse_yaml_file(filename)
|
8
|
+
create_config(config_data)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.create_config(config_hash)
|
12
|
+
Config.new(config_hash.merge(config_hash.fetch(Rails.env, {})) || {})
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.read_file(filename)
|
16
|
+
File.read(filename)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.evaluate_file(filename)
|
20
|
+
ERB.new(read_file(filename)).result
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.parse_yaml_file(filename)
|
24
|
+
YAML.load(evaluate_file(filename))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'acme_plugin/certificate_output'
|
2
|
+
|
3
|
+
module AcmePlugin
|
4
|
+
class FileOutput < CertificateOutput
|
5
|
+
def initialize(domain, cert, out_dir)
|
6
|
+
super(domain, cert)
|
7
|
+
@output_dir = out_dir
|
8
|
+
end
|
9
|
+
|
10
|
+
def output_cert(cert_type, cert_content)
|
11
|
+
File.write(File.join(@output_dir, "#{@domain}-#{cert_type}"), cert_content)
|
12
|
+
end
|
13
|
+
|
14
|
+
def display_info
|
15
|
+
Rails.logger.info('Saving certificates and key...')
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'acme_plugin/challenge_store'
|
2
|
+
|
3
|
+
module AcmePlugin
|
4
|
+
class FileStore < ChallengeStore
|
5
|
+
def initialize(challenge_content, challenge_dir_name)
|
6
|
+
super(challenge_content)
|
7
|
+
@challenge_dir_name = challenge_dir_name
|
8
|
+
end
|
9
|
+
|
10
|
+
def store_content
|
11
|
+
full_challenge_dir = File.join(Rails.root, @challenge_dir_name)
|
12
|
+
Dir.mkdir(full_challenge_dir) unless File.directory?(full_challenge_dir)
|
13
|
+
File.open(File.join(full_challenge_dir, 'challenge'), 'w') { |file| file.write(@content) }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'acme_plugin/certificate_output'
|
2
|
+
|
3
|
+
module AcmePlugin
|
4
|
+
class HerokuOutput < CertificateOutput
|
5
|
+
def initialize(domain, cert)
|
6
|
+
super(domain, cert)
|
7
|
+
end
|
8
|
+
|
9
|
+
def output_cert(cert_type, cert_content)
|
10
|
+
Rails.logger.info("====== #{@domain}-#{cert_type} ======")
|
11
|
+
puts cert_content
|
12
|
+
end
|
13
|
+
|
14
|
+
def display_info
|
15
|
+
Rails.logger.info('You are running this script on Heroku, please copy-paste certificates to your local machine')
|
16
|
+
Rails.logger.info('and then follow https://devcenter.heroku.com/articles/ssl-endpoint guide:')
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|