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.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/Rakefile +33 -0
  4. data/app/controllers/acme_plugin/application_controller.rb +34 -0
  5. data/app/models/acme_plugin/challenge.rb +17 -0
  6. data/app/models/acme_plugin/setting.rb +10 -0
  7. data/config/routes.rb +3 -0
  8. data/db/migrate/20151206135029_create_acme_plugin_challenges.rb +9 -0
  9. data/db/migrate/20160412195212_create_acme_plugin_settings.rb +9 -0
  10. data/lib/acme_plugin.rb +146 -0
  11. data/lib/acme_plugin/certificate_output.rb +17 -0
  12. data/lib/acme_plugin/challenge_store.rb +18 -0
  13. data/lib/acme_plugin/configuration.rb +27 -0
  14. data/lib/acme_plugin/database_store.rb +11 -0
  15. data/lib/acme_plugin/engine.rb +5 -0
  16. data/lib/acme_plugin/file_output.rb +18 -0
  17. data/lib/acme_plugin/file_store.rb +16 -0
  18. data/lib/acme_plugin/heroku_output.rb +19 -0
  19. data/lib/acme_plugin/private_key_store.rb +24 -0
  20. data/lib/acme_plugin/version.rb +5 -0
  21. data/lib/tasks/acme_plugin_tasks.rake +15 -0
  22. data/test/acme_plugin_test.rb +351 -0
  23. data/test/controllers/application_controller_test.rb +26 -0
  24. data/test/dummy/README.rdoc +28 -0
  25. data/test/dummy/Rakefile +6 -0
  26. data/test/dummy/app/assets/javascripts/application.js +13 -0
  27. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  28. data/test/dummy/app/controllers/application_controller.rb +5 -0
  29. data/test/dummy/app/helpers/application_helper.rb +2 -0
  30. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  31. data/test/dummy/bin/bundle +3 -0
  32. data/test/dummy/bin/rails +4 -0
  33. data/test/dummy/bin/rake +4 -0
  34. data/test/dummy/bin/setup +29 -0
  35. data/test/dummy/config.ru +4 -0
  36. data/test/dummy/config/acme_plugin.yml +19 -0
  37. data/test/dummy/config/application.rb +23 -0
  38. data/test/dummy/config/boot.rb +5 -0
  39. data/test/dummy/config/database.yml +25 -0
  40. data/test/dummy/config/environment.rb +5 -0
  41. data/test/dummy/config/environments/development.rb +41 -0
  42. data/test/dummy/config/environments/production.rb +79 -0
  43. data/test/dummy/config/environments/test.rb +45 -0
  44. data/test/dummy/config/initializers/assets.rb +11 -0
  45. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  46. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  47. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  48. data/test/dummy/config/initializers/inflections.rb +16 -0
  49. data/test/dummy/config/initializers/mime_types.rb +4 -0
  50. data/test/dummy/config/initializers/session_store.rb +3 -0
  51. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  52. data/test/dummy/config/locales/en.yml +23 -0
  53. data/test/dummy/config/routes.rb +3 -0
  54. data/test/dummy/config/secrets.yml +22 -0
  55. data/test/dummy/db/schema.rb +27 -0
  56. data/test/dummy/db/test.sqlite3 +0 -0
  57. data/test/dummy/key/test_keyfile_1024.pem +15 -0
  58. data/test/dummy/key/test_keyfile_2048.pem +27 -0
  59. data/test/dummy/key/test_keyfile_4096.pem +51 -0
  60. data/test/dummy/key/test_keyfile_8192.pem +99 -0
  61. data/test/dummy/log/development.log +0 -0
  62. data/test/dummy/log/test.log +25582 -0
  63. data/test/dummy/public/404.html +67 -0
  64. data/test/dummy/public/422.html +67 -0
  65. data/test/dummy/public/500.html +66 -0
  66. data/test/dummy/public/favicon.ico +0 -0
  67. data/test/fixtures/acme_plugin/challenges.yml +7 -0
  68. data/test/fixtures/acme_plugin/settings.yml +56 -0
  69. data/test/lib/acme_plugin/configuration_test.rb +13 -0
  70. data/test/lib/acme_plugin/private_key_store_test.rb +30 -0
  71. data/test/models/acme_plugin/challenge_test.rb +6 -0
  72. data/test/test_helper.rb +23 -0
  73. metadata +309 -0
@@ -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
@@ -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.
@@ -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
@@ -0,0 +1,3 @@
1
+ AcmePlugin::Engine.routes.draw do
2
+ get '.well-known/acme-challenge/:challenge' => 'application#index'
3
+ end
@@ -0,0 +1,9 @@
1
+ class CreateAcmePluginChallenges < ActiveRecord::Migration[4.2]
2
+ def change
3
+ create_table :acme_plugin_challenges do |t|
4
+ t.text :response
5
+
6
+ t.timestamps null: false
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ class CreateAcmePluginSettings < ActiveRecord::Migration[4.2]
2
+ def change
3
+ create_table :acme_plugin_settings do |t|
4
+ t.text :private_key
5
+
6
+ t.timestamps null: false
7
+ end
8
+ end
9
+ end
@@ -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,11 @@
1
+ require 'acme_plugin/challenge_store'
2
+
3
+ module AcmePlugin
4
+ class DatabaseStore < ChallengeStore
5
+ def store_content
6
+ ch = AcmePlugin::Challenge.first
7
+ ch = AcmePlugin::Challenge.new if ch.nil?
8
+ ch.update(response: @content)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ module AcmePlugin
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace AcmePlugin
4
+ end
5
+ 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