lib/sym/application.rb
require 'colored2'
require 'sym'
require 'sym/app'
require 'openssl'
require 'json'
module Sym
# Main Application controller class for Sym.
#
# Accepts a hash with CLI options set (as symbols), for example
#
# Example
# =======
#
# app = Sym::Application.new( encrypt: true, file: '/tmp/secrets.yml', output: '/tmp/secrets.yml.enc')
# result = app.execute
#
#
class Application
attr_accessor :opts,
:opts_slop,
:args,
:action,
:key,
:key_source,
:input_handler,
:key_handler,
:output,
:result,
:password_cache,
:stdin, :stdout, :stderr, :kernel
def initialize(opts, stdin = $stdin, stdout = $stdout, stderr = $stderr, kernel = nil)
self.stdin = stdin
self.stdout = stdout
self.stderr = stderr
self.kernel = kernel
self.opts_slop = opts.clone
self.opts = opts.is_a?(Hash) ? opts : opts.to_hash
process_negated_option(opts[:negate]) if opts[:negate]
process_edit_option
self.args = ::Sym::App::Args.new(self.provided_options)
initialize_output_stream
initialize_action
initialize_data_source
initialize_password_cache
initialize_input_handler
end
# Main action method — it looksup the command, and executes it, translating
# various exception conditions into meaningful error messages.
def execute
process_output(execute!)
rescue ::OpenSSL::Cipher::CipherError => e
{ reason: 'Invalid key provided',
exception: e }
rescue Sym::Errors::Error => e
{ reason: e.class.name.gsub(/.*::/, '').underscore.humanize.downcase,
exception: e }
rescue TypeError => e
if e.message =~ /marshal/m
{ reason: 'Corrupt source data or invalid/corrupt key provided',
exception: e }
else
{ exception: e }
end
rescue StandardError => e
{ exception: e }
end
def command
@command_class ||= Sym::App::Commands.find_command_class(opts)
@command ||= @command_class.new(self) if @command_class
@command
end
def provided_flags
provided_flags = provided_options
provided_flags.delete_if { |k, v| ![false, true].include?(v) }
provided_flags.keys
end
def provided_value_options
provided = provided_options(safe: true)
provided.delete_if { |k, v| [false, true].include?(v) }
provided
end
def provided_options(**opts)
provided_opts = self.opts.clone
provided_opts.delete_if { |k, v| !v }
if opts[:safe]
provided_options.map do |k, v|
k == :key && [44, 45].include?(v.size) ?
[k, '[reducted]'] :
[k, v]
end.to_h
else
provided_opts
end
end
def editor
editors_to_try.compact.find { |editor| File.exist?(editor) }
end
def process_output(result)
self.output.call(result) unless result.is_a?(Hash)
result
end
private
def execute!
initialize_key_source
unless command
raise Sym::Errors::InsufficientOptionsError,
" Can not determine what to do
from the options: \ n " +
" #{self.provided_options.inspect.green.bold}\n" +
"and flags #{self.provided_flags.to_s.green.bold}"
end
log :info, "command located is #{command.class.name.blue.bold}"
self.result = command.execute.tap do |result|
log :info, "result is #{result.nil? ? 'nil' : result[0..10].to_s.blue.bold }..." if opts[:trace]
end
end
def log(*args)
Sym::App.log(*args, **opts)
end
def editors_to_try
[
ENV.fetch('EDITOR', nil),
'/usr/bin/vim',
'/usr/local/bin/vim',
'/bin/vim',
'/sbin/vim',
'/usr/sbin/vim',
'/usr/bin/vi',
'/usr/local/bin/vi',
'/bin/vi',
'/sbin/vi'
]
end
def initialize_output_stream
output_klass = args.output_class
unless output_klass && output_klass.is_a?(Class)
raise "Can not determine output type from arguments #{provided_options}"
end
self.output = output_klass.new(opts, stdin, stdout, stderr, kernel).output_proc
end
def initialize_input_handler(handler = ::Sym::App::Input::Handler.new(stdin, stdout, stderr, kernel))
self.input_handler = handler
end
def initialize_key_handler
self.key_handler = ::Sym::App::PrivateKey::Handler.new(opts, input_handler, password_cache)
end
def initialize_password_cache
args = {}
args[:timeout] = (opts[:cache_timeout] || ENV['SYM_CACHE_TTL'] || Sym::Configuration.config.password_cache_timeout).to_i
args[:enabled] = opts[:cache_passwords]
args[:verbose] = opts[:verbose]
args[:provider] = opts[:cache_provider] if opts[:cache_provider]
self.password_cache = Sym::App::Password::Cache.instance.configure(**args)
end
def process_edit_option
if opts[:edit] && opts[:edit].is_a?(String) && opts[:file].nil?
opts[:file] = opts[:edit]
opts[:edit] = true
end
end
def process_negated_option(file)
opts.delete(:negate)
opts[:file] = file
extension = Sym.config.encrypted_file_extension
if file.end_with?('.enc')
opts[:decrypt] = true
opts[:output] = file.gsub(/\.#{extension}/, '')
opts.delete(:output) if opts[:output] == ''
else
opts[:encrypt] = true
opts[:output] = "#{file}.#{extension}"
end
end
def initialize_action
self.action = if opts[:encrypt]
:encr
elsif opts[:decrypt]
:decr
end
end
# If we are encrypting or decrypting, and no data has been provided, check if we
# should read from STDIN
def initialize_data_source
if self.action && opts[:string].nil? && opts[:file].nil? && !self.stdin.tty?
opts[:file] = '-'
end
end
# If no key is provided with command line options, check the default
# key location (which can be changed via Configuration class).
# In any case, attempt to initialize the key one way or another.
def initialize_key_source
detect_key_source
if args.require_key? && !self.key
log :error, 'Unable to determine the key, which appears to be required with current args'
raise Sym::Errors::NoPrivateKeyFound, "Private key is required when #{self.action ? "#{self.action.to_s}ypting" : provided_flags.join(', ')}"
end
log :debug, "initialize_key_source: detected key ends with [...#{(key ? key[-5..] : 'nil').bold.magenta}]"
log :debug, "opts: #{self.provided_value_options.to_s.green.bold}"
log :debug, "flags: #{self.provided_flags.to_s.green.bold}"
end
def detect_key_source
initialize_key_handler
self.key = self.key_handler.key
if self.key
self.key_source = key_handler.key_source
if key_source =~ /^default_file/
opts[:key] = self.key
end
log :info, "key was detected from source #{key_source.to_s.bold.green}"
end
end
end
end