letsencrypt_webfaction 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.rubocop.yml +37 -0
- data/.travis.yml +8 -0
- data/Gemfile +4 -0
- data/README.md +87 -0
- data/Rakefile +16 -0
- data/bin/console +7 -0
- data/bin/rake +16 -0
- data/bin/rspec +16 -0
- data/bin/rubocop +16 -0
- data/bin/setup +6 -0
- data/config.defaults.yml +7 -0
- data/config.example.yml +8 -0
- data/exe/letsencrypt_webfaction +5 -0
- data/letsencrypt_webfaction.gemspec +37 -0
- data/lib/letsencrypt_webfaction.rb +3 -0
- data/lib/letsencrypt_webfaction/application.rb +81 -0
- data/lib/letsencrypt_webfaction/args_parser.rb +101 -0
- data/lib/letsencrypt_webfaction/args_parser/array_validator.rb +9 -0
- data/lib/letsencrypt_webfaction/args_parser/defined_values_validator.rb +13 -0
- data/lib/letsencrypt_webfaction/args_parser/field.rb +33 -0
- data/lib/letsencrypt_webfaction/args_parser/string_validator.rb +9 -0
- data/lib/letsencrypt_webfaction/certificate_writer.rb +35 -0
- data/lib/letsencrypt_webfaction/domain_validator.rb +53 -0
- data/lib/letsencrypt_webfaction/instructions.rb +35 -0
- metadata +172 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 426a3c583e582e83ea8402d1b2e8d08803be339f
|
4
|
+
data.tar.gz: 58b5b5ac5d36b69847e4366681c0e7e2d68edb4c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 73db72bcf5ed848ecb11358bdee275596fbb5368620f156cd632ff5ff8d3ddf3061646c39bae8003043d55dd3ddf6ea94325c5a4dccd6a6b06a8e5c8d0fa9ecd
|
7
|
+
data.tar.gz: ed3e7670be93c3835e1c89ca22f3a43a9aa1bb1f98d3247983335c3b2d459235dc65cbd837445461cb994fdb21483c62b8dc0a1c16e7dab25506c5e32627922d
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
AllCops:
|
2
|
+
Exclude:
|
3
|
+
# These are autogenerated binstubs.
|
4
|
+
- 'bin/*'
|
5
|
+
# Keep rubocop from scanning installed gems.
|
6
|
+
- 'vendor/**/*'
|
7
|
+
# Line length is something that isn't dogmatic. 80 chars is a good recommendation.
|
8
|
+
Metrics/LineLength:
|
9
|
+
Enabled: false
|
10
|
+
# This requires leading comments which can tend to be redundant.
|
11
|
+
Style/Documentation:
|
12
|
+
Enabled: false
|
13
|
+
|
14
|
+
|
15
|
+
# Extra cops:
|
16
|
+
|
17
|
+
# Encourages block syntax for things like File.open
|
18
|
+
Style/AutoResourceCleanup:
|
19
|
+
Enabled: true
|
20
|
+
|
21
|
+
# Standardize multi-line params.
|
22
|
+
Style/FirstArrayElementLineBreak:
|
23
|
+
Enabled: true
|
24
|
+
Style/FirstHashElementLineBreak:
|
25
|
+
Enabled: true
|
26
|
+
Style/FirstMethodArgumentLineBreak:
|
27
|
+
Enabled: true
|
28
|
+
Style/FirstMethodParameterLineBreak:
|
29
|
+
Enabled: true
|
30
|
+
|
31
|
+
# Encourage using public_send to prove that you actually want to bypass visibility.
|
32
|
+
Style/Send:
|
33
|
+
Enabled: true
|
34
|
+
|
35
|
+
# If you interpolate a literal, just include it in the string itself.
|
36
|
+
Lint/LiteralInInterpolation:
|
37
|
+
Enabled: true
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
# LetsEncrypt Webfaction
|
2
|
+
|
3
|
+
LetsEncrypt utility client for WebFaction hosts.
|
4
|
+
|
5
|
+
This tool simplifies the manual process of using LetsEncrypt on Webfaction hosts. It can be added to cron where it will validate your domains automatically, place the generated certificates in a common folder, and then email you directions and example text to send the WebFaction support team.
|
6
|
+
|
7
|
+
[![Build Status](https://travis-ci.org/will-in-wi/letsencrypt-webfaction.svg?branch=master)](https://travis-ci.org/will-in-wi/letsencrypt-webfaction)
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Because WebFaction has an unusual Ruby setup with the default Ruby as 1.8, I recommend that you [set up RBEnv](https://github.com/rbenv/rbenv) and [Ruby Build](https://github.com/rbenv/ruby-build#readme) on your WebFaction server to run this script.
|
12
|
+
|
13
|
+
Once you have done so, install Ruby 2.1+ (probably 2.3.0 at time of writing). Then set the local Ruby and install the Gem. Finally unset the local Ruby so that you don't run into problems.
|
14
|
+
|
15
|
+
$ rbenv install 2.3.0
|
16
|
+
$ rbenv local 2.3.0
|
17
|
+
$ gem install letsencrypt_webfaction
|
18
|
+
$ rbenv rehash
|
19
|
+
$ rm .ruby-version
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
Basic example:
|
24
|
+
|
25
|
+
$ letsencrypt_webfaction --contact me@example.com --domains example.com,www.example.com --public ~/webapps/myapp/public_html/
|
26
|
+
|
27
|
+
To quickly get a list of parameters, you can call:
|
28
|
+
|
29
|
+
$ letsencrypt_webfaction --help
|
30
|
+
|
31
|
+
### Cron usage
|
32
|
+
|
33
|
+
Normally, you will run the script manually once to get the certificate, and then you will use Cron to automate future certificate renewal.
|
34
|
+
|
35
|
+
Your cron task could look something like:
|
36
|
+
|
37
|
+
0 4 * */2 * cd /home/williaminwi/Projects/letsencrypt-webfaction && RBENV_ROOT=~/.rbenv RBENV_VERSION=2.3.0 ~/.rbenv/bin/rbenv exec letsencrypt_webfaction --contact you@example.com --domains example.com,www.example.com --public ~/webapps/myapp/
|
38
|
+
|
39
|
+
This [would run](http://crontab.guru/#0_4_*_*/2_*) at 4 a.m. in Jan, Mar, May, Jul, Sep, and Nov. Certificates expire three months after issuance, so modify as desired.
|
40
|
+
|
41
|
+
If you have more than one cron task running like this, you may want to set the environment variables at the top of the file, and create a config file containing the contact information.
|
42
|
+
|
43
|
+
### Detailed examples
|
44
|
+
|
45
|
+
Default parameters can be found in [config.defaults.yml](./config.defaults.yml). All of the parameters can be overridden by passing another config file, arguments to the executable, or both. If a config file and arguments are passed, they will be interleaved with the arguments having precedence.
|
46
|
+
|
47
|
+
A config file needs to be in YAML format and have a subset of the keys in [config.defaults.yml](./config.defaults.yml). If you use a config file, you pass the `--config ./myconfig.yml` parameter.
|
48
|
+
|
49
|
+
This allows you to set up a cron task for multiple sites with the defaults for all of them (such as your email address) in a config file, and site specific directives in the command. For example:
|
50
|
+
|
51
|
+
$ letsencrypt_webfaction --config ~/le_config.yml --domains example.com,www.example.com --public ~/webapps/myapp/public_html/
|
52
|
+
|
53
|
+
This could be run automatically every two months.
|
54
|
+
|
55
|
+
### Operation
|
56
|
+
|
57
|
+
When the code runs, it places verification files into a public directory, validates the domains with LetsEncrypt (or your ACME provider), and then dumps the signed certificate and private key into an output folder. By default, the output folder is `~/le_certs/`, inside which it will create `[domain_name]/[timestamp]/`.
|
58
|
+
|
59
|
+
After this is done, the utility will dump to stdout instructions regarding how to request that your new certs be installed by the WebFaction admins. This will include sample text and a link to https://help.webfaction.com/.
|
60
|
+
|
61
|
+
If you use Cron, you should set `MAILTO=youremail@example.com` and `MAILFROM=username@servername.webfaction.com` at the top in order to receive the instructions. The [WebFaction docs](https://docs.webfaction.com/software/general.html#scheduling-tasks-cron) have more details about how to do this.
|
62
|
+
|
63
|
+
If you see messages containing SyntaxErrors, you are most likely using an old version of Ruby. This utility requires Ruby 2.1+.
|
64
|
+
|
65
|
+
### Public folders
|
66
|
+
|
67
|
+
For this utility to work, it is assumed that there is a folder which is directly served at `http://yourdomain/` into which the ACME verification files can be placed.
|
68
|
+
|
69
|
+
In the case of a PHP site, such as Drupal or Wordpress, look for the folder with `index.php` in it. This is usually in `/home/myuser/webapps/myapp/`.
|
70
|
+
|
71
|
+
In the case of a Rails app, look for a folder called `public/`. If you are deploying your app with Capistrano, this could show up in `/home/myuser/webapps/myapp/current/public/`.
|
72
|
+
|
73
|
+
## Development
|
74
|
+
|
75
|
+
To run the script directly from the repository, use:
|
76
|
+
|
77
|
+
$ ruby -Ilib exe/letsencrypt_webfaction
|
78
|
+
|
79
|
+
To test certificate issuance, consider using the [LetsEncrypt staging server](https://community.letsencrypt.org/t/testing-against-the-lets-encrypt-staging-environment/6763). This doesn't have the 5 certs per domain every 7 days rate limit. You can add the `--endpoint https://acme-staging.api.letsencrypt.org/` parameter to do so.
|
80
|
+
|
81
|
+
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.
|
82
|
+
|
83
|
+
To install this gem onto your local machine, run `bin/rake install`. To release a new version, update the version number in `lib/letsencrypt_webfaction.rb`, and then run `bin/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). This project uses [Semantic Versioning](http://semver.org/).
|
84
|
+
|
85
|
+
## Contributing
|
86
|
+
|
87
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/will-in-wi/letsencrypt-webfaction
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
require 'rubocop/rake_task'
|
4
|
+
|
5
|
+
RSpec::Core::RakeTask.new(:spec)
|
6
|
+
RuboCop::RakeTask.new
|
7
|
+
|
8
|
+
namespace :test do
|
9
|
+
desc 'Run all tests'
|
10
|
+
task :all do
|
11
|
+
Rake::Task['rubocop'].invoke
|
12
|
+
Rake::Task['spec'].invoke
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
task default: 'test:all'
|
data/bin/console
ADDED
data/bin/rake
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# This file was generated by Bundler.
|
4
|
+
#
|
5
|
+
# The application 'rake' is installed as part of a gem, and
|
6
|
+
# this file is here to facilitate running it.
|
7
|
+
#
|
8
|
+
|
9
|
+
require "pathname"
|
10
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
11
|
+
Pathname.new(__FILE__).realpath)
|
12
|
+
|
13
|
+
require "rubygems"
|
14
|
+
require "bundler/setup"
|
15
|
+
|
16
|
+
load Gem.bin_path("rake", "rake")
|
data/bin/rspec
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# This file was generated by Bundler.
|
4
|
+
#
|
5
|
+
# The application 'rspec' is installed as part of a gem, and
|
6
|
+
# this file is here to facilitate running it.
|
7
|
+
#
|
8
|
+
|
9
|
+
require "pathname"
|
10
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
11
|
+
Pathname.new(__FILE__).realpath)
|
12
|
+
|
13
|
+
require "rubygems"
|
14
|
+
require "bundler/setup"
|
15
|
+
|
16
|
+
load Gem.bin_path("rspec-core", "rspec")
|
data/bin/rubocop
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# This file was generated by Bundler.
|
4
|
+
#
|
5
|
+
# The application 'rubocop' is installed as part of a gem, and
|
6
|
+
# this file is here to facilitate running it.
|
7
|
+
#
|
8
|
+
|
9
|
+
require "pathname"
|
10
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
11
|
+
Pathname.new(__FILE__).realpath)
|
12
|
+
|
13
|
+
require "rubygems"
|
14
|
+
require "bundler/setup"
|
15
|
+
|
16
|
+
load Gem.bin_path("rubocop", "rubocop")
|
data/bin/setup
ADDED
data/config.defaults.yml
ADDED
data/config.example.yml
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'letsencrypt_webfaction'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'letsencrypt_webfaction'
|
8
|
+
spec.version = LetsencryptWebfaction::VERSION
|
9
|
+
spec.authors = ['William Johnston']
|
10
|
+
spec.email = ['william@johnstonhaus.us']
|
11
|
+
|
12
|
+
spec.summary = 'LetsEncrypt utility client for WebFaction hosts.'
|
13
|
+
spec.description = 'A tool to simplify the manual process of using ' \
|
14
|
+
'LetsEncrypt on Webfaction hosts. It can be added to ' \
|
15
|
+
'cron where it will validate your domains ' \
|
16
|
+
'automatically, place the generated certs in a common ' \
|
17
|
+
'folder, and then email you directions and example ' \
|
18
|
+
'text to send the WebFaction support team.'
|
19
|
+
spec.homepage = 'https://github.com/will-in-wi/letsencrypt-webfaction'
|
20
|
+
spec.license = 'MIT'
|
21
|
+
|
22
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
23
|
+
spec.bindir = 'exe'
|
24
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
25
|
+
spec.require_paths = %w(lib)
|
26
|
+
|
27
|
+
spec.required_ruby_version = '>= 2.1.0'
|
28
|
+
|
29
|
+
spec.add_runtime_dependency 'acme-client', '~> 0.3.0'
|
30
|
+
|
31
|
+
spec.add_development_dependency 'bundler', '~> 1.11'
|
32
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
33
|
+
spec.add_development_dependency 'rspec', '~> 3.4'
|
34
|
+
spec.add_development_dependency 'rubocop', '~> 0.37'
|
35
|
+
spec.add_development_dependency 'simplecov', '~> 0.11'
|
36
|
+
spec.add_development_dependency 'pry', '~> 0.10'
|
37
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'acme/client'
|
3
|
+
|
4
|
+
require 'letsencrypt_webfaction/args_parser'
|
5
|
+
require 'letsencrypt_webfaction/domain_validator'
|
6
|
+
require 'letsencrypt_webfaction/certificate_writer'
|
7
|
+
require 'letsencrypt_webfaction/instructions'
|
8
|
+
|
9
|
+
module LetsencryptWebfaction
|
10
|
+
class Application
|
11
|
+
def initialize
|
12
|
+
@options = LetsencryptWebfaction::ArgsParser.new(ARGV)
|
13
|
+
end
|
14
|
+
|
15
|
+
def run!
|
16
|
+
# Validate that the correct options were passed.
|
17
|
+
validate_options!
|
18
|
+
|
19
|
+
# Register the private key.
|
20
|
+
register_key!
|
21
|
+
|
22
|
+
# Validate the domains.
|
23
|
+
validator.validate!
|
24
|
+
|
25
|
+
# Write the obtained certificates.
|
26
|
+
certificate_writer.write!
|
27
|
+
|
28
|
+
# Dump help text.
|
29
|
+
puts instructions.message
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def instructions
|
35
|
+
@instructions ||= LetsencryptWebfaction::Instructions.new certificate_writer.output_dir, @options.domains
|
36
|
+
end
|
37
|
+
|
38
|
+
def certificate_writer
|
39
|
+
@certificate_writer ||= LetsencryptWebfaction::CertificateWriter.new(@options.output_dir, @options.domains.first, certificate)
|
40
|
+
end
|
41
|
+
|
42
|
+
def certificate
|
43
|
+
# We can now request a certificate, you can pass anything that returns
|
44
|
+
# a valid DER encoded CSR when calling to_der on it, for example a
|
45
|
+
# OpenSSL::X509::Request too.
|
46
|
+
@certificate ||= client.new_certificate(csr)
|
47
|
+
end
|
48
|
+
|
49
|
+
def csr
|
50
|
+
# We're going to need a certificate signing request. If not explicitly
|
51
|
+
# specified, the first name listed becomes the common name.
|
52
|
+
@csr ||= Acme::Client::CertificateRequest.new(names: @options.domains)
|
53
|
+
end
|
54
|
+
|
55
|
+
def validator
|
56
|
+
@validator ||= LetsencryptWebfaction::DomainValidator.new @options.domains, client, @options.public
|
57
|
+
end
|
58
|
+
|
59
|
+
def client
|
60
|
+
@client ||= Acme::Client.new(private_key: private_key, endpoint: @options.endpoint)
|
61
|
+
end
|
62
|
+
|
63
|
+
def register_key!
|
64
|
+
# If the private key is not known to the server, we need to register it for the first time.
|
65
|
+
registration = client.register(contact: "mailto:#{@options.contact}")
|
66
|
+
|
67
|
+
# You'll may need to agree to the term (that's up the to the server to require it or not but boulder does by default)
|
68
|
+
registration.agree_terms
|
69
|
+
end
|
70
|
+
|
71
|
+
def validate_options!
|
72
|
+
return if @options.valid?
|
73
|
+
puts @options.errors.values.join("\n")
|
74
|
+
exit
|
75
|
+
end
|
76
|
+
|
77
|
+
def private_key
|
78
|
+
OpenSSL::PKey::RSA.new(@options.key_size)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
require 'letsencrypt_webfaction/args_parser/field'
|
5
|
+
require 'letsencrypt_webfaction/args_parser/string_validator'
|
6
|
+
require 'letsencrypt_webfaction/args_parser/defined_values_validator'
|
7
|
+
require 'letsencrypt_webfaction/args_parser/array_validator'
|
8
|
+
|
9
|
+
module LetsencryptWebfaction
|
10
|
+
class ArgsParser
|
11
|
+
BANNER = 'Usage: get_cert [options]'.freeze
|
12
|
+
DEFAULTS_PATH = 'config.defaults.yml'.freeze
|
13
|
+
VALID_KEY_SIZES = [2048, 4096].freeze
|
14
|
+
|
15
|
+
FIELDS = [
|
16
|
+
Field::IntegerField.new(:key_size, 'Size of private key (e.g. 4096)', [DefinedValuesValidator.new(VALID_KEY_SIZES)]),
|
17
|
+
Field.new(:endpoint, 'ACME endpoint (e.g. https://acme-v01.api.letsencrypt.org/)', [StringValidator.new]),
|
18
|
+
Field.new(:contact, 'Email address to notify on renewal', [StringValidator.new]),
|
19
|
+
Field::ListField.new(:domains, 'Comma separated list of domains. The first one will be the common name.', [ArrayValidator.new]),
|
20
|
+
Field.new(:public, 'Location on the filesystem served by the desired site (e.g. ~/webapps/myapp/public_html)', [StringValidator.new]),
|
21
|
+
Field.new(:output_dir, 'Location on the filesystem to which the certs will be saved.', [StringValidator.new])
|
22
|
+
].freeze
|
23
|
+
|
24
|
+
# Set up getters.
|
25
|
+
FIELDS.map(&:identifier).each do |field|
|
26
|
+
attr_reader field
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(options)
|
30
|
+
@options = options
|
31
|
+
|
32
|
+
@errors = {}
|
33
|
+
|
34
|
+
# Set defaults from default config file.
|
35
|
+
load_config!(DEFAULTS_PATH)
|
36
|
+
|
37
|
+
# TODO: Rework this to not exit on instantiation due to help text.
|
38
|
+
parse!
|
39
|
+
end
|
40
|
+
|
41
|
+
def errors
|
42
|
+
errors = {}
|
43
|
+
|
44
|
+
FIELDS.each do |field|
|
45
|
+
val = instance_variable_get("@#{field.identifier}")
|
46
|
+
next if field.valid? val
|
47
|
+
errors[field.identifier] ||= []
|
48
|
+
errors[field.identifier] << "Invalid #{field.identifier} '#{val}'"
|
49
|
+
end
|
50
|
+
|
51
|
+
errors
|
52
|
+
end
|
53
|
+
|
54
|
+
def valid?
|
55
|
+
errors.empty?
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def load_config!(config_path)
|
61
|
+
config = YAML.load_file(config_path)
|
62
|
+
FIELDS.each do |field|
|
63
|
+
instance_variable_set("@#{field.identifier}", config[field.identifier.to_s])
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def handle_config(opts)
|
68
|
+
opts.on('--config=CONFIG', 'Path to config file. Arguments passed to the program will override corresponding directives in the config file.') do |c|
|
69
|
+
# Set defaults.
|
70
|
+
load_config!(c)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def handle_help(opts)
|
75
|
+
opts.on('-h', '--help', 'Prints this help') do
|
76
|
+
puts opts
|
77
|
+
exit
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def handle_field(opts, field)
|
82
|
+
opts.on("--#{field.identifier}=#{field.identifier.upcase}", field.description) do |val|
|
83
|
+
instance_variable_set("@#{field.identifier}", field.sanitize(val))
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def opt_parser
|
88
|
+
OptionParser.new do |opts|
|
89
|
+
opts.banner = BANNER
|
90
|
+
|
91
|
+
handle_config(opts)
|
92
|
+
handle_help(opts)
|
93
|
+
FIELDS.each { |field| handle_field(opts, field) }
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def parse!
|
98
|
+
opt_parser.parse!(@options)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module LetsencryptWebfaction
|
2
|
+
class ArgsParser
|
3
|
+
class Field
|
4
|
+
attr_reader :identifier, :description, :validators
|
5
|
+
|
6
|
+
def initialize(identifier, description, validators = [])
|
7
|
+
@identifier = identifier
|
8
|
+
@description = description
|
9
|
+
@validators = validators
|
10
|
+
end
|
11
|
+
|
12
|
+
def sanitize(val)
|
13
|
+
val
|
14
|
+
end
|
15
|
+
|
16
|
+
def valid?(val)
|
17
|
+
validators.reject { |validator| validator.valid?(val) }.empty?
|
18
|
+
end
|
19
|
+
|
20
|
+
class IntegerField < Field
|
21
|
+
def sanitize(val)
|
22
|
+
val.to_i
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class ListField < Field
|
27
|
+
def sanitize(val)
|
28
|
+
val.split(',').map(&:strip).compact
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module LetsencryptWebfaction
|
2
|
+
class CertificateWriter
|
3
|
+
attr_reader :output_dir
|
4
|
+
|
5
|
+
def initialize(output_dir, domain, certificate)
|
6
|
+
@certificate = certificate
|
7
|
+
|
8
|
+
cert_date = Time.now.strftime('%Y%m%d%H%M%S')
|
9
|
+
|
10
|
+
expanded_dir = File.expand_path(output_dir)
|
11
|
+
@output_dir = File.join(expanded_dir, domain, cert_date)
|
12
|
+
end
|
13
|
+
|
14
|
+
def write!
|
15
|
+
create_folder!
|
16
|
+
|
17
|
+
# Save the certificate and key
|
18
|
+
write_file!('privkey.pem', @certificate.request.private_key.to_pem)
|
19
|
+
write_file!('cert.pem', @certificate.to_pem)
|
20
|
+
write_file!('chain.pem', @certificate.chain_to_pem)
|
21
|
+
write_file!('fullchain.pem', @certificate.fullchain_to_pem)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def create_folder!
|
27
|
+
# Make sure the output directory exists.
|
28
|
+
FileUtils.mkdir_p(output_dir)
|
29
|
+
end
|
30
|
+
|
31
|
+
def write_file!(filename, data)
|
32
|
+
File.write(File.join(@output_dir, filename), data)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module LetsencryptWebfaction
|
2
|
+
class DomainValidator
|
3
|
+
def initialize(domains, client, public_dir)
|
4
|
+
@domains = domains
|
5
|
+
@client = client
|
6
|
+
@public_dir = File.expand_path(public_dir)
|
7
|
+
end
|
8
|
+
|
9
|
+
def validate!
|
10
|
+
write_files!
|
11
|
+
|
12
|
+
challenges.each(&:request_verification)
|
13
|
+
|
14
|
+
i = 0
|
15
|
+
until all_challenges_valid? || i == 10
|
16
|
+
# Wait a bit for the server to make the request, or really just blink, it should be fast.
|
17
|
+
sleep(1)
|
18
|
+
|
19
|
+
i += 1
|
20
|
+
end
|
21
|
+
|
22
|
+
raise 'Failed to verify statuses in 10 seconds.' unless all_challenges_valid?
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def authorizations
|
28
|
+
@domains.map { |domain| @client.authorize(domain: domain) }
|
29
|
+
end
|
30
|
+
|
31
|
+
def challenges
|
32
|
+
@challenges ||= authorizations.map(&:http01)
|
33
|
+
end
|
34
|
+
|
35
|
+
def all_challenges_valid?
|
36
|
+
challenges.reject { |challenge| challenge.verify_status == 'valid' }.empty?
|
37
|
+
end
|
38
|
+
|
39
|
+
def write_files!
|
40
|
+
challenges.each do |challenge|
|
41
|
+
# Save the file. We'll create a public directory to serve it from, and we'll creating the challenge directory.
|
42
|
+
FileUtils.mkdir_p(File.join(@public_dir, File.dirname(challenge.filename)))
|
43
|
+
|
44
|
+
# Then writing the file
|
45
|
+
File.write(File.join(@public_dir, challenge.filename), challenge.file_content)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def delete_files!
|
50
|
+
# TODO
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module LetsencryptWebfaction
|
2
|
+
class Instructions
|
3
|
+
def initialize(output_dir, domains)
|
4
|
+
@output_dir = output_dir
|
5
|
+
@domains = domains
|
6
|
+
end
|
7
|
+
|
8
|
+
def message
|
9
|
+
'LetsEncrypt Webfaction has generated a new certificate for ' \
|
10
|
+
"#{to_sentence @domains}. The certificates have been placed in " \
|
11
|
+
"#{@output_dir}. You now need to request installation from the " \
|
12
|
+
"WebFaction support team.\n\n" \
|
13
|
+
'Go to https://help.webfaction.com, log in, and paste the ' \
|
14
|
+
"following text into a new ticket:\n\n" \
|
15
|
+
"Please apply the new certificate in #{@output_dir} to " \
|
16
|
+
"#{to_sentence @domains}. Thanks!"
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
# Borrowed (with simplifications) from ActiveSupport.
|
22
|
+
def to_sentence(str)
|
23
|
+
case str.length
|
24
|
+
when 0
|
25
|
+
''
|
26
|
+
when 1
|
27
|
+
str[0].to_s.dup
|
28
|
+
when 2
|
29
|
+
"#{str[0]} and #{str[1]}"
|
30
|
+
else
|
31
|
+
"#{str[0...-1].join(', ')}, and #{str[-1]}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
metadata
ADDED
@@ -0,0 +1,172 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: letsencrypt_webfaction
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- William Johnston
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-02-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: acme-client
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.3.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.3.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.11'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.11'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.4'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.4'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rubocop
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.37'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.37'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: simplecov
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0.11'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0.11'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: pry
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0.10'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0.10'
|
111
|
+
description: A tool to simplify the manual process of using LetsEncrypt on Webfaction
|
112
|
+
hosts. It can be added to cron where it will validate your domains automatically,
|
113
|
+
place the generated certs in a common folder, and then email you directions and
|
114
|
+
example text to send the WebFaction support team.
|
115
|
+
email:
|
116
|
+
- william@johnstonhaus.us
|
117
|
+
executables:
|
118
|
+
- letsencrypt_webfaction
|
119
|
+
extensions: []
|
120
|
+
extra_rdoc_files: []
|
121
|
+
files:
|
122
|
+
- ".gitignore"
|
123
|
+
- ".rspec"
|
124
|
+
- ".rubocop.yml"
|
125
|
+
- ".travis.yml"
|
126
|
+
- Gemfile
|
127
|
+
- README.md
|
128
|
+
- Rakefile
|
129
|
+
- bin/console
|
130
|
+
- bin/rake
|
131
|
+
- bin/rspec
|
132
|
+
- bin/rubocop
|
133
|
+
- bin/setup
|
134
|
+
- config.defaults.yml
|
135
|
+
- config.example.yml
|
136
|
+
- exe/letsencrypt_webfaction
|
137
|
+
- letsencrypt_webfaction.gemspec
|
138
|
+
- lib/letsencrypt_webfaction.rb
|
139
|
+
- lib/letsencrypt_webfaction/application.rb
|
140
|
+
- lib/letsencrypt_webfaction/args_parser.rb
|
141
|
+
- lib/letsencrypt_webfaction/args_parser/array_validator.rb
|
142
|
+
- lib/letsencrypt_webfaction/args_parser/defined_values_validator.rb
|
143
|
+
- lib/letsencrypt_webfaction/args_parser/field.rb
|
144
|
+
- lib/letsencrypt_webfaction/args_parser/string_validator.rb
|
145
|
+
- lib/letsencrypt_webfaction/certificate_writer.rb
|
146
|
+
- lib/letsencrypt_webfaction/domain_validator.rb
|
147
|
+
- lib/letsencrypt_webfaction/instructions.rb
|
148
|
+
homepage: https://github.com/will-in-wi/letsencrypt-webfaction
|
149
|
+
licenses:
|
150
|
+
- MIT
|
151
|
+
metadata: {}
|
152
|
+
post_install_message:
|
153
|
+
rdoc_options: []
|
154
|
+
require_paths:
|
155
|
+
- lib
|
156
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
157
|
+
requirements:
|
158
|
+
- - ">="
|
159
|
+
- !ruby/object:Gem::Version
|
160
|
+
version: 2.1.0
|
161
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
162
|
+
requirements:
|
163
|
+
- - ">="
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: '0'
|
166
|
+
requirements: []
|
167
|
+
rubyforge_project:
|
168
|
+
rubygems_version: 2.5.1
|
169
|
+
signing_key:
|
170
|
+
specification_version: 4
|
171
|
+
summary: LetsEncrypt utility client for WebFaction hosts.
|
172
|
+
test_files: []
|