Revert "Replace Readline with Reline"
This commit is contained in:
@@ -92,6 +92,7 @@ class Metasploit::Framework::Command::Console < Metasploit::Framework::Command::
|
||||
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
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ class Metasploit::Framework::ParsedOptions::Console < Metasploit::Framework::Par
|
||||
options.console.plugins = []
|
||||
options.console.quiet = false
|
||||
options.console.readline = true
|
||||
options.console.real_readline = false
|
||||
options.console.resources = []
|
||||
options.console.subcommand = :run
|
||||
}
|
||||
@@ -53,11 +54,7 @@ class Metasploit::Framework::ParsedOptions::Console < Metasploit::Framework::Par
|
||||
end
|
||||
|
||||
option_parser.on('-L', '--real-readline', 'Use the system Readline library instead of RbReadline') do
|
||||
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 use the following link to tell us:\n"
|
||||
message << ' https://github.com/rapid7/metasploit-framework/issues/19399'
|
||||
warn message
|
||||
options.console.real_readline = true
|
||||
end
|
||||
|
||||
option_parser.on('-o', '--output FILE', 'Output to the specified file') do |file|
|
||||
|
||||
@@ -13,6 +13,7 @@ class Metasploit::Framework::ParsedOptions::RemoteDB < Metasploit::Framework::Pa
|
||||
options.console.local_output = nil
|
||||
options.console.plugins = []
|
||||
options.console.quiet = false
|
||||
options.console.real_readline = false
|
||||
options.console.resources = []
|
||||
options.console.subcommand = :run
|
||||
}
|
||||
|
||||
@@ -760,7 +760,7 @@ class Core
|
||||
end
|
||||
|
||||
def cmd_history(*args)
|
||||
length = ::Reline::HISTORY.length
|
||||
length = Readline::HISTORY.length
|
||||
|
||||
if length < @history_limit
|
||||
limit = length
|
||||
@@ -780,7 +780,14 @@ class Core
|
||||
limit = val.to_i
|
||||
end
|
||||
when '-c'
|
||||
::Reline::HISTORY.clear
|
||||
if Readline::HISTORY.respond_to?(:clear)
|
||||
Readline::HISTORY.clear
|
||||
elsif defined?(RbReadline)
|
||||
RbReadline.clear_history
|
||||
else
|
||||
print_error('Could not clear history, skipping file')
|
||||
return false
|
||||
end
|
||||
|
||||
# Portable file truncation?
|
||||
if File.writable?(Msf::Config.history_file)
|
||||
@@ -801,7 +808,7 @@ class Core
|
||||
|
||||
(start..length-1).each do |pos|
|
||||
cmd_num = (pos + 1).to_s
|
||||
print_line "#{cmd_num.ljust(pad_len)} #{::Reline::HISTORY[pos]}"
|
||||
print_line "#{cmd_num.ljust(pad_len)} #{Readline::HISTORY[pos]}"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -131,7 +131,6 @@ class Msf::Ui::Console::CommandDispatcher::Developer
|
||||
|
||||
framework.history_manager.with_context(name: :irb) do
|
||||
begin
|
||||
reline_autocomplete = Reline.autocompletion
|
||||
if active_module
|
||||
print_status("You are in #{active_module.fullname}\n")
|
||||
Rex::Ui::Text::IrbShell.new(active_module).run
|
||||
@@ -141,8 +140,6 @@ class Msf::Ui::Console::CommandDispatcher::Developer
|
||||
end
|
||||
rescue
|
||||
print_error("Error during IRB: #{$!}\n\n#{$@.join("\n")}")
|
||||
ensure
|
||||
Reline.autocompletion = reline_autocomplete if defined? reline_autocomplete
|
||||
end
|
||||
end
|
||||
|
||||
@@ -518,10 +515,6 @@ class Msf::Ui::Console::CommandDispatcher::Developer
|
||||
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
|
||||
|
||||
@@ -53,6 +53,8 @@ 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<String>] 'Resources' ([]) A list of resource files to
|
||||
@@ -62,6 +64,8 @@ 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
|
||||
@@ -128,6 +132,14 @@ class Driver < Msf::Ui::Driver
|
||||
# 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)
|
||||
@@ -311,11 +323,11 @@ class Driver < Msf::Ui::Driver
|
||||
# Saves the recent history to the specified file
|
||||
#
|
||||
def save_recent_history(path)
|
||||
num = ::Reline::HISTORY.length - hist_last_saved - 1
|
||||
num = Readline::HISTORY.length - hist_last_saved - 1
|
||||
|
||||
tmprc = ""
|
||||
num.times { |x|
|
||||
tmprc << ::Reline::HISTORY[hist_last_saved + x] + "\n"
|
||||
tmprc << Readline::HISTORY[hist_last_saved + x] + "\n"
|
||||
}
|
||||
|
||||
if tmprc.length > 0
|
||||
@@ -327,7 +339,7 @@ class Driver < Msf::Ui::Driver
|
||||
|
||||
# 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 = ::Reline::HISTORY.length
|
||||
self.hist_last_saved = Readline::HISTORY.length
|
||||
end
|
||||
|
||||
#
|
||||
@@ -690,6 +702,44 @@ protected
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -53,18 +53,18 @@ module Msf
|
||||
option_name = str.chop
|
||||
option_value = ''
|
||||
|
||||
::Reline.completion_append_character = ' '
|
||||
::Readline.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
|
||||
|
||||
::Reline.completion_append_character = ' '
|
||||
::Readline.completion_append_character = ' '
|
||||
return tab_complete_option_values(mod, option_value, words, opt: option_name).map { |value| "#{option_name}=#{value}" }
|
||||
end
|
||||
|
||||
::Reline.completion_append_character = ''
|
||||
::Readline.completion_append_character = ''
|
||||
tab_complete_option_names(mod, str, words).map { |name| "#{name}=" }
|
||||
end
|
||||
|
||||
|
||||
+2
-2
@@ -220,13 +220,13 @@ module Msf
|
||||
end
|
||||
|
||||
def self.history(driver)
|
||||
end_pos = ::Reline::HISTORY.length - 1
|
||||
end_pos = Readline::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} #{::Reline::HISTORY[start_pos]}\n"
|
||||
commands += "#{'%-6.6s' % start_pos.to_s} #{Readline::HISTORY[start_pos]}\n"
|
||||
start_pos += 1
|
||||
end
|
||||
|
||||
|
||||
@@ -902,7 +902,7 @@ class Console::CommandDispatcher::Stdapi::Fs
|
||||
|
||||
def tab_complete_path(str, words, dir_only)
|
||||
if client.platform == 'windows'
|
||||
::Reline.completion_case_fold = true
|
||||
::Readline.completion_case_fold = true
|
||||
end
|
||||
if client.commands.include?(COMMAND_ID_STDAPI_FS_LS)
|
||||
expanded = str
|
||||
@@ -915,7 +915,7 @@ class Console::CommandDispatcher::Stdapi::Fs
|
||||
# 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.
|
||||
::Reline.completion_append_character = nil
|
||||
::Readline.completion_append_character = nil
|
||||
end
|
||||
results
|
||||
else
|
||||
@@ -939,6 +939,7 @@ class Console::CommandDispatcher::Stdapi::Fs
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -20,7 +20,7 @@ module InteractiveSqlClient
|
||||
#
|
||||
def _interact
|
||||
while self.interacting
|
||||
sql_input = reline_multiline
|
||||
sql_input = _multiline_with_fallback
|
||||
self.interacting = (sql_input[:status] != :exit)
|
||||
|
||||
if sql_input[:status] == :help
|
||||
@@ -65,21 +65,35 @@ module InteractiveSqlClient
|
||||
# noop
|
||||
end
|
||||
|
||||
# Get multi-line input support provided by Reline.
|
||||
def reline_multiline
|
||||
# Try getting multi-line input support provided by Reline, fall back to Readline.
|
||||
def _multiline_with_fallback
|
||||
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?
|
||||
|
||||
framework.history_manager.with_context(history_file: history_file , name: name) do
|
||||
# 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
|
||||
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
|
||||
|
||||
@@ -138,6 +152,28 @@ module InteractiveSqlClient
|
||||
{ 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
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
require 'pp'
|
||||
require 'rex/text/table'
|
||||
require 'erb'
|
||||
require 'readline'
|
||||
|
||||
module Rex
|
||||
module Ui
|
||||
@@ -312,7 +311,7 @@ module DispatcherShell
|
||||
# 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.
|
||||
::Reline.completion_append_character = nil
|
||||
::Readline.completion_append_character = nil
|
||||
end
|
||||
|
||||
if dirs.length == 0 && File.directory?(str)
|
||||
@@ -407,16 +406,11 @@ module DispatcherShell
|
||||
# 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
|
||||
# Reline.basic_word_break_characters variable being set to \x00
|
||||
# Readline.basic_word_break_characters variable being set to \x00
|
||||
#
|
||||
def tab_complete(str, opts: {})
|
||||
# Compatibility with how Readline worked before Reline
|
||||
if opts[:preposing]
|
||||
str = "#{opts[:preposing]}#{str}"
|
||||
end
|
||||
|
||||
::Reline.completion_append_character = ' '
|
||||
::Reline.completion_case_fold = false
|
||||
def tab_complete(str)
|
||||
::Readline.completion_append_character = ' '
|
||||
::Readline.completion_case_fold = false
|
||||
|
||||
# Check trailing whitespace so we can tell 'x' from 'x '
|
||||
str_match = str.match(/[^\\]([\\]{2})*\s+$/)
|
||||
@@ -430,7 +424,11 @@ module DispatcherShell
|
||||
|
||||
# Pop the last word and pass it to the real method
|
||||
result = tab_complete_stub(str, split_str)
|
||||
result&.uniq
|
||||
if result
|
||||
result.uniq
|
||||
else
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
# Performs tab completion of a command, if supported
|
||||
|
||||
@@ -8,7 +8,7 @@ begin
|
||||
|
||||
###
|
||||
#
|
||||
# This class implements standard input using Reline against
|
||||
# This class implements standard input using readline against
|
||||
# standard input. It supports tab completion.
|
||||
#
|
||||
###
|
||||
@@ -18,12 +18,16 @@ begin
|
||||
# Initializes the readline-aware Input instance for text.
|
||||
#
|
||||
def initialize(tab_complete_proc = nil)
|
||||
self.extend(::Reline)
|
||||
if(not Object.const_defined?('Readline'))
|
||||
require 'readline'
|
||||
end
|
||||
|
||||
self.extend(::Readline)
|
||||
|
||||
if tab_complete_proc
|
||||
::Reline.basic_word_break_characters = ""
|
||||
::Readline.basic_word_break_characters = ""
|
||||
@rl_saved_proc = with_error_handling(tab_complete_proc)
|
||||
::Reline.completion_proc = @rl_saved_proc
|
||||
::Readline.completion_proc = @rl_saved_proc
|
||||
end
|
||||
end
|
||||
|
||||
@@ -31,8 +35,8 @@ begin
|
||||
# Reattach the original completion proc
|
||||
#
|
||||
def reset_tab_completion(tab_complete_proc = nil)
|
||||
::Reline.basic_word_break_characters = "\x00"
|
||||
::Reline.completion_proc = tab_complete_proc ? with_error_handling(tab_complete_proc) : @rl_saved_proc
|
||||
::Readline.basic_word_break_characters = "\x00"
|
||||
::Readline.completion_proc = tab_complete_proc ? with_error_handling(tab_complete_proc) : @rl_saved_proc
|
||||
end
|
||||
|
||||
|
||||
@@ -40,7 +44,11 @@ begin
|
||||
# Retrieve the line buffer
|
||||
#
|
||||
def line_buffer
|
||||
::Reline.line_buffer
|
||||
if defined? RbReadline
|
||||
RbReadline.rl_line_buffer
|
||||
else
|
||||
::Readline.line_buffer
|
||||
end
|
||||
end
|
||||
|
||||
attr_accessor :prompt
|
||||
@@ -89,7 +97,7 @@ begin
|
||||
|
||||
output.prompting
|
||||
line = readline_with_output(prompt, true)
|
||||
::Reline::HISTORY.pop if (line and line.empty?)
|
||||
::Readline::HISTORY.pop if (line and line.empty?)
|
||||
ensure
|
||||
Thread.current.priority = orig || 0
|
||||
end
|
||||
@@ -124,8 +132,13 @@ begin
|
||||
private
|
||||
|
||||
def readline_with_output(prompt, add_history=false)
|
||||
# Output needs to be set so that colorization works for the prompt on Windows.
|
||||
|
||||
# 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
|
||||
@@ -140,17 +153,38 @@ begin
|
||||
=end
|
||||
reset_sequence = ""
|
||||
|
||||
::Reline.input = fd
|
||||
::Reline.output = output
|
||||
if defined? RbReadline
|
||||
RbReadline.rl_instream = fd
|
||||
RbReadline.rl_outstream = output
|
||||
|
||||
line = ::Reline.readline(reset_sequence + prompt, add_history)
|
||||
begin
|
||||
line = RbReadline.readline(reset_sequence + prompt)
|
||||
rescue ::Exception => exception
|
||||
RbReadline.rl_cleanup_after_signal()
|
||||
RbReadline.rl_deprep_terminal()
|
||||
|
||||
# Don't add duplicate lines to history
|
||||
if ::Reline::HISTORY.length > 1 && line == ::Reline::HISTORY[-2]
|
||||
::Reline::HISTORY.pop
|
||||
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
|
||||
|
||||
line
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -39,18 +39,21 @@ class IrbShell
|
||||
# commands will work.
|
||||
IRB.conf[:MAIN_CONTEXT] = irb.context
|
||||
|
||||
begin
|
||||
old_sigint = trap("SIGINT") do
|
||||
# Trap interrupt
|
||||
old_sigint = trap("SIGINT") do
|
||||
begin
|
||||
irb.signal_handle
|
||||
end
|
||||
|
||||
# Keep processing input until the cows come home...
|
||||
catch(:IRB_EXIT) do
|
||||
rescue RubyLex::TerminateLineInput
|
||||
irb.eval_input
|
||||
end
|
||||
ensure
|
||||
trap("SIGINT", old_sigint) if old_sigint
|
||||
end
|
||||
|
||||
# Keep processing input until the cows come home...
|
||||
catch(:IRB_EXIT) do
|
||||
irb.eval_input
|
||||
end
|
||||
|
||||
trap("SIGINT", old_sigint)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -94,7 +94,6 @@ class Output::Stdio < Rex::Ui::Text::Output
|
||||
msg
|
||||
end
|
||||
alias_method :write, :print_raw
|
||||
alias_method :<<, :write
|
||||
|
||||
def supports_color?
|
||||
case config[:color]
|
||||
|
||||
@@ -66,10 +66,7 @@ module Shell
|
||||
def init_tab_complete
|
||||
if (self.input and self.input.supports_readline)
|
||||
# Unless cont_flag because there's no tab complete for continuation lines
|
||||
tab_complete_lambda = proc do |str, preposing = nil, postposing = nil|
|
||||
next tab_complete(str, opts: { preposing: preposing, postposing: postposing }).map { |result| result[preposing.to_s.length..] } unless cont_flag
|
||||
end
|
||||
self.input = Input::Readline.new(tab_complete_lambda)
|
||||
self.input = Input::Readline.new(lambda { |str| tab_complete(str) unless cont_flag })
|
||||
self.input.output = self.output
|
||||
end
|
||||
end
|
||||
@@ -117,8 +114,8 @@ module Shell
|
||||
#
|
||||
# Performs tab completion on the supplied string.
|
||||
#
|
||||
def tab_complete(str, opts: {})
|
||||
tab_complete_proc(str, opts: opts) if tab_complete_proc
|
||||
def tab_complete(str)
|
||||
return tab_complete_proc(str) if (tab_complete_proc)
|
||||
end
|
||||
|
||||
#
|
||||
@@ -307,13 +304,13 @@ module Shell
|
||||
|
||||
begin
|
||||
history_manager.with_context(history_file: histfile, name: name) do
|
||||
self.hist_last_saved = ::Reline::HISTORY.length
|
||||
self.hist_last_saved = Readline::HISTORY.length
|
||||
|
||||
yield
|
||||
end
|
||||
ensure
|
||||
history_manager.flush
|
||||
self.hist_last_saved = ::Reline::HISTORY.length
|
||||
self.hist_last_saved = Readline::HISTORY.length
|
||||
end
|
||||
end
|
||||
|
||||
@@ -513,6 +510,7 @@ module Shell
|
||||
attr_reader :cont_flag # :nodoc:
|
||||
attr_accessor :name
|
||||
private
|
||||
|
||||
def try_exec(command)
|
||||
begin
|
||||
%x{ #{ command } }
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# -*- coding: binary -*-
|
||||
|
||||
require 'singleton'
|
||||
require 'reline'
|
||||
|
||||
module Rex
|
||||
module Ui
|
||||
@@ -25,11 +24,12 @@ class HistoryManager
|
||||
#
|
||||
# @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, &block)
|
||||
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)
|
||||
push_context(history_file: history_file, name: name, input_library: input_library || :readline)
|
||||
|
||||
begin
|
||||
block.call
|
||||
@@ -73,9 +73,33 @@ class HistoryManager
|
||||
@debug
|
||||
end
|
||||
|
||||
def push_context(history_file: nil, name: nil)
|
||||
# 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
|
||||
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
|
||||
end
|
||||
|
||||
def push_context(history_file: nil, name: nil, input_library: nil)
|
||||
$stderr.puts("Push context before\n#{JSON.pretty_generate(_contexts)}") if debug?
|
||||
new_context = { history_file: history_file, name: name }
|
||||
new_context = { history_file: history_file, name: name, input_library: input_library || :readline }
|
||||
|
||||
switch_context(new_context, @contexts.last)
|
||||
@contexts.push(new_context)
|
||||
@@ -95,18 +119,40 @@ class HistoryManager
|
||||
nil
|
||||
end
|
||||
|
||||
def clear_history
|
||||
history.clear
|
||||
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 }
|
||||
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_history
|
||||
clear_library(context[:input_library])
|
||||
f.each_line(chomp: true) do |line|
|
||||
if history.last&.end_with?("\\")
|
||||
if context[:input_library] == :reline && history.last&.end_with?("\\")
|
||||
history.last.delete_suffix!("\\")
|
||||
history.last << "\n" << line
|
||||
else
|
||||
@@ -121,6 +167,8 @@ class HistoryManager
|
||||
|
||||
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
|
||||
|
||||
cmds = []
|
||||
@@ -140,10 +188,12 @@ class HistoryManager
|
||||
if new_context && new_context[:history_file]
|
||||
load_history_file(new_context)
|
||||
else
|
||||
clear_history
|
||||
clear_readline
|
||||
clear_reline
|
||||
end
|
||||
rescue SignalException => _e
|
||||
clear_history
|
||||
clear_readline
|
||||
clear_reline
|
||||
end
|
||||
|
||||
def write_history_file(history_file, cmds)
|
||||
@@ -174,10 +224,6 @@ class HistoryManager
|
||||
@write_queue << event
|
||||
@remaining_work << event
|
||||
end
|
||||
|
||||
def history
|
||||
::Reline::HISTORY
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -197,8 +197,6 @@ 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'
|
||||
@@ -250,6 +248,9 @@ 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
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
require 'reline'
|
||||
require 'readline'
|
||||
|
||||
class MetasploitModule < Msf::Post
|
||||
include Msf::Post::File
|
||||
@@ -104,9 +104,9 @@ class MetasploitModule < Msf::Post
|
||||
def prompt_show
|
||||
promptshell = "#{@vusername}@#{@vhostname}:#{pwd.strip}#{@vpromptchar} "
|
||||
comp = proc { |s| LIST.grep(/^#{Regexp.escape(s)}/) }
|
||||
Reline.completion_append_character = ' '
|
||||
Reline.completion_proc = comp
|
||||
input = Reline.readline(promptshell, true)
|
||||
Readline.completion_append_character = ' '
|
||||
Readline.completion_proc = comp
|
||||
input = Readline.readline(promptshell, true)
|
||||
return nil if input.nil?
|
||||
|
||||
input
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
#
|
||||
|
||||
require 'pathname'
|
||||
require 'reline'
|
||||
|
||||
begin
|
||||
|
||||
# Silences warnings as they only serve to confuse end users
|
||||
|
||||
@@ -212,7 +212,7 @@ RSpec.describe Msf::Ui::Debug do
|
||||
end
|
||||
|
||||
it 'correctly retrieves and parses a command history shorter than the command total' do
|
||||
stub_const('Reline::HISTORY', Array.new(4) { |i| "Command #{i + 1}" })
|
||||
stub_const('Readline::HISTORY', Array.new(4) { |i| "Command #{i + 1}" })
|
||||
|
||||
driver = instance_double(
|
||||
Msf::Ui::Console::Driver,
|
||||
@@ -251,7 +251,7 @@ RSpec.describe Msf::Ui::Debug do
|
||||
|
||||
stub_const('Msf::Ui::Debug::COMMAND_HISTORY_TOTAL', 10)
|
||||
|
||||
stub_const('Reline::HISTORY', Array.new(10) { |i| "Command #{i + 1}" })
|
||||
stub_const('Readline::HISTORY', Array.new(10) { |i| "Command #{i + 1}" })
|
||||
|
||||
history_output = <<~E_LOG
|
||||
## %grnHistory%clr
|
||||
@@ -288,7 +288,7 @@ RSpec.describe Msf::Ui::Debug do
|
||||
)
|
||||
|
||||
stub_const('Msf::Ui::Debug::COMMAND_HISTORY_TOTAL', 10)
|
||||
stub_const('Reline::HISTORY', Array.new(15) { |i| "Command #{i + 1}" })
|
||||
stub_const('Readline::HISTORY', Array.new(15) { |i| "Command #{i + 1}" })
|
||||
|
||||
history_output = <<~E_LOG
|
||||
## %grnHistory%clr
|
||||
@@ -325,7 +325,7 @@ RSpec.describe Msf::Ui::Debug do
|
||||
)
|
||||
|
||||
stub_const('Msf::Ui::Debug::COMMAND_HISTORY_TOTAL', 10)
|
||||
stub_const('Reline::HISTORY', Array.new(15) { |i| "Command #{i + 1}" })
|
||||
stub_const('Readline::HISTORY', Array.new(15) { |i| "Command #{i + 1}" })
|
||||
|
||||
history_output = <<~E_LOG
|
||||
## %grnHistory%clr
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
require 'spec_helper'
|
||||
require 'reline'
|
||||
|
||||
require 'readline'
|
||||
|
||||
RSpec.describe Msf::Ui::Console::CommandDispatcher::Core do
|
||||
include_context 'Msf::DBManager'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
require 'spec_helper'
|
||||
require 'reline'
|
||||
require 'readline'
|
||||
|
||||
RSpec.describe Rex::Ui::Text::DispatcherShell do
|
||||
let(:prompt) { '%undmsf6%clr' }
|
||||
|
||||
@@ -7,6 +7,13 @@ RSpec.describe Rex::Ui::Text::Shell::HistoryManager do
|
||||
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
|
||||
@@ -23,7 +30,7 @@ RSpec.describe Rex::Ui::Text::Shell::HistoryManager do
|
||||
(expect do |block|
|
||||
subject.with_context(name: 'a') do
|
||||
expected_contexts = [
|
||||
{ history_file: nil, name: 'a' },
|
||||
{ history_file: nil, input_library: :readline, name: 'a' },
|
||||
]
|
||||
expect(subject._contexts).to eq(expected_contexts)
|
||||
block.to_proc.call
|
||||
@@ -34,7 +41,7 @@ RSpec.describe Rex::Ui::Text::Shell::HistoryManager do
|
||||
|
||||
context 'when there is an existing stack' do
|
||||
before(:each) do
|
||||
subject.send(:push_context, history_file: nil, name: 'a')
|
||||
subject.send(:push_context, history_file: nil, input_library: :readline, name: 'a')
|
||||
end
|
||||
|
||||
it 'continues to have the previous existing stack' do
|
||||
@@ -42,7 +49,7 @@ RSpec.describe Rex::Ui::Text::Shell::HistoryManager do
|
||||
# noop
|
||||
}
|
||||
expected_contexts = [
|
||||
{ history_file: nil, name: 'a' },
|
||||
{ history_file: nil, input_library: :readline, name: 'a' },
|
||||
]
|
||||
expect(subject._contexts).to eq(expected_contexts)
|
||||
end
|
||||
@@ -51,8 +58,8 @@ RSpec.describe Rex::Ui::Text::Shell::HistoryManager do
|
||||
(expect do |block|
|
||||
subject.with_context(name: 'b') do
|
||||
expected_contexts = [
|
||||
{ history_file: nil, name: 'a' },
|
||||
{ history_file: nil, name: 'b' },
|
||||
{ history_file: nil, input_library: :readline, name: 'a' },
|
||||
{ history_file: nil, input_library: :readline, name: 'b' },
|
||||
]
|
||||
expect(subject._contexts).to eq(expected_contexts)
|
||||
block.to_proc.call
|
||||
@@ -67,7 +74,7 @@ RSpec.describe Rex::Ui::Text::Shell::HistoryManager do
|
||||
}
|
||||
end.to raise_exception ArgumentError, 'Mock error'
|
||||
expected_contexts = [
|
||||
{ history_file: nil, name: 'a' },
|
||||
{ history_file: nil, input_library: :readline, name: 'a' },
|
||||
]
|
||||
expect(subject._contexts).to eq(expected_contexts)
|
||||
end
|
||||
@@ -77,9 +84,9 @@ RSpec.describe Rex::Ui::Text::Shell::HistoryManager do
|
||||
describe '#push_context' do
|
||||
context 'when the stack is empty' 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: 'a')
|
||||
expected_contexts = [
|
||||
{ history_file: nil, name: 'a' }
|
||||
{ history_file: nil, input_library: :readline, name: 'a' }
|
||||
]
|
||||
expect(subject._contexts).to eq(expected_contexts)
|
||||
end
|
||||
@@ -88,12 +95,12 @@ RSpec.describe Rex::Ui::Text::Shell::HistoryManager do
|
||||
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, name: 'b')
|
||||
subject.send(:push_context, history_file: nil, name: 'c')
|
||||
subject.send(:push_context, history_file: nil, input_library: :readline, name: 'b')
|
||||
subject.send(:push_context, history_file: nil, input_library: :reline, name: 'c')
|
||||
expected_contexts = [
|
||||
{ history_file: nil, name: 'a' },
|
||||
{ history_file: nil, name: 'b' },
|
||||
{ history_file: nil, name: 'c' },
|
||||
{ history_file: nil, input_library: :readline, name: 'a' },
|
||||
{ history_file: nil, input_library: :readline, name: 'b' },
|
||||
{ history_file: nil, input_library: :reline, name: 'c' },
|
||||
]
|
||||
expect(subject._contexts).to eq(expected_contexts)
|
||||
end
|
||||
@@ -116,7 +123,7 @@ RSpec.describe Rex::Ui::Text::Shell::HistoryManager do
|
||||
subject.send(:push_context, history_file: nil, name: 'b')
|
||||
subject.send(:pop_context)
|
||||
expected_contexts = [
|
||||
{ history_file: nil, name: 'a' },
|
||||
{ history_file: nil, input_library: :readline, name: 'a' },
|
||||
]
|
||||
expect(subject._contexts).to eq(expected_contexts)
|
||||
end
|
||||
@@ -143,14 +150,14 @@ RSpec.describe Rex::Ui::Text::Shell::HistoryManager do
|
||||
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(:history).and_return(history_mock)
|
||||
allow(subject).to receive(:map_library_to_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 = { history_file: history_file.path, name: 'history' }
|
||||
context = { input_library: :readline, history_file: history_file.path, name: 'history'}
|
||||
|
||||
subject.send(:store_history_file, context)
|
||||
|
||||
@@ -169,16 +176,15 @@ RSpec.describe Rex::Ui::Text::Shell::HistoryManager do
|
||||
let(:history_file) { ::Tempfile.new('history') }
|
||||
|
||||
after(:each) do
|
||||
history_file.close
|
||||
history_file.unlink
|
||||
subject._close
|
||||
end
|
||||
|
||||
context 'when history file is not accessible' do
|
||||
it 'the library history remains unchanged' do
|
||||
allow(subject).to receive(:history).and_return(history_mock)
|
||||
allow(subject).to receive(:map_library_to_history).and_return(history_mock)
|
||||
history_file = ::File.join('does', 'not', 'exist', 'history')
|
||||
context = { history_file: history_file, name: 'history' }
|
||||
context = { input_library: :readline, history_file: history_file, name: 'history' }
|
||||
|
||||
subject.send(:load_history_file, context)
|
||||
expect(history_mock).to eq(initial_history)
|
||||
@@ -187,7 +193,7 @@ RSpec.describe Rex::Ui::Text::Shell::HistoryManager do
|
||||
|
||||
context 'when history file is accessible' do
|
||||
it 'correctly loads the history' do
|
||||
allow(subject).to receive(:history).and_return(history_mock)
|
||||
allow(subject).to receive(:map_library_to_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.
|
||||
@@ -198,7 +204,7 @@ RSpec.describe Rex::Ui::Text::Shell::HistoryManager do
|
||||
history_file.puts new_history
|
||||
history_file.rewind
|
||||
|
||||
context = { history_file: history_file.path, name: 'history' }
|
||||
context = { input_library: :readline, history_file: history_file.path, name: 'history' }
|
||||
|
||||
subject.send(:load_history_file, context)
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ require 'msfenv'
|
||||
$:.unshift(ENV['MSF_LOCAL_LIB']) if ENV['MSF_LOCAL_LIB']
|
||||
|
||||
require 'rex'
|
||||
require 'reline'
|
||||
require 'readline'
|
||||
require 'metasm'
|
||||
|
||||
#PowerPC, seems broken for now in metasm
|
||||
|
||||
@@ -21,7 +21,7 @@ $:.unshift(ENV['MSF_LOCAL_LIB']) if ENV['MSF_LOCAL_LIB']
|
||||
|
||||
require 'msfenv'
|
||||
require 'rex'
|
||||
require 'reline'
|
||||
require 'readline'
|
||||
|
||||
# Check to make sure nasm is installed and reachable through the user's PATH.
|
||||
begin
|
||||
|
||||
Reference in New Issue
Block a user