acme_plugin 0.0.13
Sign up to get free protection for your applications and to get access to all the features.
- 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
|