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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 970e5b0545fab8204c13aca8399e6807f1602ba6
4
- data.tar.gz: 2d5439882e197c9a5d13c0b75becbcd6f0f636db
3
+ metadata.gz: 77fec6d13d402402a206ec4d4691e5bac1e78f94
4
+ data.tar.gz: 0ff5be2480ed5bb421f678b301a9f8c4e1da6ea5
5
5
  SHA512:
6
- metadata.gz: c9290e25b80543791e4a271d0447e5b2b523f846b8cbc684b25be5eba7a8fd9417c0804175474d247773bcfe2142d5716a292597e79db056732e963e0171dcc6
7
- data.tar.gz: 9b0d082740c97fb5bbe2114cb05870fa864b0372e0579a124b9e9a0d1f1baa160dfe21c2b35b1a3a404ac136f5e12b3b9df2db39fd42b9d10ac52a244f887824
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("../test/dummy/Rakefile", __FILE__)
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 :get_challenge_response, only: [:index]
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
- def get_challenge_response
19
- @response = Challenge.first
20
- challenge_failed('Challenge failed - Can not get response from database!') if @response.nil?
21
- end
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
- def challenge_failed(msg)
24
- Rails.logger.error(msg)
25
- render plain: msg, status: :bad_request
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
- class Challenge < ActiveRecord::Base
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
@@ -1,4 +1,141 @@
1
- require "letsencrypt_plugin/engine"
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,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module LetsencryptPlugin
2
- VERSION = "0.0.6"
3
+ VERSION = '0.0.7'.freeze
3
4
  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 :letsencrypt_plugin => :setup_logger do
13
- def generate_certificate()
14
- client ||= Acme::Client.new(private_key: load_private_key, endpoint: CONFIG[:endpoint])
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