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