Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/lib/rex/ui/text/dispatcher_shell.rb
Views: 11655
# -*- coding: binary -*-1require 'pp'2require 'rex/text/table'3require 'erb'45module Rex6module Ui7module Text89###10#11# The dispatcher shell class is designed to provide a generic means12# of processing various shell commands that may be located in13# different modules or chunks of codes. These chunks are referred14# to as command dispatchers. The only requirement for command dispatchers is15# that they prefix every method that they wish to be mirrored as a command16# with the cmd_ prefix.17#18###19module DispatcherShell2021include Resource2223###24#25# Empty template base class for command dispatchers.26#27###28module CommandDispatcher2930module ClassMethods31#32# Check whether or not the command dispatcher is capable of handling the33# specified command. The command may still be disabled through some means34# at runtime.35#36# @param [String] name The name of the command to check.37# @return [Boolean] true if the dispatcher can handle the command.38def has_command?(name)39self.method_defined?("cmd_#{name}")40end4142def included(base)43# Propagate the included hook44CommandDispatcher.included(base)45end46end4748def self.included(base)49# Install class methods so they are inheritable50base.extend(ClassMethods)51end5253#54# Initializes the command dispatcher mixin.55#56def initialize(shell)57self.shell = shell58self.tab_complete_items = []59end6061#62# Returns nil for an empty set of commands.63#64# This method should be overridden to return a Hash with command65# names for keys and brief help text for values.66#67def commands68end6970#71# Returns an empty set of commands.72#73# This method should be overridden if the dispatcher has commands that74# should be treated as deprecated. Deprecated commands will not show up in75# help and will not tab-complete, but will still be callable.76#77def deprecated_commands78[]79end8081#82# Wraps shell.print_error83#84def print_error(msg = '')85shell.print_error(msg)86end8788alias_method :print_bad, :print_error8990#91# Wraps shell.print_status92#93def print_status(msg = '')94shell.print_status(msg)95end9697#98# Wraps shell.print_line99#100def print_line(msg = '')101shell.print_line(msg)102end103104#105# Wraps shell.print_good106#107def print_good(msg = '')108shell.print_good(msg)109end110111#112# Wraps shell.print_warning113#114def print_warning(msg = '')115shell.print_warning(msg)116end117118#119# Wraps shell.print120#121def print(msg = '')122shell.print(msg)123end124125#126# Print a warning that the called command is deprecated and optionally127# forward to the replacement +method+ (useful for when commands are128# renamed).129#130def deprecated_cmd(method=nil, *args)131cmd = caller[0].match(/`cmd_(.*)'/)[1]132print_error "The #{cmd} command is DEPRECATED"133if cmd == "db_autopwn"134print_error "See http://r-7.co/xY65Zr instead"135elsif method and self.respond_to?("cmd_#{method}", true)136print_error "Use #{method} instead"137self.send("cmd_#{method}", *args)138end139end140141def deprecated_help(method=nil)142cmd = caller[0].match(/`cmd_(.*)_help'/)[1]143print_error "The #{cmd} command is DEPRECATED"144if cmd == "db_autopwn"145print_error "See http://r-7.co/xY65Zr instead"146elsif method and self.respond_to?("cmd_#{method}_help", true)147print_error "Use 'help #{method}' instead"148self.send("cmd_#{method}_help")149end150end151152#153# Wraps shell.update_prompt154#155def update_prompt(*args)156shell.update_prompt(*args)157end158159def cmd_help_help160print_line "There's only so much I can do"161end162163#164# Displays the help banner. With no arguments, this is just a list of165# all commands grouped by dispatcher. Otherwise, tries to use a method166# named cmd_#{+cmd+}_help for the first dispatcher that has a command167# named +cmd+. If no such method exists, uses +cmd+ as a regex to168# compare against each enstacked dispatcher's name and dumps commands169# of any that match.170#171def cmd_help(cmd=nil, *ignored)172if cmd173help_found = false174cmd_found = false175shell.dispatcher_stack.each do |dispatcher|176next unless dispatcher.respond_to?(:commands)177next if (dispatcher.commands.nil?)178next if (dispatcher.commands.length == 0)179180if dispatcher.respond_to?("cmd_#{cmd}", true)181cmd_found = true182break unless dispatcher.respond_to?("cmd_#{cmd}_help", true)183dispatcher.send("cmd_#{cmd}_help")184help_found = true185break186end187end188189unless cmd_found190# We didn't find a cmd, try it as a dispatcher name191shell.dispatcher_stack.each do |dispatcher|192if dispatcher.name =~ /#{cmd}/i193print_line(dispatcher.help_to_s)194cmd_found = help_found = true195end196end197end198199if docs_dir && File.exist?(File.join(docs_dir, cmd + '.md'))200print_line201print(File.read(File.join(docs_dir, cmd + '.md')))202end203print_error("No help for #{cmd}, try -h") if cmd_found and not help_found204print_error("No such command") if not cmd_found205else206print(shell.help_to_s)207if docs_dir && File.exist?(File.join(docs_dir + '.md'))208print_line209print(File.read(File.join(docs_dir + '.md')))210end211end212end213214#215# Tab completion for the help command216#217# By default just returns a list of all commands in all dispatchers.218#219def cmd_help_tabs(str, words)220return [] if words.length > 1221222tabs = []223shell.dispatcher_stack.each { |dispatcher|224tabs += dispatcher.commands.keys225}226return tabs227end228229alias cmd_? cmd_help230231#232# Return a pretty, user-readable table of commands provided by this233# dispatcher.234# The command column width can be modified by passing in :command_width.235#236def help_to_s(opts={})237# If this dispatcher has no commands, we can't do anything useful.238return "" if commands.nil? or commands.length == 0239240# Display the commands241tbl = Rex::Text::Table.new(242'Header' => "#{self.name} Commands",243'Indent' => opts['Indent'] || 4,244'Columns' =>245[246'Command',247'Description'248],249'ColProps' =>250{251'Command' =>252{253'Width' => opts[:command_width]254}255})256257commands.sort.each { |c|258tbl << c259}260261return "\n" + tbl.to_s + "\n"262end263264#265# Return the subdir of the `documentation/` directory that should be used266# to find usage documentation267#268# TODO: get this value from somewhere that doesn't invert a bunch of269# dependencies270#271def docs_dir272File.expand_path(File.join(__FILE__, '..', '..', '..', '..', '..', 'documentation', 'cli'))273end274275#276# No tab completion items by default277#278attr_accessor :shell, :tab_complete_items279280#281# Provide a generic tab completion for file names.282#283# If the only completion is a directory, this descends into that directory284# and continues completions with filenames contained within.285#286def tab_complete_filenames(str, words)287matches = ::Readline::FILENAME_COMPLETION_PROC.call(str)288if matches and matches.length == 1 and File.directory?(matches[0])289dir = matches[0]290dir += File::SEPARATOR if dir[-1,1] != File::SEPARATOR291matches = ::Readline::FILENAME_COMPLETION_PROC.call(dir)292end293matches.nil? ? [] : matches294end295296#297# Return a list of possible directory for tab completion.298#299def tab_complete_directory(str, words)300directory = str[-1] == File::SEPARATOR ? str : File.dirname(str)301filename = str[-1] == File::SEPARATOR ? '' : File.basename(str)302entries = Dir.entries(directory).select { |fp| fp.start_with?(filename) }303dirs = entries - ['.', '..']304dirs = dirs.map { |fp| File.join(directory, fp).gsub(/\A\.\//, '') }305dirs = dirs.select { |x| File.directory?(x) }306dirs = dirs.map { |x| x + File::SEPARATOR }307if dirs.length == 1 && dirs[0] != str && dirs[0].end_with?(File::SEPARATOR)308# If Readline receives a single value from this function, it will assume we're done with the tab309# completing, and add an extra space at the end.310# This is annoying if we're recursively tab-traversing our way through subdirectories -311# we may want to continue traversing, but MSF will add a space, requiring us to back up to continue312# tab-completing our way through successive subdirectories.313::Readline.completion_append_character = nil314end315316if dirs.length == 0 && File.directory?(str)317# we've hit the end of the road318dirs = [str]319end320321dirs322end323324#325# Provide a generic tab completion function based on the specification326# pass as fmt. The fmt argument in a hash where values are an array327# defining how the command should be completed. The first element of the328# array can be one of:329# nil - This argument is a flag and takes no option.330# true - This argument takes an option with no suggestions.331# :address - This option is a source address.332# :bool - This option is a boolean.333# :file - This option is a file path.334# Array - This option is an array of possible values.335#336def tab_complete_generic(fmt, str, words)337last_word = words[-1]338fmt = fmt.select { |key, value| last_word == key || !words.include?(key) }339340val = fmt[last_word]341return fmt.keys if !val # the last word does not look like a fmtspec342arg = val[0]343return fmt.keys if !arg # the last word is a fmtspec that takes no argument344345tabs = []346if arg.to_s.to_sym == :address347tabs = tab_complete_source_address348elsif arg.to_s.to_sym == :bool349tabs = ['true', 'false']350elsif arg.to_s.to_sym == :file351tabs = tab_complete_filenames(str, words)352elsif arg.kind_of?(Array)353tabs = arg.map {|a| a.to_s}354end355tabs356end357358#359# Return a list of possible source addresses for tab completion.360#361def tab_complete_source_address362addresses = [Rex::Socket.source_address]363# getifaddrs was introduced in 2.1.2364if ::Socket.respond_to?(:getifaddrs)365ifaddrs = ::Socket.getifaddrs.select do |ifaddr|366ifaddr.addr && ifaddr.addr.ip?367end368addresses += ifaddrs.map { |ifaddr| ifaddr.addr.ip_address }369end370addresses371end372373#374# A callback that can be used to handle unknown commands. This can for example, allow a dispatcher to mark a command375# as being disabled.376#377# @return [Symbol, nil] Returns a symbol specifying the action that was taken by the handler or `nil` if no action378# was taken. The only supported action at this time is `:handled`, signifying that the unknown command was handled379# by this dispatcher and no additional dispatchers should receive it.380def unknown_command(method, line)381nil382end383end384385#386# DispatcherShell derives from shell.387#388include Shell389390#391# Initialize the dispatcher shell.392#393def initialize(prompt, prompt_char = '>', histfile = nil, framework = nil, name = nil)394super395396# Initialize the dispatcher array397self.dispatcher_stack = []398399# Initialize the tab completion array400self.on_command_proc = nil401end402403#404# This method accepts the entire line of text from the Readline405# routine, stores all completed words, and passes the partial406# word to the real tab completion function. This works around407# a design problem in the Readline module and depends on the408# Readline.basic_word_break_characters variable being set to \x00409#410def tab_complete(str)411::Readline.completion_append_character = ' '412::Readline.completion_case_fold = false413414# Check trailing whitespace so we can tell 'x' from 'x '415str_match = str.match(/[^\\]([\\]{2})*\s+$/)416str_trail = (str_match.nil?) ? '' : str_match[0]417418# Split the line up by whitespace into words419split_str = shellsplitex(str)420421# Append an empty token if we had trailing whitespace422split_str[:tokens] << { begin: str.length, value: '' } if str_trail.length > 0423424# Pop the last word and pass it to the real method425result = tab_complete_stub(str, split_str)426if result427result.uniq428else429result430end431end432433# Performs tab completion of a command, if supported434#435def tab_complete_stub(original_str, split_str)436*preceding_tokens, current_token = split_str[:tokens]437return nil unless current_token438439items = []440current_word = current_token[:value]441tab_words = preceding_tokens.map { |word| word[:value] }442443# Next, try to match internal command or value completion444# Enumerate each entry in the dispatcher stack445dispatcher_stack.each do |dispatcher|446447# If no command is set and it supports commands, add them all448if tab_words.empty? and dispatcher.respond_to?('commands')449items.concat(dispatcher.commands.keys)450end451452# If the dispatcher exports a tab completion function, use it453if dispatcher.respond_to?('tab_complete_helper')454res = dispatcher.tab_complete_helper(current_word, tab_words)455else456res = tab_complete_helper(dispatcher, current_word, tab_words)457end458459if res.nil?460# A nil response indicates no optional arguments461return [''] if items.empty?462else463if res.second == :override_completions464return res.first465else466# Otherwise we add the completion items to the list467items.concat(res)468end469end470end471472# Match based on the partial word473matches = items.select do |word|474word.downcase.start_with?(current_word.downcase)475end476477# Prepend the preceding string of the command (or it all gets replaced!)478preceding_str = original_str[0...current_token[:begin]]479quote = current_token[:quote]480matches_with_preceding_words_appended = matches.map do |word|481word = quote.nil? ? word.gsub('\\') { '\\\\' }.gsub(' ', '\\ ') : "#{quote}#{word}#{quote}"482preceding_str + word483end484485matches_with_preceding_words_appended486end487488#489# Provide command-specific tab completion490#491def tab_complete_helper(dispatcher, str, words)492tabs_meth = "cmd_#{words[0]}_tabs"493# Is the user trying to tab complete one of our commands?494if dispatcher.commands.include?(words[0]) and dispatcher.respond_to?(tabs_meth)495res = dispatcher.send(tabs_meth, str, words)496return [] if res.nil?497return res498end499500# Avoid the default completion list for unknown commands501[]502end503504#505# Run a single command line.506#507# @param [String] line The command string that should be executed.508# @param [Boolean] propagate_errors Whether or not to raise exceptions that are caught while executing the command.509#510# @return [Boolean] A boolean value signifying whether or not the command was handled. Value is `true` when the511# command line was handled.512def run_single(line, propagate_errors: false)513arguments = parse_line(line)514method = arguments.shift515cmd_status = nil # currently either nil or :handled, more statuses can be added in the future516error = false517518# If output is disabled output will be nil519output.reset_color if (output)520521if (method)522entries = dispatcher_stack.length523524dispatcher_stack.each { |dispatcher|525next if not dispatcher.respond_to?('commands')526527begin528if (dispatcher.commands.has_key?(method) or dispatcher.deprecated_commands.include?(method))529self.on_command_proc.call(line.strip) if self.on_command_proc530run_command(dispatcher, method, arguments)531cmd_status = :handled532elsif cmd_status.nil?533cmd_status = dispatcher.unknown_command(method, line)534end535rescue ::Interrupt536cmd_status = :handled537print_error("#{method}: Interrupted")538raise if propagate_errors539rescue OptionParser::ParseError => e540print_error("#{method}: #{e.message}")541raise if propagate_errors542rescue543error = $!544545print_error(546"Error while running command #{method}: #{$!}" +547"\n\nCall stack:\n#{$@.join("\n")}")548549raise if propagate_errors550rescue ::Exception => e551error = $!552553print_error(554"Error while running command #{method}: #{$!}")555556raise if propagate_errors557end558559# If the dispatcher stack changed as a result of this command,560# break out561break if (dispatcher_stack.length != entries)562}563564if (cmd_status.nil? && error == false)565unknown_command(method, line)566end567end568569return cmd_status == :handled570end571572#573# Runs the supplied command on the given dispatcher.574#575def run_command(dispatcher, method, arguments)576self.busy = true577578if(blocked_command?(method))579print_error("The #{method} command has been disabled.")580else581dispatcher.send('cmd_' + method, *arguments)582end583ensure584self.busy = false585end586587#588# If the command is unknown...589#590def unknown_command(method, line)591# Map each dispatchers commands to valid_commands592valid_commands = dispatcher_stack.flat_map { |dispatcher| dispatcher.commands.keys }593594message = "Unknown command: #{method}."595suggestion = DidYouMean::SpellChecker.new(dictionary: valid_commands).correct(method).first596message << " Did you mean %grn#{suggestion}%clr?" if suggestion597message << ' Run the %grnhelp%clr command for more details.'598599print_error(message)600end601602#603# Push a dispatcher to the front of the stack.604#605def enstack_dispatcher(dispatcher)606self.dispatcher_stack.unshift(inst = dispatcher.new(self))607608inst609end610611#612# Pop a dispatcher from the front of the stacker.613#614def destack_dispatcher615self.dispatcher_stack.shift616end617618#619# Adds the supplied dispatcher to the end of the dispatcher stack so that620# it doesn't affect any enstack'd dispatchers.621#622def append_dispatcher(dispatcher)623inst = dispatcher.new(self)624self.dispatcher_stack.each { |disp|625if (disp.name == inst.name)626raise "Attempting to load already loaded dispatcher #{disp.name}"627end628}629self.dispatcher_stack.push(inst)630631inst632end633634#635# Removes the supplied dispatcher instance.636#637def remove_dispatcher(name)638self.dispatcher_stack.delete_if { |inst|639(inst.name == name)640}641end642643#644# Returns the current active dispatcher645#646def current_dispatcher647self.dispatcher_stack[0]648end649650#651# Return a readable version of a help banner for all of the enstacked652# dispatchers.653#654# See +CommandDispatcher#help_to_s+655#656def help_to_s(opts = {})657str = ''658659max_command_length = dispatcher_stack.flat_map { |dispatcher| dispatcher.commands.to_a }.map { |(name, _description)| name.length }.max660661dispatcher_stack.reverse.each { |dispatcher|662str << dispatcher.help_to_s(opts.merge({ command_width: [max_command_length, 12].max }))663}664665return str << "For more info on a specific command, use %grn<command> -h%clr or %grnhelp <command>%clr.\n\n"666end667668669#670# Returns nil for an empty set of blocked commands.671#672def blocked_command?(cmd)673return false if not self.blocked674self.blocked.has_key?(cmd)675end676677#678# Block a specific command679#680def block_command(cmd)681self.blocked ||= {}682self.blocked[cmd] = true683end684685#686# Unblock a specific command687#688def unblock_command(cmd)689self.blocked || return690self.blocked.delete(cmd)691end692693#694# Split a line as Shellwords.split would however instead of raising an695# ArgumentError on unbalanced quotes return the remainder of the string as if696# the last character were the closing quote.697#698# This code was originally taken from https://github.com/ruby/ruby/blob/93420d34aaf8c30f11a66dd08eb186da922c831d/lib/shellwords.rb#L88699#700def shellsplitex(line)701tokens = []702field_value = String.new703field_begin = nil704705line.scan(/\G(\s*)(?>([^\s\\\'\"]+)|'([^\']*)'|"((?:[^\"\\]|\\.)*)"|(\\.?)|(\S))(\s|\z)?/m) do |preceding_whitespace, word, sq, dq, esc, garbage, sep|706field_begin ||= Regexp.last_match.begin(0) + preceding_whitespace.length707if garbage708quote_start_begin = Regexp.last_match.begin(0) + preceding_whitespace.length709field_quote = garbage710field_value << line[quote_start_begin + 1..-1].gsub('\\\\', '\\')711712tokens << { begin: field_begin, value: field_value, quote: field_quote }713break714end715716field_value << (word || sq || (dq && dq.gsub(/\\([$`"\\\n])/, '\\1')) || esc.gsub(/\\(.)/, '\\1'))717if sep718tokens << { begin: field_begin, value: field_value, quote: ((sq && "'") || (dq && '"') || nil) }719field_value = String.new720field_begin = nil721end722end723724{ tokens: tokens }725end726727attr_accessor :dispatcher_stack # :nodoc:728attr_accessor :busy # :nodoc:729attr_accessor :blocked # :nodoc:730731end732733end734end735end736737738