acme_manager 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: 9d9ddae0f05cc091f956174954b0b09ade18febb
4
+ data.tar.gz: 58ba851e02f8ca2c84c84db337a8e0594425f2ec
5
+ SHA512:
6
+ metadata.gz: a59e3a3a7964018a394fa9049f7eeb32b66613af882f05f0ee541c8010b228f13308194c5196af7275e4304c97e73b34cfeb9e0d6f45c06f2486469b2a764be1
7
+ data.tar.gz: d39a859e35552e44f6a707f5821be91a59d0f302a9f8e235f008ee7a81143855e10a130d4eb805e0b446522dc4473fbeb8fe2dbe1b59c5d28e59eedf8fc41f86
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.0
5
+ before_install: gem install bundler -v 1.14.6
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in acme_manager.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Dan Wentworth
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # AcmeManager
2
+
3
+ This is a client library for [acme-manager](https://github.com/atech/acme-manager), which is a tool to issue and manage letsencrypt certificates on a host.
4
+
5
+ The library enables you to view the certificates currently managed by an instance of acme-manager, issue new certificates and provides an optional middleware to redirect letsencrypt domain verification requests to acme-manager.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'acme_manager'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install acme_manager
22
+
23
+ ## Configuration
24
+
25
+ To configure the library you must configure the location of your acme-manager server and provide it's API key. You can do this with a configure block, or with the environment variables `ACME_MANAGER_HOST` and `ACME_MANAGER_API_KEY`.
26
+
27
+ ```ruby
28
+ # config/initializers/acme_manager.rb
29
+ AcmeManager.configure do |config|
30
+ config.host = 'https://acme-manager.example.com'
31
+ config.api_key = '1234567890'
32
+ end
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ ### Issuing a Certificate
38
+
39
+ To issue a certificate call `AcmeManager.issue` this will return an object which indicates whether the request was successful, and what errors were received if it wasn't.
40
+
41
+ ```ruby
42
+ issue_request = AcmeManager.issue('subdomain.example.com')
43
+ issue_request.success?
44
+ # => false
45
+ issue_request.error
46
+ # => "Error message raised by LE"
47
+ ```
48
+
49
+ ### Listing Certificates
50
+
51
+ To get a list of existing certificates managed me acme-manager call `AcmeManager.list`. This will return an array of
52
+ certificate objects.
53
+
54
+ ```ruby
55
+ certificate = AcmeManager.list.first
56
+ certificate.name
57
+ # => "subdomain.example.com"
58
+ certificate.not_after
59
+ # => 2017-05-17 16:18:22 UTC
60
+ certificate.expired?
61
+ # => false
62
+ ```
63
+
64
+ ### Verification Request Proxying
65
+
66
+ Acme-manager was designed to be run behind a load balancer, with any LetsEncrypt verification requests or requests to
67
+ /~acmemanager being sent to it.
68
+
69
+ If you're not lucky enough to have a load balancer that can siphon these requests for you, this library packs a
70
+ middleware that you can use to proxy the verification requests. Instructions below are for Rails, if you need to
71
+ include the middleware in another Rack app, you probably know what you're doing already.
72
+
73
+ ```ruby
74
+ # config/application.rb
75
+ # ...
76
+ require 'acme_manager/middleware/forward_verification_challenge'
77
+
78
+ module MyApp
79
+ class Application < Rails::Application
80
+ # ...
81
+ config.middleware.insert_before ActionDispatch::Static, AcmeManager::Middleware::ForwardVerificationChallenge
82
+ # ...
83
+ end
84
+ end
85
+ ```
86
+
87
+ ## Development
88
+
89
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
90
+
91
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
92
+
93
+ ## Contributing
94
+
95
+ Bug reports and pull requests are welcome on GitHub at https://github.com/atech/acme-manager-client.
96
+
97
+
98
+ ## License
99
+
100
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
101
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'acme_manager/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "acme_manager"
8
+ spec.version = AcmeManager::VERSION
9
+ spec.authors = ["Dan Wentworth"]
10
+ spec.email = ["dan@atechmedia.com"]
11
+
12
+ spec.summary = "Client library for the acme-manager server"
13
+ spec.description = "Provides a client library for interacting with the acme-manager server
14
+ (https://github.com/catphish/acme-manager) which assists with issuing lets-encrypt certificates"
15
+ spec.homepage = "https://github.com/darkphnx/acme-manager-client"
16
+ spec.license = "MIT"
17
+
18
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
19
+ f.match(%r{^(test|spec|features)/})
20
+ end
21
+ spec.bindir = "exe"
22
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
+ spec.require_paths = ["lib"]
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.14"
26
+ spec.add_development_dependency "rake", "~> 10.0"
27
+ spec.add_development_dependency "rspec", "~> 3.0"
28
+ end
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "acme_manager"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,55 @@
1
+ require "acme_manager/version"
2
+ require "acme_manager/configuration"
3
+ require "acme_manager/request"
4
+ require "acme_manager/certificate"
5
+ require "acme_manager/issue_request"
6
+
7
+ module AcmeManager
8
+ class Error < StandardError; end;
9
+
10
+ # @return [Configuration] The current configuration (or new if uninitialized)
11
+ def self.config
12
+ @config ||= Configuration.new
13
+ end
14
+
15
+ # Pass a block to configure the AcmeManager client
16
+ #
17
+ # @yieldparam [Configuration] Current configuration (see #config)
18
+ #
19
+ # @return [Configuration] Configuration after block has been called
20
+ def self.configure
21
+ yield config
22
+ config
23
+ end
24
+
25
+ # Get a list of certificates currently managed by the acme-manager
26
+ #
27
+ # @return [Array<Certificate>] List of certificates
28
+ def self.list
29
+ Certificate.all
30
+ end
31
+
32
+ # Instruct the acme-manager to issue a new certificate
33
+ #
34
+ # @param [String] name Domain name to issue a new certificate for
35
+ #
36
+ # @return [IssueRequest] Object containing result of the issue request
37
+ def self.issue(name)
38
+ IssueRequest.make(name)
39
+ end
40
+
41
+ def self.logger
42
+ @logger ||= begin
43
+ logger = Logger.new(config.log_path)
44
+ logger.level = config.log_level
45
+ logger
46
+ end
47
+ end
48
+
49
+ # Allow a custom logger to be set instead of configuring our own
50
+ #
51
+ # @param [Logger] new_logger Custom logger to set
52
+ def self.logger=(new_logger)
53
+ @logger = new_logger
54
+ end
55
+ end
@@ -0,0 +1,28 @@
1
+ module AcmeManager
2
+ # Represents a certificaate managed by acme-manager
3
+ class Certificate
4
+ attr_accessor :name, :not_after
5
+
6
+ # @param [String] name Domain name the certificate relates to
7
+ # @param [Time] not_after Timestamp representing the expiry time of the certificate
8
+ def initialize(name, not_after)
9
+ self.name = name
10
+ self.not_after = not_after
11
+ end
12
+
13
+ # Certificate is expired when we're past it's expiry time
14
+ def expired?
15
+ Time.now.utc > not_after
16
+ end
17
+
18
+ # Fetch a list of all certificates that acme-manager is currently managing
19
+ #
20
+ # @return [Array<Certificate>] All currently managed certificates
21
+ def self.all
22
+ Request.make('list').map do |cert_info|
23
+ AcmeManager.logger.info "Requesting list of certificates"
24
+ new(cert_info['name'], Time.iso8601(cert_info['not_after']))
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,34 @@
1
+ module AcmeManager
2
+ # All global configuration for AcmeManager client
3
+ class Configuration
4
+ attr_writer :host, :api_key, :log_path, :log_level
5
+
6
+ # @return [String] The hostname where acme-manager is running. Set via accessor or by reading environment variable
7
+ # ACME_MANAGER_HOST
8
+ #
9
+ # @raise [AcmeManager::Error] Raised when unconfigured
10
+ def host
11
+ @host || ENV['ACME_MANAGER_HOST'] || raise(Error, "`host` has not been configured. Set it using the " \
12
+ "`AcmeManager.configure` block or use the `ACME_MANAGER_HOST` environment variable")
13
+ end
14
+
15
+ # @return [String] The API key used to authenticate with acme-manager is running. Set via accessor or by reading
16
+ # environment variable ACME_MANAGER_API_KEY
17
+ #
18
+ # @raise [AcmeManager::Error] Raised when unconfigured
19
+ def api_key
20
+ @api_key || ENV['ACME_MANAGER_API_KEY'] || raise(Error, "`api_key` has not been configured. Set it using the " \
21
+ "`AcmeManager.configure` block or use the `ACME_MANAGER_API_KEY` environment variable")
22
+ end
23
+
24
+ # @return [IO] Where log output should be written to. STDOUT by default.
25
+ def log_path
26
+ @log_path || STDOUT
27
+ end
28
+
29
+ # @return [Integer] Severity level to write logs at. Logger::WARNING by default.
30
+ def log_level
31
+ @log_level || Logger::WARNING
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,48 @@
1
+ module AcmeManager
2
+ # Handles the request and response associated with issuing a new certificate through acme-manager
3
+ class IssueRequest
4
+ SUCCESSFUL_RESULTS = %w(issued not_due).freeze
5
+
6
+ attr_reader :name, :success, :error_type, :error
7
+
8
+ # @param [String] name Domain name to issue a cert for
9
+ def initialize(name)
10
+ @name = name
11
+ end
12
+
13
+ # Convenience method for issuing a new certificate
14
+ #
15
+ # @param [String] name Domain name to issue a cert for
16
+ #
17
+ # @return [IssueRequest] A new instance after the request has been made
18
+ def self.make(name)
19
+ request = new(name)
20
+ request.make
21
+ request
22
+ end
23
+
24
+ # Send a request to acme-manager to issue a new certificate. If the request is a failure error_type and error will
25
+ # be set containing the failure details.
26
+ #
27
+ # @return [Boolean] true if the request was successful.
28
+ def make
29
+ AcmeManager.logger.info "Requesting certificate issue for '#{name}'"
30
+ response = Request.make("issue/#{name}")
31
+
32
+ if SUCCESSFUL_RESULTS.include?(response['result'])
33
+ AcmeManager.logger.info "Issue for '#{name}' successful"
34
+ @success = true
35
+ else
36
+ @error_type = response['reason']['type']
37
+ @error = response['reason']['detail']
38
+ AcmeManager.logger.warn "Issue for '#{name}' failed - #{error_type}, #{error}"
39
+ @success = false
40
+ end
41
+ end
42
+
43
+ # Was the request sucessful? This will return false if the request hasn't been made yet
44
+ def success?
45
+ !!@success
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,22 @@
1
+ module AcmeManager
2
+ module Middleware
3
+ # If your setup is such that you cannot use a load-balancer to automatically syphon off LE requests to
4
+ # /.well-known/acme-challenge and direct them to acme-manager, you can use this middleware instead to act as a
5
+ # proxy between LE and acme-manager
6
+ class ForwardVerificationChallenge
7
+ def initialize(app)
8
+ @app = app
9
+ end
10
+
11
+ def call(env)
12
+ if env['PATH_INFO'] =~ %r{\A/.well-known/acme-challenge/}
13
+ AcmeManager.logger.info "Fowarding request to #{env['PATH_INFO']} to #{AcmeManager.config.host}"
14
+ result = Net::HTTP.get_response(URI("#{AcmeManager.config.host}#{env['PATH_INFO']}"))
15
+ [result.code.to_i, result.to_hash, [result.body]]
16
+ else
17
+ @app.call(env)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,74 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+ require 'json'
4
+
5
+ module AcmeManager
6
+ # Simplify the process of making an API request to acme-manager
7
+ class Request
8
+ PATH_PREFIX = '~acmemanager/'.freeze
9
+
10
+ # @param [String] path The API call you wish to make. This will have PATH_PREFIX prepended to it when
11
+ # making a request
12
+ def initialize(path)
13
+ @path = path
14
+ end
15
+
16
+ # Convenicence method to make a new instance and return the result of the request
17
+ #
18
+ # @param [String] path The API call you wish to make. See #new for more details.
19
+ def self.make(path)
20
+ new(path).make
21
+ end
22
+
23
+ # Make a request to the acme-manager API. Requests will be sent to the host defined in AcmeManager.config, and
24
+ # sent with the API key from the same configuration.
25
+ #
26
+ # Successfull responses are assumed to be in JSON format and are automatically parsed
27
+ #
28
+ # @return [Array, Hash] Parsed JSON data returned from the API
29
+ #
30
+ # @raise [Net::HTTPError] Raised when a non-2xx result is returned
31
+ def make
32
+ AcmeManager.logger.debug "Requesting #{request_uri}"
33
+
34
+ http = new_http_connection
35
+
36
+ request = Net::HTTP::Get.new(request_uri.path)
37
+ request['X-API-KEY'] = AcmeManager.config.api_key
38
+
39
+ result = http.request(request)
40
+
41
+ AcmeManager.logger.debug "Response Status: #{result.code}"
42
+ AcmeManager.logger.debug "Response Body:\n#{result.body}"
43
+
44
+ if result.is_a?(Net::HTTPSuccess)
45
+ JSON.parse(result.body)
46
+ else
47
+ AcmeManager.logger.warn "Request to #{request_uri} failed"
48
+ result.value
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ # Build a new connection object to the configured acme-manager host.
55
+ #
56
+ # @return [Net::HTTP] Connection object
57
+ def new_http_connection
58
+ http = Net::HTTP.start(request_uri.host, request_uri.port)
59
+ if request_uri.scheme == 'https'
60
+ http.use_ssl = true
61
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
62
+ end
63
+
64
+ http
65
+ end
66
+
67
+ # Build a complete URL for the request and parse it with URI
68
+ #
69
+ # @return [URI::HTTP] Parsed request URI
70
+ def request_uri
71
+ @request_uri ||= URI("#{AcmeManager.config.host}/#{PATH_PREFIX}#{@path}")
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,3 @@
1
+ module AcmeManager
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: acme_manager
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Dan Wentworth
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-05-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.14'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.14'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ description: |-
56
+ Provides a client library for interacting with the acme-manager server
57
+ (https://github.com/catphish/acme-manager) which assists with issuing lets-encrypt certificates
58
+ email:
59
+ - dan@atechmedia.com
60
+ executables: []
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - ".rspec"
65
+ - ".travis.yml"
66
+ - Gemfile
67
+ - LICENSE.txt
68
+ - README.md
69
+ - Rakefile
70
+ - acme_manager.gemspec
71
+ - bin/console
72
+ - bin/setup
73
+ - lib/acme_manager.rb
74
+ - lib/acme_manager/certificate.rb
75
+ - lib/acme_manager/configuration.rb
76
+ - lib/acme_manager/issue_request.rb
77
+ - lib/acme_manager/middleware/forward_verification_challenge.rb
78
+ - lib/acme_manager/request.rb
79
+ - lib/acme_manager/version.rb
80
+ homepage: https://github.com/darkphnx/acme-manager-client
81
+ licenses:
82
+ - MIT
83
+ metadata: {}
84
+ post_install_message:
85
+ rdoc_options: []
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ requirements: []
99
+ rubyforge_project:
100
+ rubygems_version: 2.5.1
101
+ signing_key:
102
+ specification_version: 4
103
+ summary: Client library for the acme-manager server
104
+ test_files: []