letsencrypt_plugin 0.0.6 → 0.0.7
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/Rakefile +1 -5
- data/app/controllers/letsencrypt_plugin/application_controller.rb +20 -15
- data/app/models/letsencrypt_plugin/challenge.rb +14 -1
- data/lib/letsencrypt_plugin.rb +138 -1
- data/lib/letsencrypt_plugin/certificate_output.rb +17 -0
- data/lib/letsencrypt_plugin/challenge_store.rb +18 -0
- data/lib/letsencrypt_plugin/database_store.rb +11 -0
- data/lib/letsencrypt_plugin/file_output.rb +18 -0
- data/lib/letsencrypt_plugin/file_store.rb +16 -0
- data/lib/letsencrypt_plugin/heroku_output.rb +19 -0
- data/lib/letsencrypt_plugin/version.rb +2 -1
- data/lib/tasks/letsencrypt_plugin_tasks.rake +4 -97
- data/test/cassettes/letsencrypt.log +0 -0
- data/test/cassettes/registration_agree_terms.yml +326 -0
- data/test/controllers/application_controller_test.rb +9 -4
- data/test/dummy/bin/setup +11 -11
- data/test/dummy/config/application.rb +1 -3
- data/test/dummy/config/letsencrypt_plugin.yml +7 -7
- data/test/dummy/config/routes.rb +1 -2
- 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/test.log +7718 -0
- data/test/le.sublime-project +8 -0
- data/test/le.sublime-workspace +686 -0
- data/test/letsencrypt_plugin_test.rb +79 -1
- data/test/test_helper.rb +27 -6
- metadata +126 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 77fec6d13d402402a206ec4d4691e5bac1e78f94
|
4
|
+
data.tar.gz: 0ff5be2480ed5bb421f678b301a9f8c4e1da6ea5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 94a936bfa6798b4917cbdcb0afa243f655cbcd6e6decd5451635ae08578bd676bcf5ccffd4a7c97d4df43d0f8884d01d902b1c5dc98e09b4b196153140d6f674
|
7
|
+
data.tar.gz: 4c4f999d13674ae6693b7a576fe657367f47cfbfb3e1916fb33a05e1fe4bedc44e7141fb11a51985eeea6d50ca502fdd9ea922b0b92e5aedddec7c7aaa3aa283
|
data/Rakefile
CHANGED
@@ -14,14 +14,11 @@ RDoc::Task.new(:rdoc) do |rdoc|
|
|
14
14
|
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
15
|
end
|
16
16
|
|
17
|
-
APP_RAKEFILE = File.expand_path(
|
17
|
+
APP_RAKEFILE = File.expand_path('../test/dummy/Rakefile', __FILE__)
|
18
18
|
load 'rails/tasks/engine.rake'
|
19
19
|
|
20
|
-
|
21
20
|
load 'rails/tasks/statistics.rake'
|
22
21
|
|
23
|
-
|
24
|
-
|
25
22
|
Bundler::GemHelper.install_tasks
|
26
23
|
|
27
24
|
require 'rake/testtask'
|
@@ -33,5 +30,4 @@ Rake::TestTask.new(:test) do |t|
|
|
33
30
|
t.verbose = false
|
34
31
|
end
|
35
32
|
|
36
|
-
|
37
33
|
task default: :test
|
@@ -1,28 +1,33 @@
|
|
1
1
|
module LetsencryptPlugin
|
2
2
|
class ApplicationController < ActionController::Base
|
3
|
-
before_action :
|
3
|
+
before_action :challenge_response, only: [:index]
|
4
4
|
before_action :validate_length, only: [:index]
|
5
|
-
|
5
|
+
|
6
6
|
def index
|
7
7
|
# There is only one item in DB with challenge response from our task
|
8
8
|
# we will use it to render plain text response
|
9
9
|
render plain: @response.response, status: :ok
|
10
10
|
end
|
11
|
-
|
11
|
+
|
12
12
|
private
|
13
|
-
def validate_length
|
14
|
-
# Challenge request should have at least 128bit
|
15
|
-
challenge_failed('Challenge failed - Request has invalid length!') if params[:challenge].nil? || params[:challenge].length < 16 || params[:challenge].length > 256
|
16
|
-
end
|
17
13
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
14
|
+
def validate_length
|
15
|
+
# Challenge request should have at least 128bit
|
16
|
+
challenge_failed('Challenge failed - Request has invalid length!') if invalid_length
|
17
|
+
end
|
18
|
+
|
19
|
+
def challenge_response
|
20
|
+
@response = CONFIG[:challenge_dir_name] ? Challenge.new : Challenge.first
|
21
|
+
challenge_failed('Challenge failed - Can not get response from database!') if @response.nil?
|
22
|
+
end
|
23
|
+
|
24
|
+
def challenge_failed(msg)
|
25
|
+
Rails.logger.error(msg)
|
26
|
+
render plain: msg, status: :bad_request
|
27
|
+
end
|
22
28
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
end
|
29
|
+
def invalid_length
|
30
|
+
params[:challenge].nil? || params[:challenge].length < 16 || params[:challenge].length > 256
|
31
|
+
end
|
27
32
|
end
|
28
33
|
end
|
@@ -1,4 +1,17 @@
|
|
1
1
|
module LetsencryptPlugin
|
2
|
-
|
2
|
+
# if the project doesn't use ActiveRecord, we assume the challenge will
|
3
|
+
# be stored in the filesystem
|
4
|
+
if 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, CONFIG[:challenge_dir_name], 'challenge')
|
13
|
+
@response = IO.read(full_challenge_dir)
|
14
|
+
end
|
15
|
+
end
|
3
16
|
end
|
4
17
|
end
|
data/lib/letsencrypt_plugin.rb
CHANGED
@@ -1,4 +1,141 @@
|
|
1
|
-
require
|
1
|
+
require 'letsencrypt_plugin/engine'
|
2
|
+
require 'letsencrypt_plugin/file_output'
|
3
|
+
require 'letsencrypt_plugin/heroku_output'
|
4
|
+
require 'letsencrypt_plugin/file_store'
|
5
|
+
require 'letsencrypt_plugin/database_store'
|
6
|
+
require 'openssl'
|
7
|
+
require 'acme/client'
|
2
8
|
|
3
9
|
module LetsencryptPlugin
|
10
|
+
class CertGenerator
|
11
|
+
attr_reader :options
|
12
|
+
|
13
|
+
def initialize(options = {})
|
14
|
+
@options = options
|
15
|
+
@options.freeze
|
16
|
+
end
|
17
|
+
|
18
|
+
def authorize_and_handle_challenge(domains)
|
19
|
+
result = false
|
20
|
+
domains.each do |domain|
|
21
|
+
authorize(domain)
|
22
|
+
handle_challenge
|
23
|
+
request_challenge_verification
|
24
|
+
result = valid_verification_status
|
25
|
+
break unless result
|
26
|
+
end
|
27
|
+
result
|
28
|
+
end
|
29
|
+
|
30
|
+
def generate_certificate
|
31
|
+
create_client
|
32
|
+
register
|
33
|
+
|
34
|
+
domains = @options[:domain].split(' ')
|
35
|
+
if authorize_and_handle_challenge(domains)
|
36
|
+
# We can now request a certificate
|
37
|
+
Rails.logger.info('Creating CSR...')
|
38
|
+
save_certificate(@client.new_certificate(Acme::Client::CertificateRequest.new(names: domains)))
|
39
|
+
Rails.logger.info('Certificate has been generated.')
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def create_client
|
44
|
+
@client ||= Acme::Client.new(private_key: load_private_key, endpoint: @options[:endpoint])
|
45
|
+
rescue Exception => e
|
46
|
+
Rails.logger.error(e.to_s)
|
47
|
+
raise e
|
48
|
+
end
|
49
|
+
|
50
|
+
def valid_key_size?(key)
|
51
|
+
key.n.num_bits >= 2048 && key.n.num_bits <= 4096
|
52
|
+
end
|
53
|
+
|
54
|
+
def privkey_path
|
55
|
+
fail 'Private key is not set, please check your '\
|
56
|
+
'config/letsencrypt_plugin.yml file!' if @options[:private_key].nil? || @options[:private_key].empty?
|
57
|
+
File.join(Rails.root, @options[:private_key])
|
58
|
+
end
|
59
|
+
|
60
|
+
def open_priv_key
|
61
|
+
private_key_path = privkey_path
|
62
|
+
fail "Can not open private key: #{private_key_path}" unless File.exist?(private_key_path) && !File.directory?(private_key_path)
|
63
|
+
OpenSSL::PKey::RSA.new(File.read(private_key_path))
|
64
|
+
end
|
65
|
+
|
66
|
+
def load_private_key
|
67
|
+
Rails.logger.info('Loading private key...')
|
68
|
+
private_key = open_priv_key
|
69
|
+
fail "Invalid key size: #{private_key.n.num_bits}." \
|
70
|
+
' Required size is between 2048 - 4096 bits' unless valid_key_size?(private_key)
|
71
|
+
private_key
|
72
|
+
end
|
73
|
+
|
74
|
+
def register
|
75
|
+
Rails.logger.info('Trying to register at Let\'s Encrypt service...')
|
76
|
+
begin
|
77
|
+
registration = @client.register(contact: "mailto:#{@options[:email]}")
|
78
|
+
registration.agree_terms
|
79
|
+
Rails.logger.info('Registration succeed.')
|
80
|
+
rescue
|
81
|
+
Rails.logger.info('Already registered.')
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def common_domain_name
|
86
|
+
@domain ||= @options[:domain].split(' ').first.to_s
|
87
|
+
end
|
88
|
+
|
89
|
+
def authorize(domain = common_domain_name)
|
90
|
+
Rails.logger.info("Sending authorization request for: #{domain}...")
|
91
|
+
@authorization = @client.authorize(domain: domain)
|
92
|
+
end
|
93
|
+
|
94
|
+
def store_challenge(challenge)
|
95
|
+
if @options[:challenge_dir_name].nil? || @options[:challenge_dir_name].empty?
|
96
|
+
DatabaseStore.new(challenge.file_content).store
|
97
|
+
else
|
98
|
+
FileStore.new(challenge.file_content, @options[:challenge_dir_name]).store
|
99
|
+
end
|
100
|
+
sleep(2)
|
101
|
+
end
|
102
|
+
|
103
|
+
def handle_challenge
|
104
|
+
@challenge = @authorization.http01
|
105
|
+
store_challenge(@challenge)
|
106
|
+
end
|
107
|
+
|
108
|
+
def request_challenge_verification
|
109
|
+
@challenge.request_verification
|
110
|
+
end
|
111
|
+
|
112
|
+
def wait_for_status(challenge)
|
113
|
+
Rails.logger.info('Waiting for challenge status...')
|
114
|
+
counter = 0
|
115
|
+
while challenge.verify_status == 'pending' && counter < 10
|
116
|
+
sleep(1)
|
117
|
+
counter += 1
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def valid_verification_status
|
122
|
+
wait_for_status(@challenge)
|
123
|
+
begin
|
124
|
+
Rails.logger.error('Challenge verification failed! ' \
|
125
|
+
"Error: #{@challenge.error['type']}: #{@challenge.error['detail']}")
|
126
|
+
return false
|
127
|
+
end unless @challenge.verify_status == 'valid'
|
128
|
+
true
|
129
|
+
end
|
130
|
+
|
131
|
+
# Save the certificate and key
|
132
|
+
def save_certificate(certificate)
|
133
|
+
begin
|
134
|
+
return HerokuOutput.new(common_domain_name, certificate).output unless ENV['DYNO'].nil?
|
135
|
+
output_dir = File.join(Rails.root, @options[:output_cert_dir])
|
136
|
+
return FileOutput.new(common_domain_name, certificate, output_dir).output if File.directory?(output_dir)
|
137
|
+
Rails.logger.error("Output directory: '#{output_dir}' does not exist!")
|
138
|
+
end unless certificate.nil?
|
139
|
+
end
|
140
|
+
end
|
4
141
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module LetsencryptPlugin
|
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 LetsencryptPlugin
|
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,11 @@
|
|
1
|
+
require 'letsencrypt_plugin/challenge_store'
|
2
|
+
|
3
|
+
module LetsencryptPlugin
|
4
|
+
class DatabaseStore < ChallengeStore
|
5
|
+
def store_content
|
6
|
+
ch = LetsencryptPlugin::Challenge.first
|
7
|
+
ch = LetsencryptPlugin::Challenge.new if ch.nil?
|
8
|
+
ch.update(response: @content)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'letsencrypt_plugin/certificate_output'
|
2
|
+
|
3
|
+
module LetsencryptPlugin
|
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 'letsencrypt_plugin/challenge_store'
|
2
|
+
|
3
|
+
module LetsencryptPlugin
|
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 'letsencrypt_plugin/certificate_output'
|
2
|
+
|
3
|
+
module LetsencryptPlugin
|
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
|
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'openssl'
|
2
2
|
require 'acme/client'
|
3
3
|
|
4
|
-
#Sets up logging - should only be called from other rake tasks
|
4
|
+
# Sets up logging - should only be called from other rake tasks
|
5
5
|
task setup_logger: :environment do
|
6
6
|
logger = Logger.new(STDOUT)
|
7
7
|
logger.level = Logger::INFO
|
@@ -9,100 +9,7 @@ task setup_logger: :environment do
|
|
9
9
|
end
|
10
10
|
|
11
11
|
desc "Generates SSL certificate using Let's Encrypt service"
|
12
|
-
task :
|
13
|
-
|
14
|
-
|
15
|
-
Rails.logger.info("Trying to register at Let's Encrypt service...")
|
16
|
-
begin
|
17
|
-
registration = client.register(contact: "mailto:#{CONFIG[:email]}")
|
18
|
-
registration.agree_terms
|
19
|
-
Rails.logger.info("Registration succeed.")
|
20
|
-
rescue
|
21
|
-
Rails.logger.info("Already registered.")
|
22
|
-
end
|
23
|
-
|
24
|
-
Rails.logger.info("Sending authorization request...")
|
25
|
-
authorization = client.authorize(domain: CONFIG[:domain])
|
26
|
-
challenge = authorization.http01
|
27
|
-
|
28
|
-
store_challenge(challenge)
|
29
|
-
|
30
|
-
challenge.request_verification # => true
|
31
|
-
|
32
|
-
wait_for_status(challenge)
|
33
|
-
|
34
|
-
if challenge.verify_status == 'valid'
|
35
|
-
# We can now request a certificate
|
36
|
-
certificate = client.new_certificate(create_csr)
|
37
|
-
save_certificate(certificate)
|
38
|
-
|
39
|
-
Rails.logger.info("Certificate has been generated.")
|
40
|
-
else
|
41
|
-
Rails.logger.error("Challenge verification failed! Error: #{challenge.error['type']}: #{challenge.error['detail']}")
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
def load_private_key
|
46
|
-
Rails.logger.info("Loading private key...")
|
47
|
-
OpenSSL::PKey::RSA.new(File.read(File.join(Rails.root, CONFIG[:private_key])))
|
48
|
-
end
|
49
|
-
|
50
|
-
def store_challenge(challenge)
|
51
|
-
Rails.logger.info("Storing challenge information...")
|
52
|
-
ch = LetsencryptPlugin::Challenge.first
|
53
|
-
if ch.nil?
|
54
|
-
ch = LetsencryptPlugin::Challenge.new
|
55
|
-
ch.save!(:response => challenge.file_content)
|
56
|
-
else
|
57
|
-
ch.update(:response => challenge.file_content)
|
58
|
-
end
|
59
|
-
sleep(2)
|
60
|
-
end
|
61
|
-
|
62
|
-
def wait_for_status(challenge)
|
63
|
-
Rails.logger.info("Waiting for challenge status...")
|
64
|
-
counter = 0
|
65
|
-
while challenge.verify_status == 'pending' && counter < 10
|
66
|
-
sleep(1)
|
67
|
-
counter += 1
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
def create_csr
|
72
|
-
Rails.logger.info("Creating CSR...")
|
73
|
-
Acme::Client::CertificateRequest.new(names: [ CONFIG[:domain] ])
|
74
|
-
end
|
75
|
-
|
76
|
-
# Save the certificate and key
|
77
|
-
def save_certificate(certificate)
|
78
|
-
if !certificate.nil?
|
79
|
-
if !ENV['DYNO'].nil?
|
80
|
-
Rails.logger.info('You are running this script on Heroku, please copy-paste certificates to your local machine')
|
81
|
-
Rails.logger.info('and then follow https://devcenter.heroku.com/articles/ssl-endpoint guide:')
|
82
|
-
|
83
|
-
Rails.logger.info("====== #{CONFIG[:domain]}-cert.pem ======")
|
84
|
-
puts certificate.to_pem
|
85
|
-
|
86
|
-
Rails.logger.info("====== #{CONFIG[:domain]}-key.pem ======")
|
87
|
-
puts certificate.request.private_key.to_pem
|
88
|
-
|
89
|
-
Rails.logger.info("====== #{CONFIG[:domain]}-chain.pem ======")
|
90
|
-
puts certificate.chain_to_pem
|
91
|
-
|
92
|
-
Rails.logger.info("====== #{CONFIG[:domain]}-fullchain.pem ======")
|
93
|
-
puts certificate.fullchain_to_pem
|
94
|
-
|
95
|
-
elsif File.directory?(File.join(Rails.root, CONFIG[:output_cert_dir]))
|
96
|
-
Rails.logger.info("Saving certificates and key...")
|
97
|
-
File.write(File.join(Rails.root, CONFIG[:output_cert_dir], "#{CONFIG[:domain]}-cert.pem"), certificate.to_pem)
|
98
|
-
File.write(File.join(Rails.root, CONFIG[:output_cert_dir], "#{CONFIG[:domain]}-key.pem"), certificate.request.private_key.to_pem)
|
99
|
-
File.write(File.join(Rails.root, CONFIG[:output_cert_dir], "#{CONFIG[:domain]}-chain.pem"), certificate.chain_to_pem)
|
100
|
-
File.write(File.join(Rails.root, CONFIG[:output_cert_dir], "#{CONFIG[:domain]}-fullchain.pem"), certificate.fullchain_to_pem)
|
101
|
-
else
|
102
|
-
Rails.logger.error("Output directory: '#{File.join(Rails.root, CONFIG[:output_cert_dir])}' does not exist!")
|
103
|
-
end
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
|
-
generate_certificate
|
12
|
+
task letsencrypt_plugin: :setup_logger do
|
13
|
+
cert_generator = LetsencryptPlugin::CertGenerator.new(CONFIG)
|
14
|
+
cert_generator.generate_certificate
|
108
15
|
end
|