class LetsCert::Runner

Runner class: analyse and execute CLI commands. @author Sylvain Daubert rubocop:disable Metrics/ClassLength

Constants

ECDSA_DEFAULT_ACCOUNT_KEY_SIZE

Default account key size for ECDSA type

RETURN_ERROR

Exit value for error(s)

RETURN_OK

Exit value for OK

RETURN_OK_CERT

Exit value for OK but with creation/renewal of certificate data

RSA_DEFAULT_ACCOUNT_KEY_SIZE

Default account key size for RSA type

RSA_DEFAULT_KEY_SIZE

Default key size for RSA certificates

Attributes

logger[RW]

@return [Logger]

options[R]

Get options @return [Hash]

Public Class Methods

new() click to toggle source
# File lib/letscert/runner.rb, line 69
def initialize
  @options = {
    verbose: 0,
    domains: [],
    files: [],
    valid_min: ValidTime.new('30d'),
    account_key_type: 'rsa',
    tos_sha256: '33d233c8ab558ba6c8ebc370a509acdded8b80e5d587aa5d192193f3' \
                '5226540f',
    server: 'https://acme-v01.api.letsencrypt.org/directory'
  }

  @logger = Logger.new($stdout)
  @logger.formatter = LoggerFormatter.new
end
run() click to toggle source

Run LetsCert @return [Integer] @see run

# File lib/letscert/runner.rb, line 63
def self.run
  runner = new
  runner.parse_options
  runner.run
end

Public Instance Methods

check_persisted() click to toggle source

Check all components are covered by plugins @raise [Error]

# File lib/letscert/runner.rb, line 242
def check_persisted
  persisted = persisted_data
  not_persisted = persisted.keys.find_all { |k| persisted[k].nil? }

  unless not_persisted.empty?
    raise Error, 'Selected IO plugins do not cover following components: ' +
                 not_persisted.join(', ')
  end
end
parse_options() click to toggle source

Parse line command options @raise [OptionParser::InvalidOption] on unrecognized or malformed option @return [void] rubocop:disable Metrics/MethodLength rubocop:disable Metrics/BlockLength

# File lib/letscert/runner.rb, line 117
def parse_options
  @opt_parser = OptionParser.new do |opts|
    opts.banner = 'Usage: lestcert [options]'

    opts.separator('')

    opts.on('-h', '--help', 'Show this help message and exit') do
      @options[:print_help] = true
    end
    opts.on('-V', '--version', 'Show version and exit') do |v|
      @options[:show_version] = v
    end
    opts.on('-v', '--verbose', 'Run verbosely') do |v|
      @options[:verbose] += 1 if v
    end

    opts.separator("\nWebroot manager:")

    opts.on('-d', '--domain DOMAIN[:PATH]',
            'Domain name to include in the certificate.',
            'Must be specified at least once.',
            'Its path on the disk must also be provided.') do |domain|
      @options[:domains] << domain
    end

    opts.on('--default-root PATH', 'Default webroot path',
            'Use for domains without PATH part.') do |path|
      @options[:default_root] = path
    end

    opts.separator("\nCertificate data files:")

    opts.on('--revoke', 'Revoke existing certificates') do |revoke|
      @options[:revoke] = revoke
    end

    opts.on('-f', '--file FILE', 'Input/output file.',
            'Can be specified multiple times',
            'Allowed values: account_key.json, cert.der,',
            'cert.pem, chain.pem, full.pem,',
            'fullchain.pem, key.der, key.pem.') do |file|
      @options[:files] << file
    end

    opts.on('--cert-ecdsa CURVE', String,
            'Generate ECDSA certificate on CURVE') do |curve|
      @options[:cert_ecdsa] = curve
    end

    opts.on('--cert-rsa BITS', Integer,
            'Generate RSA certificate with a BITS-bit',
            'private key') do |bits|
      @options[:cert_rsa] = bits
    end
    opts.on('--cert-key-size BITS', Integer,
            'Certificate key size in bits',
            '(equivalent to --cert-rsa)',
            "(default: #{RSA_DEFAULT_KEY_SIZE})") do |bits|
      @options[:cert_rsa] = bits
    end

    opts.accept(ValidTime) do |valid_time|
      ValidTime.new(valid_time)
    end
    opts.on('--valid-min TIME', ValidTime,
            'Renew existing certificate if validity',
            'is lesser than TIME',
            "(default: #{@options[:valid_min]})") do |vt|
      @options[:valid_min] = vt
    end

    opts.on('--reuse-key', 'Reuse previous private key') do |rk|
      @options[:reuse_key] = rk
    end

    opts.separator("\nRegistration:")
    opts.separator('  Automatically register an account with he ACME CA' \
                   ' specified  by --server')
    opts.separator('')

    opts.on('--account-key-type TYPE', %w(rsa ecdsa),
            'Account key type: rsa or ecdsa',
            '(Defaul: rsa)') do |type|
      @options[:account_key_type] = type
    end

    opts.on('--account-key-size BITS', Integer,
            'Account key size (default: ' \
            "#{RSA_DEFAULT_ACCOUNT_KEY_SIZE} (RSA) or ",
            "#{ECDSA_DEFAULT_ACCOUNT_KEY_SIZE} (ECDSA))") do |bits|
      @options[:account_key_size] = bits
    end

    opts.on('--tos-sha256 HASH', String,
            'SHA-256 digest of the content of Terms',
            'Of Service URI') do |hash|
      @options[:tos_sha256] = hash
    end

    opts.on('--email EMAIL', String,
            'E-mail address. CA is likely to use it to',
            'remind about expiring certificates, as well',
            'as for account recovery. It is highly',
            'recommended to set this value.') do |email|
      @options[:email] = email
    end

    opts.separator("\nHTTP:")
    opts.separator('  Configure properties of HTTP requests and responses.')
    opts.separator('')

    opts.on('--server URI', 'URI for the CA ACME API endpoint',
            "(default: #{@options[:server]})") do |uri|
      @options[:server] = uri
    end
  end

  @opt_parser.parse!
  compute_roots
  select_default_cert_type_if_none_specified
  select_default_account_key_size_if_none_specified
end
run() click to toggle source

@return [Integer] exit code

* 0 if certificate data were created or updated
* 1 if renewal was not necessery
* 2 in case of errors
# File lib/letscert/runner.rb, line 89
def run
  print_help_if_needed
  show_version_if_needed
  set_logger_level
  set_logger

  begin
    check_domains
    if @options[:revoke]
      revoke
    else
      check_persisted
      get_certificate
    end
  rescue Error, Acme::Client::Error => ex
    msg = ex.message
    msg = "[Acme] #{msg}" if ex.is_a?(Acme::Client::Error)
    @logger.error msg
    $stderr.puts "Error: #{msg}"
    RETURN_ERROR
  end
end

Private Instance Methods

check_domains() click to toggle source

Check at least on domain is given. @return [void] @raise [Error] no domain given

# File lib/letscert/runner.rb, line 304
def check_domains
  if @options[:domains].empty?
    raise Error, 'At leat one domain must be given with --domain ' \
                 "option.\nTry 'letscert --help' for more information."
  end
end
compute_roots() click to toggle source

Compute webroots and set +@options+ @return [Hash] where keys are domains and value are their webroot path

# File lib/letscert/runner.rb, line 368
def compute_roots
  roots = {}

  @options[:domains].each do |domain|
    match = domain.match(/([-\w\.]+):(.*)/)
    if match
      roots[match[1]] = match[2]
    elsif @options[:default_root]
      roots[domain] = @options[:default_root]
    else
      roots[domain] = nil
    end
  end

  @options[:roots] = roots
end
get_certificate() click to toggle source

Create/update a certificate @return [Integer] exit status rubocop:disable Style/AccessorMethodName

# File lib/letscert/runner.rb, line 326
def get_certificate
  data = load_data_from_disk(@options[:files])

  certificate = Certificate.new(data[:cert])
  min_time = @options[:valid_min].to_seconds
  if certificate.valid?(@options[:domains], min_time)
    @logger.info { 'no need to update cert' }
    RETURN_OK
  else
    # update/create cert
    certificate.get data[:account_key], data[:key], @options
    RETURN_OK_CERT
  end
end
load_data_from_disk(files) click to toggle source

Load existing data from disk @param [Array<String>] files @return [Hash]

# File lib/letscert/runner.rb, line 344
def load_data_from_disk(files)
  all_data = IOPlugin.empty_data

  files.each do |plugin_name|
    persisted = IOPlugin.registered[plugin_name].persisted
    data = IOPlugin.registered[plugin_name].load

    test = IOPlugin.empty_data.keys.all? do |key|
      persisted[key] or data[key].nil?
    end
    raise Error unless test

    # Merge data into all_data. New value replace old one only if old
    # one was not defined
    all_data.merge!(data) do |_key, oldval, newval|
      oldval || newval
    end
  end

  all_data
end
persisted_data() click to toggle source
# File lib/letscert/runner.rb, line 400
def persisted_data
  persisted = IOPlugin.empty_data
  @options[:files].each do |file|
    ioplugin = IOPlugin.registered[file]
    next if ioplugin.nil?
    persisted.merge!(ioplugin.persisted) do |_k, oldv, newv|
      oldv || newv
    end
  end
  persisted
end
print_help_if_needed() click to toggle source

Print help and exit, if :print_help option is set @return [void] rubocop:disable Style/GuardClause

revoke() click to toggle source

Revoke a certificate @return [Integer] exit status

# File lib/letscert/runner.rb, line 313
def revoke
  data = load_data_from_disk(IOPlugin.registered.keys)
  certificate = Certificate.new(data[:cert])
  if certificate.revoke(data[:account_key], @options)
    RETURN_OK
  else
    RETURN_ERROR
  end
end
select_default_account_key_size_if_none_specified() click to toggle source
# File lib/letscert/runner.rb, line 391
def select_default_account_key_size_if_none_specified
  case @options[:account_key_type]
  when 'rsa'
    @options[:account_key_size] ||= RSA_DEFAULT_ACCOUNT_KEY_SIZE
  when 'ecdsa'
    @options[:account_key_size] ||= ECDSA_DEFAULT_ACCOUNT_KEY_SIZE
  end
end
select_default_cert_type_if_none_specified() click to toggle source
# File lib/letscert/runner.rb, line 385
def select_default_cert_type_if_none_specified
  if @options[:cert_ecdsa].nil? and @options[:cert_rsa].nil?
    @options[:cert_rsa] = RSA_DEFAULT_KEY_SIZE
  end
end
set_logger() click to toggle source

Set logger for IOPlugin and Certificate classes. @return [void]

# File lib/letscert/runner.rb, line 295
def set_logger
  @logger.debug { "options are: #{@options.inspect}" }
  IOPlugin.logger = @logger
  Certificate.logger = @logger
end
set_logger_level() click to toggle source

Set logger level from :verbose option @return [void]

# File lib/letscert/runner.rb, line 282
def set_logger_level
  case @options[:verbose]
  when 0
    @logger.level = Logger::Severity::WARN
  when 1
    @logger.level = Logger::Severity::INFO
  when 2..5
    @logger.level = Logger::Severity::DEBUG
  end
end
show_version() click to toggle source
# File lib/letscert/runner.rb, line 274
def show_version
  puts "letscert #{LetsCert::VERSION}"
  puts 'Copyright (c) 2016 Sylvain Daubert'
  puts 'License MIT: see http://opensource.org/licenses/MIT'
end
show_version_if_needed() click to toggle source

Show version and exit, if :show_version option is set @return [void]

# File lib/letscert/runner.rb, line 267
def show_version_if_needed
  if @options[:show_version]
    show_version
    exit RETURN_OK
  end
end