From 83a07c50a7ef146e2c2175f72bc000b80d32c5e6 Mon Sep 17 00:00:00 2001 From: sjanusz-r7 Date: Tue, 13 Aug 2024 15:51:09 +0100 Subject: [PATCH] WIP: Replace Readline with Reline --- lib/metasploit/framework/command/console.rb | 1 - .../framework/parsed_options/console.rb | 7 +- .../framework/parsed_options/remote_db.rb | 1 - lib/msf/ui/console/command_dispatcher/core.rb | 16 ++-- .../console/command_dispatcher/developer.rb | 4 + lib/msf/ui/console/driver.rb | 56 ++---------- .../console/module_option_tab_completion.rb | 12 ++- lib/msf/ui/debug.rb | 8 +- .../console/command_dispatcher/stdapi/fs.rb | 9 +- .../sql/ui/console/interactive_sql_client.rb | 44 +--------- lib/rex/ui/text/dispatcher_shell.rb | 8 +- lib/rex/ui/text/input/readline.rb | 73 ++++------------ lib/rex/ui/text/output/stdio.rb | 1 + lib/rex/ui/text/shell.rb | 8 +- lib/rex/ui/text/shell/history_manager.rb | 86 +++++-------------- metasploit-framework.gemspec | 5 +- modules/post/linux/manage/pseudo_shell.rb | 9 +- msfconsole | 2 + spec/lib/msf/debug_spec.rb | 8 +- .../console/command_dispatcher/core_spec.rb | 6 +- spec/lib/msf/ui/text/dispatcher_shell_spec.rb | 2 +- .../rex/ui/text/shell/history_manager_spec.rb | 47 +++++----- tools/exploit/metasm_shell.rb | 2 +- tools/exploit/nasm_shell.rb | 2 +- 24 files changed, 141 insertions(+), 276 deletions(-) diff --git a/lib/metasploit/framework/command/console.rb b/lib/metasploit/framework/command/console.rb index 3c476574289c9..d104a042d33be 100644 --- a/lib/metasploit/framework/command/console.rb +++ b/lib/metasploit/framework/command/console.rb @@ -92,7 +92,6 @@ def driver_options driver_options['ModulePath'] = options.modules.path driver_options['Plugins'] = options.console.plugins driver_options['Readline'] = options.console.readline - driver_options['RealReadline'] = options.console.real_readline driver_options['Resource'] = options.console.resources driver_options['XCommands'] = options.console.commands diff --git a/lib/metasploit/framework/parsed_options/console.rb b/lib/metasploit/framework/parsed_options/console.rb index 34aa126633aa3..716ce1b741b32 100644 --- a/lib/metasploit/framework/parsed_options/console.rb +++ b/lib/metasploit/framework/parsed_options/console.rb @@ -16,7 +16,6 @@ def options options.console.plugins = [] options.console.quiet = false options.console.readline = true - options.console.real_readline = false options.console.resources = [] options.console.subcommand = :run } @@ -54,7 +53,11 @@ def option_parser end option_parser.on('-L', '--real-readline', 'Use the system Readline library instead of RbReadline') do - options.console.real_readline = true + message = "The RealReadline option has been marked as deprecated, and is currently a noop.\n" + message << "Metasploit Framework now uses Reline exclusively as the input handling library.\n" + message << "If you require this functionality, please raise an issue on GitHub:\n" + message << ' https://github.com/rapid7/metasploit-framework/issues/new?assignees=&labels=bug&projects=&template=bug_report.md' + warn message end option_parser.on('-o', '--output FILE', 'Output to the specified file') do |file| diff --git a/lib/metasploit/framework/parsed_options/remote_db.rb b/lib/metasploit/framework/parsed_options/remote_db.rb index 49dd9c466644d..5344539bfd42c 100644 --- a/lib/metasploit/framework/parsed_options/remote_db.rb +++ b/lib/metasploit/framework/parsed_options/remote_db.rb @@ -13,7 +13,6 @@ def options options.console.local_output = nil options.console.plugins = [] options.console.quiet = false - options.console.real_readline = false options.console.resources = [] options.console.subcommand = :run } diff --git a/lib/msf/ui/console/command_dispatcher/core.rb b/lib/msf/ui/console/command_dispatcher/core.rb index 5599879afad6a..f12e8f07d8d6c 100644 --- a/lib/msf/ui/console/command_dispatcher/core.rb +++ b/lib/msf/ui/console/command_dispatcher/core.rb @@ -760,7 +760,7 @@ def cmd_features_tabs(_str, words) end def cmd_history(*args) - length = Readline::HISTORY.length + length = history_object.length if length < @history_limit limit = length @@ -780,10 +780,8 @@ 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_object.respond_to?(:clear) + history_object.clear else print_error('Could not clear history, skipping file') return false @@ -808,7 +806,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 @@ -2895,6 +2893,12 @@ def retrieve_grep_lines(all_lines,line_num, before = nil, after = nil) all_lines.slice(start..finish) end + private + + def history_object + ::Reline::HISTORY + end + end end end end end diff --git a/lib/msf/ui/console/command_dispatcher/developer.rb b/lib/msf/ui/console/command_dispatcher/developer.rb index 33c2c39a3f1f6..df756775d3e61 100644 --- a/lib/msf/ui/console/command_dispatcher/developer.rb +++ b/lib/msf/ui/console/command_dispatcher/developer.rb @@ -515,6 +515,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/driver.rb b/lib/msf/ui/console/driver.rb index bce665b24eb9f..a701f8fbd4e11 100644 --- a/lib/msf/ui/console/driver.rb +++ b/lib/msf/ui/console/driver.rb @@ -53,8 +53,6 @@ class Driver < Msf::Ui::Driver # @option opts [Boolean] 'AllowCommandPassthru' (true) Whether to allow # unrecognized commands to be executed by the system shell # @option opts [Boolean] 'Readline' (true) Whether to use the readline or not - # @option opts [Boolean] 'RealReadline' (false) Whether to use the system's - # readline library instead of RBReadline # @option opts [String] 'HistFile' (Msf::Config.history_file) Path to a file # where we can store command history # @option opts [Array] 'Resources' ([]) A list of resource files to @@ -64,8 +62,6 @@ class Driver < Msf::Ui::Driver # @option opts [Boolean] 'SkipDatabaseInit' (false) Whether to skip # connecting to the database and running migrations def initialize(prompt = DefaultPrompt, prompt_char = DefaultPromptChar, opts = {}) - choose_readline(opts) - histfile = opts['HistFile'] || Msf::Config.history_file begin @@ -132,14 +128,6 @@ def initialize(prompt = DefaultPrompt, prompt_char = DefaultPromptChar, opts = { # stack enstack_dispatcher(CommandDispatcher::Core) - # Report readline error if there was one.. - if !@rl_err.nil? - print_error("***") - print_error("* Unable to load readline: #{@rl_err}") - print_error("* Falling back to RbReadLine") - print_error("***") - end - # Load the other "core" command dispatchers CommandDispatchers.each do |dispatcher_class| dispatcher = enstack_dispatcher(dispatcher_class) @@ -323,11 +311,11 @@ def save_config # Saves the recent history to the specified file # def save_recent_history(path) - num = Readline::HISTORY.length - hist_last_saved - 1 + num = history_object.length - hist_last_saved - 1 tmprc = "" num.times { |x| - tmprc << Readline::HISTORY[hist_last_saved + x] + "\n" + tmprc << history_object[hist_last_saved + x] + "\n" } if tmprc.length > 0 @@ -339,7 +327,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_object.length end # @@ -703,42 +691,10 @@ def handle_session_tlv_logging(val) false end - # Require the appropriate readline library based on the user's preference. - # - # @return [void] - def choose_readline(opts) - # Choose a readline library before calling the parent - @rl_err = nil - if opts['RealReadline'] - # Remove the gem version from load path to be sure we're getting the - # stdlib readline. - gem_dir = Gem::Specification.find_all_by_name('rb-readline').first.gem_dir - rb_readline_path = File.join(gem_dir, "lib") - index = $LOAD_PATH.index(rb_readline_path) - # Bundler guarantees that the gem will be there, so it should be safe to - # assume we found it in the load path, but check to be on the safe side. - if index - $LOAD_PATH.delete_at(index) - end - end + private - begin - require 'readline' - rescue ::LoadError => e - if @rl_err.nil? && index - # Then this is the first time the require failed and we have an index - # for the gem version as a fallback. - @rl_err = e - # Put the gem back and see if that works - $LOAD_PATH.insert(index, rb_readline_path) - index = rb_readline_path = nil - retry - else - # Either we didn't have the gem to fall back on, or we failed twice. - # Nothing more we can do here. - raise e - end - end + def history_object + ::Reline::HISTORY end 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..45146f7a9b448 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 = ' ' + input_library.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 = ' ' + input_library.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 = '' + input_library.completion_append_character = '' tab_complete_option_names(mod, str, words).map { |name| "#{name}=" } end @@ -369,6 +369,12 @@ def option_values_target_ports(mod) res.uniq end + + private + + def input_library + ::Reline + end end end end diff --git a/lib/msf/ui/debug.rb b/lib/msf/ui/debug.rb index 6bf8f8e3b1385..75036b404aa8c 100644 --- a/lib/msf/ui/debug.rb +++ b/lib/msf/ui/debug.rb @@ -220,13 +220,13 @@ def self.framework_config(framework) end def self.history(driver) - end_pos = Readline::HISTORY.length - 1 + end_pos = history_object.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_object[start_pos]}\n" start_pos += 1 end @@ -295,6 +295,10 @@ class << self private + def history_object + ::Reline::HISTORY + end + def build_regex_file_section(path, match_total, regex, header_name, blurb) unless File.file?(path) return build_section( 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..91211fc69583b 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 + input_library.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 + input_library.completion_append_character = nil end results else @@ -940,6 +940,11 @@ def unexpand_path_for_suggestions(original_path, expanded_path, suggestions) end end + private + + def input_library + ::Reline + end end end 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..d8b39af488bd1 100644 --- a/lib/rex/post/sql/ui/console/interactive_sql_client.rb +++ b/lib/rex/post/sql/ui/console/interactive_sql_client.rb @@ -20,7 +20,7 @@ module InteractiveSqlClient # def _interact while self.interacting - sql_input = _multiline_with_fallback + sql_input = reline_multiline self.interacting = (sql_input[:status] != :exit) if sql_input[:status] == :help @@ -65,35 +65,21 @@ def _winch # noop end - # Try getting multi-line input support provided by Reline, fall back to Readline. - def _multiline_with_fallback + # Get multi-line input support provided by Reline. + def reline_multiline name = session.type query = {} history_file = Msf::Config.history_file_for_session_type(session_type: name, interactive: true) 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) do query = _multiline end - if query[:status] == :fail - framework.history_manager.with_context(history_file: history_file, name: name, input_library: :readline) do - query = _fallback - end - end - query end def _multiline - begin - require 'reline' unless defined?(::Reline) - rescue ::LoadError => e - elog('Failed to load Reline', e) - return { status: :fail, errors: [e] } - end - stop_words = %w[stop s exit e end quit q].freeze help_words = %w[help h].freeze @@ -152,28 +138,6 @@ def _multiline { status: :success, result: raw_query } end - def _fallback - stop_words = %w[stop s exit e end quit q].freeze - line_buffer = [] - while (line = ::Readline.readline(prompt = line_buffer.empty? ? 'SQL >> ' : 'SQL *> ', add_history = true)) - return { status: :exit, result: nil } unless self.interacting - - if stop_words.include? line.chomp.downcase - self.interacting = false - print_status 'Exiting Interactive mode.' - return { status: :exit, result: nil } - end - - next if line.empty? - - line_buffer.append line - - break if line.end_with? ';' - end - - { status: :success, result: line_buffer.join(' ') } - end - attr_accessor :on_log_proc, :client_dispatcher private diff --git a/lib/rex/ui/text/dispatcher_shell.rb b/lib/rex/ui/text/dispatcher_shell.rb index 2131cc363292d..239f39c2c689b 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 + ::Reline.completion_append_character = nil end if dirs.length == 0 && File.directory?(str) @@ -406,11 +406,11 @@ def initialize(prompt, prompt_char = '>', histfile = nil, framework = nil, name # routine, stores all completed words, and passes the partial # word to the real tab completion function. This works around # a design problem in the Readline module and depends on the - # Readline.basic_word_break_characters variable being set to \x00 + # Reline.completer_word_break_characters variable being set to \x00 # def tab_complete(str) - ::Readline.completion_append_character = ' ' - ::Readline.completion_case_fold = false + ::Reline.completion_append_character = ' ' + ::Reline.completion_case_fold = false # Check trailing whitespace so we can tell 'x' from 'x ' str_match = str.match(/[^\\]([\\]{2})*\s+$/) diff --git a/lib/rex/ui/text/input/readline.rb b/lib/rex/ui/text/input/readline.rb index 85ca9d1218056..6be7465162f29 100644 --- a/lib/rex/ui/text/input/readline.rb +++ b/lib/rex/ui/text/input/readline.rb @@ -8,7 +8,7 @@ module Text ### # - # This class implements standard input using readline against + # This class implements standard input using Reline against # standard input. It supports tab completion. # ### @@ -18,16 +18,12 @@ class Input::Readline < Rex::Ui::Text::Input # Initializes the readline-aware Input instance for text. # def initialize(tab_complete_proc = nil) - if(not Object.const_defined?('Readline')) - require 'readline' - end - - self.extend(::Readline) + self.extend(::Reline) if tab_complete_proc - ::Readline.basic_word_break_characters = "" + ::Reline.completer_word_break_characters = "" @rl_saved_proc = with_error_handling(tab_complete_proc) - ::Readline.completion_proc = @rl_saved_proc + ::Reline.completion_proc = @rl_saved_proc end end @@ -35,8 +31,8 @@ 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 + ::Reline.completer_word_break_characters = "\x00" + ::Reline.completion_proc = tab_complete_proc ? with_error_handling(tab_complete_proc) : @rl_saved_proc end @@ -44,11 +40,7 @@ def reset_tab_completion(tab_complete_proc = nil) # Retrieve the line buffer # def line_buffer - if defined? RbReadline - RbReadline.rl_line_buffer - else - ::Readline.line_buffer - end + ::Reline.line_buffer end attr_accessor :prompt @@ -97,7 +89,6 @@ def pgets output.prompting line = readline_with_output(prompt, true) - ::Readline::HISTORY.pop if (line and line.empty?) ensure Thread.current.priority = orig || 0 end @@ -132,13 +123,8 @@ def intrinsic_shell? 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. + # 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 @@ -153,38 +139,17 @@ def readline_with_output(prompt, add_history=false) =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 + ::Reline.input = fd + ::Reline.output = output + + line = ::Reline.readline(reset_sequence + prompt, true) + + # Don't add duplicate lines to history + if ::Reline::HISTORY.length > 1 && line == ::Reline::HISTORY[-2] + ::Reline::HISTORY.pop end + + line end private diff --git a/lib/rex/ui/text/output/stdio.rb b/lib/rex/ui/text/output/stdio.rb index 085c912a60335..4ea52954924f6 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/shell.rb b/lib/rex/ui/text/shell.rb index 56a323c5bfacd..a88fd98744d73 100644 --- a/lib/rex/ui/text/shell.rb +++ b/lib/rex/ui/text/shell.rb @@ -304,13 +304,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 = history_object.length yield end ensure history_manager.flush - self.hist_last_saved = Readline::HISTORY.length + self.hist_last_saved = history_object.length end end @@ -511,6 +511,10 @@ def format_prompt(str) attr_accessor :name private + def history_object + ::Reline::HISTORY + end + def try_exec(command) begin %x{ #{ command } } diff --git a/lib/rex/ui/text/shell/history_manager.rb b/lib/rex/ui/text/shell/history_manager.rb index cde3495fc9e8e..9d9f61e4ea34b 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' module Rex module Ui @@ -24,12 +25,11 @@ def initialize # # @param [String,nil] history_file The file to load and persist commands to # @param [String] name Human readable history context name - # @param [Symbol] input_library The input library to provide context for. :reline, :readline # @param [Proc] block # @return [nil] - def with_context(history_file: nil, name: nil, input_library: nil, &block) + def with_context(history_file: nil, name: nil, &block) # Default to Readline for backwards compatibility. - push_context(history_file: history_file, name: name, input_library: input_library || :readline) + push_context(history_file: history_file, name: name) begin block.call @@ -69,37 +69,17 @@ def _close private - def debug? - @debug - end - - # A wrapper around mapping the input library to its history; this way we can mock the return value of this method. - def map_library_to_history(input_library) - case input_library - when :readline - ::Readline::HISTORY - when :reline - ::Reline::HISTORY - else - $stderr.puts("Unknown input library: #{input_library}") if debug? - [] - end + def history_object + ::Reline::HISTORY end - def clear_library(input_library) - case input_library - when :readline - clear_readline - when :reline - clear_reline - else - $stderr.puts("Unknown input library: #{input_library}") if debug? - end + def debug? + @debug end - def push_context(history_file: nil, name: nil, input_library: nil) + def push_context(history_file: nil, name: nil) $stderr.puts("Push context before\n#{JSON.pretty_generate(_contexts)}") if debug? - new_context = { history_file: history_file, name: name, input_library: input_library || :readline } + new_context = { history_file: history_file, name: name } switch_context(new_context, @contexts.last) @contexts.push(new_context) @@ -119,44 +99,22 @@ def pop_context nil end - def readline_available? - defined?(::Readline) - end - - def reline_available? - begin - require 'reline' - defined?(::Reline) - rescue ::LoadError => _e - false - end - end - - def clear_readline - return unless readline_available? - - ::Readline::HISTORY.length.times { ::Readline::HISTORY.pop } - end - - def clear_reline - return unless reline_available? - - ::Reline::HISTORY.length.times { ::Reline::HISTORY.pop } + def clear_history + history_object.length.times { history_object.pop } end def load_history_file(context) history_file = context[:history_file] - history = map_library_to_history(context[:input_library]) begin File.open(history_file, 'rb') do |f| - clear_library(context[:input_library]) + clear_history f.each_line(chomp: true) do |line| - if context[:input_library] == :reline && history.last&.end_with?("\\") - history.last.delete_suffix!("\\") - history.last << "\n" << line + if history_object.last&.end_with?("\\") + history_object.last.delete_suffix!("\\") + history_object.last << "\n" << line else - history << line + history_object << line end end end @@ -167,13 +125,11 @@ def load_history_file(context) def store_history_file(context) history_file = context[:history_file] - history = map_library_to_history(context[:input_library]) - - history_diff = history.length < MAX_HISTORY ? history.length : MAX_HISTORY + history_diff = history_object.length < MAX_HISTORY ? history_object.length : MAX_HISTORY cmds = [] history_diff.times do - entry = history.pop + entry = history_object.pop cmds << entry.scrub.split("\n").join("\\\n") end @@ -188,12 +144,10 @@ def switch_context(new_context, old_context=nil) if new_context && new_context[:history_file] load_history_file(new_context) else - clear_readline - clear_reline + clear_history end rescue SignalException => _e - clear_readline - clear_reline + clear_history end def write_history_file(history_file, cmds) diff --git a/metasploit-framework.gemspec b/metasploit-framework.gemspec index bb0d3a5e80d2a..8bd2fff06a1b3 100644 --- a/metasploit-framework.gemspec +++ b/metasploit-framework.gemspec @@ -197,6 +197,8 @@ Gem::Specification.new do |spec| # Library for exploit development helpers spec.add_runtime_dependency 'rex-exploitation' # Command line editing, history, and tab completion in msfconsole + spec.add_runtime_dependency 'reline' + # Legacy support for FILENAME_COMPLETION_PROC, not present in Reline spec.add_runtime_dependency 'rb-readline' # Needed by some modules spec.add_runtime_dependency 'rubyzip' @@ -248,9 +250,6 @@ Gem::Specification.new do |spec| # to generate PNG files, not to parse untrusted PNG files. spec.add_runtime_dependency 'chunky_png' - # Needed for multiline REPL support for interactive SQL sessions - spec.add_runtime_dependency 'reline' - # Standard libraries: https://www.ruby-lang.org/en/news/2023/12/25/ruby-3-3-0-released/ %w[ abbrev diff --git a/modules/post/linux/manage/pseudo_shell.rb b/modules/post/linux/manage/pseudo_shell.rb index 6cdac56f8e230..8dafa8e3dd5b5 100644 --- a/modules/post/linux/manage/pseudo_shell.rb +++ b/modules/post/linux/manage/pseudo_shell.rb @@ -3,7 +3,7 @@ # Current source: https://github.com/rapid7/metasploit-framework ## -require 'readline' +require 'reline' class MetasploitModule < Msf::Post include Msf::Post::File @@ -104,9 +104,10 @@ 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) + $stderr.puts "[Log] Setting to comp" + Reline.completion_append_character = ' ' + Reline.completion_proc = comp + input = Reline.readline(promptshell, true) return nil if input.nil? input diff --git a/msfconsole b/msfconsole index 96630b48cb42e..95a8b9021653c 100755 --- a/msfconsole +++ b/msfconsole @@ -6,6 +6,8 @@ # require 'pathname' +require 'reline' + begin # Silences warnings as they only serve to confuse end users diff --git a/spec/lib/msf/debug_spec.rb b/spec/lib/msf/debug_spec.rb index 82baa903f8bdd..928d94b44492e 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}" }) + stub_const('Reline::HISTORY', 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}" }) + stub_const('Reline::HISTORY', 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}" }) + stub_const('Reline::HISTORY', 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}" }) + stub_const('Reline::HISTORY', 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..56c16b5ee185b 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,5 @@ require 'spec_helper' - -require 'readline' +require 'reline' RSpec.describe Msf::Ui::Console::CommandDispatcher::Core do include_context 'Msf::DBManager' @@ -333,11 +332,14 @@ def set_tabs_test(option) allow(double).to receive(:sessions).and_return([]) allow_any_instance_of(Msf::Post).to receive(:framework).and_return(double) + # require 'pry-byebug'; binding.pry; # Test for setting incomplete option output = core.cmd_set_tabs(option, ["set"]) + # require 'pry-byebug'; binding.pry; expect(output).to be_kind_of(Array).or eq(nil) # Test for setting option + require 'pry-byebug'; binding.pry; output = core.cmd_set_tabs("", ["set", option]) expect(output).to be_kind_of(Array).or eq(nil) end diff --git a/spec/lib/msf/ui/text/dispatcher_shell_spec.rb b/spec/lib/msf/ui/text/dispatcher_shell_spec.rb index a9dbea33842d4..c2da56b211a6c 100644 --- a/spec/lib/msf/ui/text/dispatcher_shell_spec.rb +++ b/spec/lib/msf/ui/text/dispatcher_shell_spec.rb @@ -1,5 +1,5 @@ require 'spec_helper' -require 'readline' +require 'reline' RSpec.describe Rex::Ui::Text::DispatcherShell do let(:prompt) { '%undmsf6%clr' } diff --git a/spec/lib/rex/ui/text/shell/history_manager_spec.rb b/spec/lib/rex/ui/text/shell/history_manager_spec.rb index 74c8bb76cc975..68f86bd75a561 100644 --- a/spec/lib/rex/ui/text/shell/history_manager_spec.rb +++ b/spec/lib/rex/ui/text/shell/history_manager_spec.rb @@ -7,13 +7,6 @@ include_context 'wait_for_expect' subject { described_class.send(:new) } - let(:readline_available) { false } - let(:reline_available) { false } - - before(:each) do - allow(subject).to receive(:readline_available?).and_return(readline_available) - allow(subject).to receive(:reline_available?).and_return(reline_available) - end describe '#with_context' do context 'when there is not an existing stack' do @@ -30,7 +23,7 @@ (expect do |block| subject.with_context(name: 'a') do expected_contexts = [ - { history_file: nil, input_library: :readline, name: 'a' }, + { history_file: nil, name: 'a' }, ] expect(subject._contexts).to eq(expected_contexts) block.to_proc.call @@ -41,7 +34,7 @@ context 'when there is an existing stack' do before(:each) do - subject.send(:push_context, history_file: nil, input_library: :readline, name: 'a') + subject.send(:push_context, history_file: nil, name: 'a') end it 'continues to have the previous existing stack' do @@ -49,7 +42,7 @@ # noop } expected_contexts = [ - { history_file: nil, input_library: :readline, name: 'a' }, + { history_file: nil, name: 'a' }, ] expect(subject._contexts).to eq(expected_contexts) end @@ -58,8 +51,8 @@ (expect do |block| subject.with_context(name: 'b') do expected_contexts = [ - { history_file: nil, input_library: :readline, name: 'a' }, - { history_file: nil, input_library: :readline, name: 'b' }, + { history_file: nil, name: 'a' }, + { history_file: nil, name: 'b' }, ] expect(subject._contexts).to eq(expected_contexts) block.to_proc.call @@ -74,7 +67,7 @@ } end.to raise_exception ArgumentError, 'Mock error' expected_contexts = [ - { history_file: nil, input_library: :readline, name: 'a' }, + { history_file: nil, name: 'a' }, ] expect(subject._contexts).to eq(expected_contexts) end @@ -84,9 +77,9 @@ describe '#push_context' do context 'when the stack is empty' do it 'stores the history contexts' do - subject.send(:push_context, history_file: nil, input_library: :readline, name: 'a') + subject.send(:push_context, history_file: nil, name: 'a') expected_contexts = [ - { history_file: nil, input_library: :readline, name: 'a' } + { history_file: nil, name: 'a' } ] expect(subject._contexts).to eq(expected_contexts) end @@ -95,12 +88,12 @@ context 'when multiple values are pushed' do it 'stores the history contexts' do subject.send(:push_context, history_file: nil, name: 'a') - subject.send(:push_context, history_file: nil, input_library: :readline, name: 'b') - subject.send(:push_context, history_file: nil, input_library: :reline, name: 'c') + subject.send(:push_context, history_file: nil, name: 'b') + subject.send(:push_context, history_file: nil, name: 'c') expected_contexts = [ - { history_file: nil, input_library: :readline, name: 'a' }, - { history_file: nil, input_library: :readline, name: 'b' }, - { history_file: nil, input_library: :reline, name: 'c' }, + { history_file: nil, name: 'a' }, + { history_file: nil, name: 'b' }, + { history_file: nil, name: 'c' }, ] expect(subject._contexts).to eq(expected_contexts) end @@ -123,7 +116,7 @@ subject.send(:push_context, history_file: nil, name: 'b') subject.send(:pop_context) expected_contexts = [ - { history_file: nil, input_library: :readline, name: 'a' }, + { history_file: nil, name: 'a' }, ] expect(subject._contexts).to eq(expected_contexts) end @@ -150,14 +143,14 @@ context "when storing #{test[:history_size]} lines" do it "correctly stores #{test[:expected_size]} lines" do allow(subject).to receive(:store_history_file).and_call_original - allow(subject).to receive(:map_library_to_history).and_return(history_mock) + allow(subject).to receive(:history).and_return(history_mock) test[:history_size].times do # This imitates the user typing in a command and pressing the 'enter' key. history_mock << history_choices.sample end - context = { input_library: :readline, history_file: history_file.path, name: 'history'} + context = { history_file: history_file.path, name: 'history' } subject.send(:store_history_file, context) @@ -182,9 +175,9 @@ context 'when history file is not accessible' do it 'the library history remains unchanged' do - allow(subject).to receive(:map_library_to_history).and_return(history_mock) + allow(subject).to receive(:history).and_return(history_mock) history_file = ::File.join('does', 'not', 'exist', 'history') - context = { input_library: :readline, history_file: history_file, name: 'history' } + context = { history_file: history_file, name: 'history' } subject.send(:load_history_file, context) expect(history_mock).to eq(initial_history) @@ -193,7 +186,7 @@ context 'when history file is accessible' do it 'correctly loads the history' do - allow(subject).to receive(:map_library_to_history).and_return(history_mock) + allow(subject).to receive(:history).and_return(history_mock) # Populate our own history file with random entries. # Using this allows us to not have to worry about history files present/not present on disk. @@ -204,7 +197,7 @@ history_file.puts new_history history_file.rewind - context = { input_library: :readline, history_file: history_file.path, name: 'history' } + context = { history_file: history_file.path, name: 'history' } subject.send(:load_history_file, context) diff --git a/tools/exploit/metasm_shell.rb b/tools/exploit/metasm_shell.rb index b2b24234083cf..5b7a55ff76411 100755 --- a/tools/exploit/metasm_shell.rb +++ b/tools/exploit/metasm_shell.rb @@ -31,7 +31,7 @@ $:.unshift(ENV['MSF_LOCAL_LIB']) if ENV['MSF_LOCAL_LIB'] require 'rex' -require 'readline' +require 'reline' 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..57fe288dacf7e 100755 --- a/tools/exploit/nasm_shell.rb +++ b/tools/exploit/nasm_shell.rb @@ -21,7 +21,7 @@ require 'msfenv' require 'rex' -require 'readline' +require 'reline' # Check to make sure nasm is installed and reachable through the user's PATH. begin