diff --git a/Gemfile.lock b/Gemfile.lock index 1ffd86486a53e..c5240e7e0a7d9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -405,7 +405,7 @@ GEM nokogiri redcarpet (3.6.0) regexp_parser (2.9.2) - reline (0.5.10) + reline (0.5.11) io-console (~> 0.5) require_all (3.0.0) rex-arch (0.1.16) diff --git a/lib/msf/base/sessions/command_shell.rb b/lib/msf/base/sessions/command_shell.rb index bb9b04472aa74..d2a7bda503e66 100644 --- a/lib/msf/base/sessions/command_shell.rb +++ b/lib/msf/base/sessions/command_shell.rb @@ -547,7 +547,7 @@ def cmd_irb(*args) if expressions.empty? print_status('Starting IRB shell...') print_status("You are in the \"self\" (session) object\n") - framework.history_manager.with_context(name: :irb) do + framework.history_manager.with_context(name: :irb, input_library: :reline) do Rex::Ui::Text::IrbShell.new(self).run end else @@ -586,7 +586,7 @@ def cmd_pry(*args) print_status('Starting Pry shell...') print_status("You are in the \"self\" (session) object\n") Pry.config.history_load = false - framework.history_manager.with_context(history_file: Msf::Config.pry_history, name: :pry) do + framework.history_manager.with_context(history_file: Msf::Config.pry_history, name: :pry, input_library: Pry.input) do self.pry end end diff --git a/lib/msf/core/feature_manager.rb b/lib/msf/core/feature_manager.rb index 872087c350493..c40944ccf301c 100644 --- a/lib/msf/core/feature_manager.rb +++ b/lib/msf/core/feature_manager.rb @@ -29,6 +29,7 @@ class FeatureManager LDAP_SESSION_TYPE = 'ldap_session_type' SHOW_SUCCESSFUL_LOGINS = 'show_successful_logins' DISPLAY_MODULE_ACTION = 'display_module_action' + USE_RELINE = 'use_reline' DEFAULTS = [ { @@ -132,6 +133,13 @@ class FeatureManager requires_restart: false, default_value: true, developer_notes: 'Added as a feature so users can turn it off if they wish to reduce clutter in their terminal' + }.freeze, + { + name: USE_RELINE, + description: 'When enabled, the new Reline library will be used instead of the legacy Readline library for input/output.', + requires_restart: true, + default_value: false, + developer_notes: 'To be enabled by default after sufficient testing and Reline fixes the issues raised here: https://github.com/ruby/reline/issues/created_by/sjanusz-r7' }.freeze ].freeze diff --git a/lib/msf/ui/console/command_dispatcher/core.rb b/lib/msf/ui/console/command_dispatcher/core.rb index 5599879afad6a..d7399ac6ee73b 100644 --- a/lib/msf/ui/console/command_dispatcher/core.rb +++ b/lib/msf/ui/console/command_dispatcher/core.rb @@ -760,7 +760,8 @@ def cmd_features_tabs(_str, words) end def cmd_history(*args) - length = Readline::HISTORY.length + history = Msf::Ui::Console::MsfReadline.instance.history + length = history.length if length < @history_limit limit = length @@ -780,10 +781,10 @@ def cmd_history(*args) limit = val.to_i end when '-c' - if Readline::HISTORY.respond_to?(:clear) - Readline::HISTORY.clear - elsif defined?(RbReadline) - RbReadline.clear_history + if history.respond_to?(:clear) + history.clear + elsif history.respond_to?(:pop) && history.respond_to?(:length) + history.length.times { |_i| history.pop } else print_error('Could not clear history, skipping file') return false @@ -808,7 +809,7 @@ def cmd_history(*args) (start..length-1).each do |pos| cmd_num = (pos + 1).to_s - print_line "#{cmd_num.ljust(pad_len)} #{Readline::HISTORY[pos]}" + print_line "#{cmd_num.ljust(pad_len)} #{history[pos]}" end end diff --git a/lib/msf/ui/console/command_dispatcher/developer.rb b/lib/msf/ui/console/command_dispatcher/developer.rb index 33c2c39a3f1f6..3fbc0c3e15fec 100644 --- a/lib/msf/ui/console/command_dispatcher/developer.rb +++ b/lib/msf/ui/console/command_dispatcher/developer.rb @@ -129,8 +129,15 @@ def cmd_irb(*args) if expressions.empty? print_status('Starting IRB shell...') - framework.history_manager.with_context(name: :irb) do + framework.history_manager.with_context(name: :irb, input_library: :reline) do begin + if Msf::Ui::Console::MsfReadline.instance.using_reline? + config = {} + config[:autocomplete] = Msf::Ui::Console::MsfReadline.instance.autocompletion + # IRB changes propagate out of the context of IRB. We store the current state and restore it on exit. + # TODO: Once IRB fixes this behaviour, we should be able to remove this patch. + config[:core] = Msf::Ui::Console::MsfReadline.instance.backend.core.dup + end if active_module print_status("You are in #{active_module.fullname}\n") Rex::Ui::Text::IrbShell.new(active_module).run @@ -140,6 +147,11 @@ def cmd_irb(*args) end rescue print_error("Error during IRB: #{$!}\n\n#{$@.join("\n")}") + ensure + if Msf::Ui::Console::MsfReadline.instance.using_reline? + Msf::Ui::Console::MsfReadline.instance.backend.instance_variable_set(:@core, config[:core]) + Msf::Ui::Console::MsfReadline.instance.autocompletion = config[:autocomplete] + end end end @@ -192,7 +204,7 @@ def cmd_pry(*args) print_status('Starting Pry shell...') Pry.config.history_load = false - framework.history_manager.with_context(history_file: Msf::Config.pry_history, name: :pry) do + framework.history_manager.with_context(history_file: Msf::Config.pry_history, name: :pry, input_library: Pry.input) do if active_module print_status("You are in the \"#{active_module.fullname}\" module object\n") active_module.pry @@ -515,6 +527,10 @@ def cmd_time_help private def modified_files + # Temporary work-around until Open3 gets fixed on Windows 11: + # https://github.com/ruby/open3/issues/9 + return [] if Rex::Compat.is_cygwin || Rex::Compat.is_windows + # Using an array avoids shelling out, so we avoid escaping/quoting changed_files = %w[git diff --name-only] begin diff --git a/lib/msf/ui/console/command_dispatcher/session.rb b/lib/msf/ui/console/command_dispatcher/session.rb index de791bdea29d7..9c59e7fc5b02a 100644 --- a/lib/msf/ui/console/command_dispatcher/session.rb +++ b/lib/msf/ui/console/command_dispatcher/session.rb @@ -96,7 +96,7 @@ def cmd_irb(*args) if expressions.empty? print_status('Starting IRB shell...') print_status("You are in the session object\n") - framework.history_manager.with_context(name: :irb) do + framework.history_manager.with_context(name: :irb, input_library: :reline) do Rex::Ui::Text::IrbShell.new(session).run end else @@ -136,7 +136,7 @@ def cmd_pry(*args) print_status("You are in the session object\n") Pry.config.history_load = false - session.framework.history_manager.with_context(history_file: Msf::Config.pry_history, name: :pry) do + session.framework.history_manager.with_context(history_file: Msf::Config.pry_history, name: :pry, input_library: Pry.input) do session.pry end end diff --git a/lib/msf/ui/console/driver.rb b/lib/msf/ui/console/driver.rb index bc42cb1a4cb99..f256fcb633192 100644 --- a/lib/msf/ui/console/driver.rb +++ b/lib/msf/ui/console/driver.rb @@ -313,12 +313,9 @@ def save_config # Saves the recent history to the specified file # def save_recent_history(path) - num = Readline::HISTORY.length - hist_last_saved - 1 - - tmprc = "" - num.times { |x| - tmprc << Readline::HISTORY[hist_last_saved + x] + "\n" - } + history = Msf::Ui::Console::MsfReadline.instance.history + num = history.length - hist_last_saved - 1 + tmprc = history.entries[hist_last_saved..].join("\n") if tmprc.length > 0 print_status("Saving last #{num} commands to #{path} ...") @@ -329,7 +326,7 @@ def save_recent_history(path) # Always update this, even if we didn't save anything. We do this # so that we don't end up saving the "makerc" command itself. - self.hist_last_saved = Readline::HISTORY.length + self.hist_last_saved = history.length end # diff --git a/lib/msf/ui/console/module_option_tab_completion.rb b/lib/msf/ui/console/module_option_tab_completion.rb index f8ecd07216e3d..919a5caa7a832 100644 --- a/lib/msf/ui/console/module_option_tab_completion.rb +++ b/lib/msf/ui/console/module_option_tab_completion.rb @@ -53,18 +53,18 @@ def tab_complete_option(mod, str, words) option_name = str.chop option_value = '' - ::Readline.completion_append_character = ' ' + Msf::Ui::Console::MsfReadline.instance.completion_append_character = ' ' return tab_complete_option_values(mod, option_value, words, opt: option_name).map { |value| "#{str}#{value}" } elsif str.include?('=') str_split = str.split('=') option_name = str_split[0].strip option_value = str_split[1].strip - ::Readline.completion_append_character = ' ' + Msf::Ui::Console::MsfReadline.instance.completion_append_character = ' ' return tab_complete_option_values(mod, option_value, words, opt: option_name).map { |value| "#{option_name}=#{value}" } end - ::Readline.completion_append_character = '' + Msf::Ui::Console::MsfReadline.instance.completion_append_character = '' tab_complete_option_names(mod, str, words).map { |name| "#{name}=" } end diff --git a/lib/msf/ui/console/msf_readline.rb b/lib/msf/ui/console/msf_readline.rb new file mode 100644 index 0000000000000..520af9dff1712 --- /dev/null +++ b/lib/msf/ui/console/msf_readline.rb @@ -0,0 +1,117 @@ +# +# This class is responsible for handling Readline/Reline-agnostic user input. +# +class Msf::Ui::Console::MsfReadline + require 'singleton' + # Required to check the Reline flag. + require 'msf/core/feature_manager' + require 'readline' + require 'reline' + + include Singleton + + attr_reader :history + # This is currently required for a Reline + IRB workaround, where changes made by IRB to the Reline Face and LineEditor + # persist after exiting IRB. + # TODO: We can remove this once IRB fixes the config escaping the IRB context + attr_reader :backend + + def initialize + @backend = using_reline? ? ::Reline : ::Readline + @history = @backend::HISTORY + end + + def method_missing(sym, *args, &block) + if @backend.respond_to?(sym) + @backend.send(sym, *args, &block) + else + msg = "Method '#{sym}' not found in #{@backend.class}" + elog(msg) + raise NoMethodError, msg + end + end + + # Read a line from the user, and return it. + # @param prompt [String] The prompt to show to the user. + # @param add_history [Boolean] True if the user's input should be saved to the history. + # @param opts [Hash] Options + # @return String The line that the user has entered. + def readline(prompt, add_history = false, opts: {}) + input(prompt, add_history, opts: opts) + end + + def using_reline? + return @using_reline unless @using_reline.nil? + + @using_reline = Msf::FeatureManager.instance.enabled?(Msf::FeatureManager::USE_RELINE) + end + + private + + attr_accessor :using_reline + attr_writer :backend + + def input(*args, opts: {}) + using_reline? ? input_reline(*args, opts: opts) : input_rbreadline(*args, opts: opts) + end + + def input_rbreadline(prompt, add_history = false, opts: {}) + # rb-readlines's Readline.readline hardcodes the input and output to + # $stdin and $stdout, which means setting `Readline.input` or + # `Readline.output` has no effect when running `Readline.readline` with + # rb-readline, so need to reimplement + # []`Readline.readline`](https://github.com/luislavena/rb-readline/blob/ce4908dae45dbcae90a6e42e3710b8c3a1f2cd64/lib/readline.rb#L36-L58) + # for rb-readline to support setting input and output. Output needs to + # be set so that colorization works for the prompt on Windows. + + input_on_entry = RbReadline.rl_instream + output_on_entry = RbReadline.rl_outstream + + begin + RbReadline.rl_instream = opts[:fd] + RbReadline.rl_outstream = opts[:output] + line = RbReadline.readline(prompt.to_s) + rescue ::StandardError => e + RbReadline.rl_instream = input_on_entry + RbReadline.rl_outstream = output_on_entry + RbReadline.rl_cleanup_after_signal + RbReadline.rl_deprep_terminal + + raise e + end + + if add_history && line && !line.start_with?(' ') + # Don't add duplicate lines to history + if ::Readline::HISTORY.empty? || line.strip != ::Readline::HISTORY[-1] + RbReadline.add_history(line.strip) + end + end + + line.dup + end + + def input_reline(prompt, add_history = false, opts: {}) + input_on_entry = Reline::IOGate.instance_variable_get(:@input) + output_on_entry = Reline::IOGate.instance_variable_get(:@output) + + begin + input_external_encoding = opts[:fd].external_encoding + input_internal_encoding = opts[:fd].internal_encoding + opts[:fd].set_encoding(::Encoding::UTF_8) + Reline.input = opts[:fd] + Reline.output = opts[:output] + line = Reline.readline(prompt.to_s, add_history) + ensure + opts[:fd].set_encoding(input_external_encoding, input_internal_encoding) + Reline.input = input_on_entry + Reline.output = output_on_entry + end + + # Don't add duplicate lines to history + if Reline::HISTORY.length > 1 && line == Reline::HISTORY[-2] + Reline::HISTORY.pop + end + + line.dup + end +end diff --git a/lib/msf/ui/debug.rb b/lib/msf/ui/debug.rb index 6bf8f8e3b1385..42067acff3457 100644 --- a/lib/msf/ui/debug.rb +++ b/lib/msf/ui/debug.rb @@ -220,13 +220,14 @@ def self.framework_config(framework) end def self.history(driver) - end_pos = Readline::HISTORY.length - 1 + history = Msf::Ui::Console::MsfReadline.instance.history + end_pos = history.length - 1 start_pos = end_pos - COMMAND_HISTORY_TOTAL > driver.hist_last_saved ? end_pos - (COMMAND_HISTORY_TOTAL - 1) : driver.hist_last_saved commands = '' while start_pos <= end_pos # Formats command position in history to 6 characters in length - commands += "#{'%-6.6s' % start_pos.to_s} #{Readline::HISTORY[start_pos]}\n" + commands += "#{'%-6.6s' % start_pos.to_s} #{history[start_pos]}\n" start_pos += 1 end diff --git a/lib/msf_autoload.rb b/lib/msf_autoload.rb index c546aa8dc1957..83e652c183be9 100644 --- a/lib/msf_autoload.rb +++ b/lib/msf_autoload.rb @@ -347,3 +347,17 @@ def finalize_loader(loader) # XXX: Should be removed once the `lib/metasploit` folder is loaded by Zeitwerk require 'metasploit/framework/hashes' + +# TODO: Remove this monkey-patch once the following issue is addressed over on Reline's repository here: +# https://github.com/ruby/reline/issues/756 +require 'reline' +class ::Reline::Core + alias old_completion_append_character= completion_append_character= + alias old_completion_append_character completion_append_character + + def completion_append_character=(v) + self.old_completion_append_character = v + # Additionally keep the line_editor in sync + line_editor.completion_append_character = self.old_completion_append_character + end +end diff --git a/lib/rex/post/meterpreter/ui/console/command_dispatcher/stdapi/fs.rb b/lib/rex/post/meterpreter/ui/console/command_dispatcher/stdapi/fs.rb index 676be4ace17e8..8183175cc161f 100644 --- a/lib/rex/post/meterpreter/ui/console/command_dispatcher/stdapi/fs.rb +++ b/lib/rex/post/meterpreter/ui/console/command_dispatcher/stdapi/fs.rb @@ -902,7 +902,7 @@ def tab_complete_cdirectory(str, words) def tab_complete_path(str, words, dir_only) if client.platform == 'windows' - ::Readline.completion_case_fold = true + Msf::Ui::Console::MsfReadline.instance.completion_case_fold = true end if client.commands.include?(COMMAND_ID_STDAPI_FS_LS) expanded = str @@ -915,7 +915,7 @@ def tab_complete_path(str, words, dir_only) # This is annoying if we're recursively tab-traversing our way through subdirectories - # we may want to continue traversing, but MSF will add a space, requiring us to back up to continue # tab-completing our way through successive subdirectories. - ::Readline.completion_append_character = nil + Msf::Ui::Console::MsfReadline.instance.completion_append_character = nil end results else diff --git a/lib/rex/post/sql/ui/console/interactive_sql_client.rb b/lib/rex/post/sql/ui/console/interactive_sql_client.rb index 75fc5c106f452..6a57bc7eaf19e 100644 --- a/lib/rex/post/sql/ui/console/interactive_sql_client.rb +++ b/lib/rex/post/sql/ui/console/interactive_sql_client.rb @@ -73,7 +73,7 @@ def _multiline_with_fallback return { status: :fail, errors: ["Unable to get history file for session type: #{name}"] } if history_file.nil? # Multiline (Reline) and fallback (Readline) have separate history contexts as they are two different libraries. - framework.history_manager.with_context(history_file: history_file , name: name, input_library: :reline) do + framework.history_manager.with_context(history_file: history_file, name: name, input_library: :reline) do query = _multiline end diff --git a/lib/rex/ui/text/dispatcher_shell.rb b/lib/rex/ui/text/dispatcher_shell.rb index 2131cc363292d..58c26c44fb51f 100644 --- a/lib/rex/ui/text/dispatcher_shell.rb +++ b/lib/rex/ui/text/dispatcher_shell.rb @@ -311,7 +311,7 @@ def tab_complete_directory(str, words) # This is annoying if we're recursively tab-traversing our way through subdirectories - # we may want to continue traversing, but MSF will add a space, requiring us to back up to continue # tab-completing our way through successive subdirectories. - ::Readline.completion_append_character = nil + Msf::Ui::Console::MsfReadline.instance.completion_append_character = nil end if dirs.length == 0 && File.directory?(str) @@ -408,9 +408,13 @@ def initialize(prompt, prompt_char = '>', histfile = nil, framework = nil, name # a design problem in the Readline module and depends on the # Readline.basic_word_break_characters variable being set to \x00 # - def tab_complete(str) - ::Readline.completion_append_character = ' ' - ::Readline.completion_case_fold = false + def tab_complete(str, opts: {}) + Msf::Ui::Console::MsfReadline.instance.completion_append_character = ' ' + Msf::Ui::Console::MsfReadline.instance.completion_case_fold = false + + if opts[:preposing] && Msf::Ui::Console::MsfReadline.instance.using_reline? + str = "#{opts[:preposing]}#{str}" + end # Check trailing whitespace so we can tell 'x' from 'x ' str_match = str.match(/[^\\]([\\]{2})*\s+$/) @@ -424,11 +428,7 @@ def tab_complete(str) # Pop the last word and pass it to the real method result = tab_complete_stub(str, split_str) - if result - result.uniq - else - result - end + result&.uniq end # Performs tab completion of a command, if supported diff --git a/lib/rex/ui/text/input/readline.rb b/lib/rex/ui/text/input/readline.rb index 85ca9d1218056..529a39e34e7ed 100644 --- a/lib/rex/ui/text/input/readline.rb +++ b/lib/rex/ui/text/input/readline.rb @@ -4,30 +4,32 @@ module Rex module Ui module Text -begin - - ### # # This class implements standard input using readline against # standard input. It supports tab completion. # - ### class Input::Readline < Rex::Ui::Text::Input # - # Initializes the readline-aware Input instance for text. + # The prompt that is to be displayed. # - def initialize(tab_complete_proc = nil) - if(not Object.const_defined?('Readline')) - require 'readline' - end + attr_accessor :prompt - self.extend(::Readline) + # + # The output handle to use when displaying the prompt. + # + attr_accessor :output + # + # Initializes the readline-aware Input instance for text. + # + def initialize(tab_complete_proc = nil) + super() if tab_complete_proc - ::Readline.basic_word_break_characters = "" - @rl_saved_proc = with_error_handling(tab_complete_proc) - ::Readline.completion_proc = @rl_saved_proc + Msf::Ui::Console::MsfReadline.instance.basic_word_break_characters = '' + # Cache the value so that we can use it when resetting the proc. + @completion_proc = tab_complete_proc + Msf::Ui::Console::MsfReadline.instance.completion_proc = with_error_handling(@completion_proc) end end @@ -35,24 +37,11 @@ def initialize(tab_complete_proc = nil) # Reattach the original completion proc # def reset_tab_completion(tab_complete_proc = nil) - ::Readline.basic_word_break_characters = "\x00" - ::Readline.completion_proc = tab_complete_proc ? with_error_handling(tab_complete_proc) : @rl_saved_proc + Msf::Ui::Console::MsfReadline.instance.basic_word_break_characters = "\x00" + @completion_proc = with_error_handling(tab_complete_proc) if tab_complete_proc + Msf::Ui::Console::MsfReadline.instance.completion_proc = @completion_proc end - - # - # Retrieve the line buffer - # - def line_buffer - if defined? RbReadline - RbReadline.rl_line_buffer - else - ::Readline.line_buffer - end - end - - attr_accessor :prompt - # # Whether or not the input medium supports readline. # @@ -95,11 +84,11 @@ def pgets begin Thread.current.priority = -20 - output.prompting - line = readline_with_output(prompt, true) - ::Readline::HISTORY.pop if (line and line.empty?) + output.prompting(true) + line = Msf::Ui::Console::MsfReadline.instance.readline(prompt, true, opts: { fd: fd, output: output }) ensure Thread.current.priority = orig || 0 + output.prompting(false) end line @@ -120,88 +109,21 @@ def intrinsic_shell? true end - # - # The prompt that is to be displayed. - # - attr_accessor :prompt - # - # The output handle to use when displaying the prompt. - # - attr_accessor :output - private - def readline_with_output(prompt, add_history=false) - # rb-readlines's Readline.readline hardcodes the input and output to - # $stdin and $stdout, which means setting `Readline.input` or - # `Readline.ouput` has no effect when running `Readline.readline` with - # rb-readline, so need to reimplement - # []`Readline.readline`](https://github.com/luislavena/rb-readline/blob/ce4908dae45dbcae90a6e42e3710b8c3a1f2cd64/lib/readline.rb#L36-L58) - # for rb-readline to support setting input and output. Output needs to - # be set so that colorization works for the prompt on Windows. - self.prompt = prompt - - # TODO: there are unhandled quirks in async output buffering that - # we have not solved yet, for instance when loading meterpreter - # extensions, supporting Windows, printing output from commands, etc. - # Remove this guard when issues are resolved. -=begin - reset_sequence = "\n\001\r\033[K\002" - if (/mingw/ =~ RUBY_PLATFORM) - reset_sequence = "" - end -=end - reset_sequence = "" - - if defined? RbReadline - RbReadline.rl_instream = fd - RbReadline.rl_outstream = output - - begin - line = RbReadline.readline(reset_sequence + prompt) - rescue ::Exception => exception - RbReadline.rl_cleanup_after_signal() - RbReadline.rl_deprep_terminal() - - raise exception - end - - if add_history && line && !line.start_with?(' ') - # Don't add duplicate lines to history - if ::Readline::HISTORY.empty? || line.strip != ::Readline::HISTORY[-1] - RbReadline.add_history(line.strip) - end - end - - line.try(:dup) - else - # The line that's read is immediately added to history - line = ::Readline.readline(reset_sequence + prompt, true) - - # Don't add duplicate lines to history - if ::Readline::HISTORY.length > 1 && line == ::Readline::HISTORY[-2] - ::Readline::HISTORY.pop - end - - line - end - end - - private + attr_accessor :completion_proc def with_error_handling(proc) proc do |*args| proc.call(*args) - rescue StandardError => e + rescue ::StandardError => e elog("tab_complete_proc has failed with args #{args}", error: e) [] end end end -rescue LoadError end end end -end diff --git a/lib/rex/ui/text/irb_shell.rb b/lib/rex/ui/text/irb_shell.rb index 49536041dcc53..20d2511bc7f86 100644 --- a/lib/rex/ui/text/irb_shell.rb +++ b/lib/rex/ui/text/irb_shell.rb @@ -28,6 +28,7 @@ def run IRB.setup(nil) IRB.conf[:PROMPT_MODE] = :SIMPLE + IRB.conf[:USE_MULTILINE] = true @@IrbInitialized = true end diff --git a/lib/rex/ui/text/output.rb b/lib/rex/ui/text/output.rb index ef8f8fd030c6c..7877e81d61eaa 100644 --- a/lib/rex/ui/text/output.rb +++ b/lib/rex/ui/text/output.rb @@ -86,6 +86,11 @@ def puts(*args) nil end + + # For Reline interop + def tty? + true + end end end diff --git a/lib/rex/ui/text/output/stdio.rb b/lib/rex/ui/text/output/stdio.rb index 085c912a60335..7d53fa0af8088 100644 --- a/lib/rex/ui/text/output/stdio.rb +++ b/lib/rex/ui/text/output/stdio.rb @@ -94,6 +94,7 @@ def print_raw(msg = '') msg end alias_method :write, :print_raw + alias_method :<<, :write def supports_color? case config[:color] diff --git a/lib/rex/ui/text/output/tee.rb b/lib/rex/ui/text/output/tee.rb index ebfbb4034a4b7..c729590ed05dc 100644 --- a/lib/rex/ui/text/output/tee.rb +++ b/lib/rex/ui/text/output/tee.rb @@ -44,6 +44,7 @@ def print_raw(msg = '') end alias :write :print_raw + alias_method :<<, :write def close self.fd.close if self.fd diff --git a/lib/rex/ui/text/shell.rb b/lib/rex/ui/text/shell.rb index 56a323c5bfacd..8cf0452ff6f09 100644 --- a/lib/rex/ui/text/shell.rb +++ b/lib/rex/ui/text/shell.rb @@ -63,10 +63,21 @@ def initialize(prompt, prompt_char = '>', histfile = nil, framework = nil, name self.framework = framework end + def create_tab_complete_proc + # Unless cont_flag because there's no tab complete for continuation lines + proc do |str, preposing = nil, postposting = nil| + next nil if cont_flag + + result = tab_complete(str, opts: { preposing: preposing, postposting: postposting }) + result.map! { |val| val[preposing.to_s.length..] } if result && Msf::Ui::Console::MsfReadline.instance.using_reline? + + next result + end + end + def init_tab_complete if (self.input and self.input.supports_readline) - # Unless cont_flag because there's no tab complete for continuation lines - self.input = Input::Readline.new(lambda { |str| tab_complete(str) unless cont_flag }) + self.input = Input::Readline.new(create_tab_complete_proc) self.input.output = self.output end end @@ -114,8 +125,8 @@ def unset_log_source # # Performs tab completion on the supplied string. # - def tab_complete(str) - return tab_complete_proc(str) if (tab_complete_proc) + def tab_complete(str, opts: {}) + return tab_complete_proc(str, opts: opts) if (tab_complete_proc) end # @@ -304,13 +315,13 @@ def with_history_manager_context begin history_manager.with_context(history_file: histfile, name: name) do - self.hist_last_saved = Readline::HISTORY.length + self.hist_last_saved = Msf::Ui::Console::MsfReadline.instance.history.length yield end ensure history_manager.flush - self.hist_last_saved = Readline::HISTORY.length + self.hist_last_saved = Msf::Ui::Console::MsfReadline.instance.history.length end end diff --git a/lib/rex/ui/text/shell/history_manager.rb b/lib/rex/ui/text/shell/history_manager.rb index cde3495fc9e8e..8dc07ae8b0ecd 100644 --- a/lib/rex/ui/text/shell/history_manager.rb +++ b/lib/rex/ui/text/shell/history_manager.rb @@ -1,6 +1,7 @@ # -*- coding: binary -*- -require 'singleton' +require 'reline' +require 'readline' module Rex module Ui @@ -28,8 +29,10 @@ def initialize # @param [Proc] block # @return [nil] def with_context(history_file: nil, name: nil, input_library: nil, &block) - # Default to Readline for backwards compatibility. - push_context(history_file: history_file, name: name, input_library: input_library || :readline) + # Instead of defaulting to either Reline or Readline and potentially break the console when things get out of sync, + # use the single source of truth to determine what we should default to. + default_input_library = Msf::Ui::Console::MsfReadline.instance.using_reline? ? :reline : :readline + push_context(history_file: history_file, name: name, input_library: input_library || default_input_library) begin block.call @@ -124,12 +127,7 @@ def readline_available? end def reline_available? - begin - require 'reline' - defined?(::Reline) - rescue ::LoadError => _e - false - end + defined?(::Reline) end def clear_readline diff --git a/modules/post/linux/manage/pseudo_shell.rb b/modules/post/linux/manage/pseudo_shell.rb index 6cdac56f8e230..2dbdb9a7ff3ae 100644 --- a/modules/post/linux/manage/pseudo_shell.rb +++ b/modules/post/linux/manage/pseudo_shell.rb @@ -4,6 +4,7 @@ ## require 'readline' +require 'reline' class MetasploitModule < Msf::Post include Msf::Post::File @@ -104,9 +105,9 @@ def help def prompt_show promptshell = "#{@vusername}@#{@vhostname}:#{pwd.strip}#{@vpromptchar} " comp = proc { |s| LIST.grep(/^#{Regexp.escape(s)}/) } - Readline.completion_append_character = ' ' - Readline.completion_proc = comp - input = Readline.readline(promptshell, true) + Msf::Ui::Console::MsfReadline.instance.completion_append_character = ' ' + Msf::Ui::Console::MsfReadline.instance.completion_proc = comp + input = Msf::Ui::Console::MsfReadline.instance.readline(promptshell, true) return nil if input.nil? input diff --git a/spec/lib/msf/debug_spec.rb b/spec/lib/msf/debug_spec.rb index 82baa903f8bdd..ce7cd60899129 100644 --- a/spec/lib/msf/debug_spec.rb +++ b/spec/lib/msf/debug_spec.rb @@ -212,7 +212,7 @@ end it 'correctly retrieves and parses a command history shorter than the command total' do - stub_const('Readline::HISTORY', Array.new(4) { |i| "Command #{i + 1}" }) + allow(Msf::Ui::Console::MsfReadline.instance).to receive(:history).and_return(Array.new(4) { |i| "Command #{i + 1}" }) driver = instance_double( Msf::Ui::Console::Driver, @@ -251,7 +251,7 @@ stub_const('Msf::Ui::Debug::COMMAND_HISTORY_TOTAL', 10) - stub_const('Readline::HISTORY', Array.new(10) { |i| "Command #{i + 1}" }) + allow(Msf::Ui::Console::MsfReadline.instance).to receive(:history).and_return(Array.new(10) { |i| "Command #{i + 1}" }) history_output = <<~E_LOG ## %grnHistory%clr @@ -288,7 +288,7 @@ ) stub_const('Msf::Ui::Debug::COMMAND_HISTORY_TOTAL', 10) - stub_const('Readline::HISTORY', Array.new(15) { |i| "Command #{i + 1}" }) + allow(Msf::Ui::Console::MsfReadline.instance).to receive(:history).and_return(Array.new(15) { |i| "Command #{i + 1}" }) history_output = <<~E_LOG ## %grnHistory%clr @@ -325,7 +325,7 @@ ) stub_const('Msf::Ui::Debug::COMMAND_HISTORY_TOTAL', 10) - stub_const('Readline::HISTORY', Array.new(15) { |i| "Command #{i + 1}" }) + allow(Msf::Ui::Console::MsfReadline.instance).to receive(:history).and_return(Array.new(15) { |i| "Command #{i + 1}" }) history_output = <<~E_LOG ## %grnHistory%clr diff --git a/spec/lib/msf/ui/console/command_dispatcher/core_spec.rb b/spec/lib/msf/ui/console/command_dispatcher/core_spec.rb index 79de59e7c79fb..f0088811f4c7b 100644 --- a/spec/lib/msf/ui/console/command_dispatcher/core_spec.rb +++ b/spec/lib/msf/ui/console/command_dispatcher/core_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' require 'readline' +require 'reline' RSpec.describe Msf::Ui::Console::CommandDispatcher::Core do include_context 'Msf::DBManager' diff --git a/spec/lib/msf/ui/text/dispatcher_shell_spec.rb b/spec/lib/msf/ui/text/dispatcher_shell_spec.rb index a9dbea33842d4..15941b04f67e0 100644 --- a/spec/lib/msf/ui/text/dispatcher_shell_spec.rb +++ b/spec/lib/msf/ui/text/dispatcher_shell_spec.rb @@ -1,5 +1,6 @@ require 'spec_helper' require 'readline' +require 'reline' RSpec.describe Rex::Ui::Text::DispatcherShell do let(:prompt) { '%undmsf6%clr' } diff --git a/tools/exploit/metasm_shell.rb b/tools/exploit/metasm_shell.rb index b2b24234083cf..68a5b22adad9b 100755 --- a/tools/exploit/metasm_shell.rb +++ b/tools/exploit/metasm_shell.rb @@ -31,7 +31,6 @@ $:.unshift(ENV['MSF_LOCAL_LIB']) if ENV['MSF_LOCAL_LIB'] require 'rex' -require 'readline' require 'metasm' #PowerPC, seems broken for now in metasm diff --git a/tools/exploit/nasm_shell.rb b/tools/exploit/nasm_shell.rb index 9915274b37d1f..c60543c2a2322 100755 --- a/tools/exploit/nasm_shell.rb +++ b/tools/exploit/nasm_shell.rb @@ -21,7 +21,6 @@ require 'msfenv' require 'rex' -require 'readline' # Check to make sure nasm is installed and reachable through the user's PATH. begin