rails-letsencrypt 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 97064ad048920d4fa44d9447f9d154d3573d366b
4
+ data.tar.gz: 79344a1b185cda5c7995b966cf7d95ce0976dfd1
5
+ SHA512:
6
+ metadata.gz: c2f8dbf68b900f909ea10f300b146c232de28e02dc00d40ab4d6925c9c60dd2a8f0dca208132bb827fd9472de74087ac046c7ee4efa28034d38e641d39798fff
7
+ data.tar.gz: e36cc6b648b879d66f2871ab5f023deff93b2f42416a820319d2e2c5f9232c6b2b2428a997545e3c453ae5581bba81c37b25a2398e698c31136799237a6e30a9
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2017 蒼時弦也
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/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # LetsEncrypt
2
+ Short description and motivation.
3
+
4
+ ## Usage
5
+ How to use my plugin.
6
+
7
+ ## Installation
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'lets_encrypt'
12
+ ```
13
+
14
+ And then execute:
15
+ ```bash
16
+ $ bundle
17
+ ```
18
+
19
+ Or install it yourself as:
20
+ ```bash
21
+ $ gem install lets_encrypt
22
+ ```
23
+
24
+ ## Contributing
25
+ Contribution directions go here.
26
+
27
+ ## License
28
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,21 @@
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 = 'LetsEncrypt'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ load 'rails/tasks/statistics.rake'
18
+
19
+ require 'bundler/gem_tasks'
20
+
21
+ task default: :stats
@@ -0,0 +1,5 @@
1
+ module LetsEncrypt
2
+ class ApplicationController < ActionController::Base
3
+ protect_from_forgery with: :exception
4
+ end
5
+ end
@@ -0,0 +1,25 @@
1
+ require_dependency 'lets_encrypt/application_controller'
2
+
3
+ module LetsEncrypt
4
+ # :nodoc:
5
+ class VerificationsController < ApplicationController
6
+ def show
7
+ return render_verification_string if certificate.present?
8
+ render plain: 'Verification not found', status: 404
9
+ end
10
+
11
+ protected
12
+
13
+ def render_verification_string
14
+ render plain: certificate.verification_string
15
+ end
16
+
17
+ def certificate
18
+ LetsEncrypt::Certificate.find_by(verification_path: filename)
19
+ end
20
+
21
+ def filename
22
+ ".well-known/acme-challenge/#{params[:verification_path]}"
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,4 @@
1
+ module LetsEncrypt
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,37 @@
1
+ module LetsEncrypt
2
+ # :nodoc:
3
+ module CertificateIssuable
4
+ extend ActiveSupport::Concern
5
+
6
+ def issue
7
+ logger.info "Getting certificate for #{domain}"
8
+ create_certificate
9
+ # rubocop:disable Metrics/LineLength
10
+ logger.info "Certificate issued (expires on #{expires_at}, will renew after #{renew_after})"
11
+ # rubocop:enable Metrics/LineLength
12
+ true
13
+ end
14
+
15
+ private
16
+
17
+ def csr
18
+ csr = OpenSSL::X509::Request.new
19
+ csr.subject = OpenSSL::X509::Name.new(
20
+ [['CN', domain, OpenSSL::ASN1::UTF8STRING]]
21
+ )
22
+ private_key = OpenSSL::PKey::RSA.new(key)
23
+ csr.public_key = private_key.public_key
24
+ csr.sign(private_key, OpenSSL::Digest::SHA256.new)
25
+ csr
26
+ end
27
+
28
+ def create_certificate
29
+ https_cert = LetsEncrypt.client.new_certificate(csr)
30
+ self.certificate = https_cert.to_pem
31
+ self.intermediaries = https_cert.chain_to_pem
32
+ self.expires_at = https_cert.x509.not_after
33
+ self.renew_after = (expires_at - 1.month) + rand(10).days
34
+ save!
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,66 @@
1
+ module LetsEncrypt
2
+ # :nodoc:
3
+ module CertificateVerifiable
4
+ extend ActiveSupport::Concern
5
+
6
+ def verify
7
+ start_authorize
8
+ start_challenge
9
+ wait_verify_status
10
+ check_verify_status
11
+ rescue Acme::Client::Error => e
12
+ retry_on_verify_error(e)
13
+ end
14
+
15
+ private
16
+
17
+ def start_authorize
18
+ authorization = LetsEncrypt.client.authorize(domain: domain)
19
+ @challenge = authorization.http01
20
+ self.verification_path = @challenge.filename
21
+ self.verification_string = @challenge.file_content
22
+ save!
23
+ end
24
+
25
+ def start_challenge
26
+ logger.info "Attempting verification of #{domain}"
27
+ @challenge.request_verification
28
+ end
29
+
30
+ def wait_verify_status
31
+ checks = 0
32
+ until @challenge.verify_status != 'pending'
33
+ checks += 1
34
+ if checks > 30
35
+ logger.info 'Status remained at pending for 30 checks'
36
+ return false
37
+ end
38
+ sleep 1
39
+ end
40
+ end
41
+
42
+ def check_verify_status
43
+ unless @challenge.verify_status == 'valid'
44
+ logger.info "Status was not valid (was: #{@challenge.verify_status})"
45
+ return false
46
+ end
47
+
48
+ true
49
+ end
50
+
51
+ def retry_on_verify_error
52
+ @retries = 0
53
+ if e.is_a?(Acme::Client::Error::BadNonce) && @retries < 5
54
+ @retries += 1
55
+ # rubocop:disable Metrics/LineLength
56
+ logger.info "Bad nounce encountered. Retrying (#{@retries} of 5 attempts)"
57
+ # rubocop:enable Metrics/LineLength
58
+ sleep 1
59
+ verify
60
+ else
61
+ logger.info "Error: #{e.class} (#{e.message})"
62
+ return false
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,5 @@
1
+ module LetsEncrypt
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,21 @@
1
+ module LetsEncrypt
2
+ # :nodoc:
3
+ class Certificate < ApplicationRecord
4
+ include CertificateVerifiable
5
+ include CertificateIssuable
6
+
7
+ validates :domain, presence: true, uniqueness: true
8
+
9
+ before_create -> { self.key = OpenSSL::PKey::RSA.new(4096).to_s }
10
+
11
+ def get
12
+ verify && issue
13
+ end
14
+
15
+ protected
16
+
17
+ def logger
18
+ LetsEncrypt.logger
19
+ end
20
+ end
21
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,3 @@
1
+ LetsEncrypt::Engine.routes.draw do
2
+ get '/acme-challenge/:verification_path', to: 'verifications#show'
3
+ end
@@ -0,0 +1,17 @@
1
+ class CreateLetsEncryptCertificates < ActiveRecord::Migration[5.1]
2
+ def change
3
+ create_table :lets_encrypt_certificates do |t|
4
+ t.string :domain
5
+ t.text :certificate, limit: 65535
6
+ t.text :intermediaries, limit: 65535
7
+ t.text :key, limit: 65535
8
+ t.datetime :expires_at
9
+ t.datetime :renew_after
10
+ t.string :verification_path
11
+ t.string :verification_string
12
+
13
+ t.index :domain
14
+ t.timestamps
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,52 @@
1
+ require 'openssl'
2
+ require 'acme-client'
3
+ require 'letsencrypt/engine'
4
+ require 'letsencrypt/logger_proxy'
5
+
6
+ # :nodoc:
7
+ module LetsEncrypt
8
+ def self.client
9
+ @client ||= ::Acme::Client.new(private_key: private_key, endpoint: endpoint)
10
+ end
11
+
12
+ def self.private_key
13
+ # TODO: Add options to retrieve key
14
+ @private_key ||= if private_key_path.exist?
15
+ OpenSSL::PKey::RSA.new(File.open(private_key_path))
16
+ else
17
+ generate_private_key
18
+ end
19
+ end
20
+
21
+ def self.endpoint
22
+ @endpoint ||= if Rails.env.production?
23
+ 'https://acme-v01.api.letsencrypt.org/'
24
+ else
25
+ 'https://acme-staging.api.letsencrypt.org'
26
+ end
27
+ end
28
+
29
+ def self.register(email)
30
+ registration = client.register(contact: "mailto:#{email}")
31
+ logger.info "Successfully registered private key with address #{email}"
32
+ registration.agree_terms
33
+ logger.info 'Terms have been accepted'
34
+ true
35
+ end
36
+
37
+ def self.private_key_path
38
+ # TODO: Add options for specify path
39
+ Rails.root.join('config', 'letsencrypt.key')
40
+ end
41
+
42
+ def self.generate_private_key
43
+ key = OpenSSL::PKey::RSA.new(4096)
44
+ File.open(private_key_path, 'w') { |f| f.write(key.to_s) }
45
+ logger.info "Created new private key for Let's Encrypt"
46
+ key
47
+ end
48
+
49
+ def self.logger
50
+ @logger ||= LoggerProxy.new(Rails.logger, tags: ['LetsEncrypt'])
51
+ end
52
+ end
@@ -0,0 +1,9 @@
1
+ module LetsEncrypt
2
+ # :nodoc:
3
+ class Engine < ::Rails::Engine
4
+ isolate_namespace LetsEncrypt
5
+ engine_name :letsencrypt
6
+
7
+ config.generators.test_framework :rspec
8
+ end
9
+ end
@@ -0,0 +1,37 @@
1
+ module LetsEncrypt
2
+ # :nodoc:
3
+ class LoggerProxy
4
+ attr_reader :tags
5
+
6
+ def initialize(logger, tags:)
7
+ @logger = logger
8
+ @tags = tags.flatten
9
+ end
10
+
11
+ def add_tags(*tags)
12
+ @tags += tags.flatten
13
+ @tags = @tags.uniq
14
+ end
15
+
16
+ def tag(logger)
17
+ if logger.respond_to?(:tagged)
18
+ current_tags = tags - logger.formatter.current_tags
19
+ logger.tagged(*current_tags) { yield }
20
+ else
21
+ yield
22
+ end
23
+ end
24
+
25
+ %i[debug info warn error fatal unknown].each do |severity|
26
+ define_method(severity) do |message|
27
+ log severity, message
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def log(type, message)
34
+ tag(@logger) { @logger.send type, message }
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,3 @@
1
+ module LetsEncrypt
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1 @@
1
+ require 'letsencrypt'
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :lets_encrypt do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rails-letsencrypt
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - 蒼時弦也
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-05-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 5.1.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 5.1.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: acme-client
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: sqlite3
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec-rails
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: The Let's Encrypt certificate manager for rails
70
+ email:
71
+ - elct9620@frost.tw
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - MIT-LICENSE
77
+ - README.md
78
+ - Rakefile
79
+ - app/controllers/lets_encrypt/application_controller.rb
80
+ - app/controllers/lets_encrypt/verifications_controller.rb
81
+ - app/jobs/lets_encrypt/application_job.rb
82
+ - app/models/concerns/lets_encrypt/certificate_issuable.rb
83
+ - app/models/concerns/lets_encrypt/certificate_verifiable.rb
84
+ - app/models/lets_encrypt/application_record.rb
85
+ - app/models/lets_encrypt/certificate.rb
86
+ - config/routes.rb
87
+ - db/migrate/20170505165114_create_lets_encrypt_certificates.rb
88
+ - lib/letsencrypt.rb
89
+ - lib/letsencrypt/engine.rb
90
+ - lib/letsencrypt/logger_proxy.rb
91
+ - lib/letsencrypt/version.rb
92
+ - lib/rails-letsencrypt.rb
93
+ - lib/tasks/lets_encrypt_tasks.rake
94
+ homepage: https://github.com/elct9620/rails-letsencrypt
95
+ licenses:
96
+ - MIT
97
+ metadata: {}
98
+ post_install_message:
99
+ rdoc_options: []
100
+ require_paths:
101
+ - lib
102
+ required_ruby_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ required_rubygems_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ requirements: []
113
+ rubyforge_project:
114
+ rubygems_version: 2.6.11
115
+ signing_key:
116
+ specification_version: 4
117
+ summary: The Let's Encrypt certificate manager for rails
118
+ test_files: []