Skip to content

Commit

Permalink
WIP: Replace Readline with Reline
Browse files Browse the repository at this point in the history
  • Loading branch information
sjanusz-r7 committed Aug 15, 2024
1 parent 233f6dc commit 83a07c5
Show file tree
Hide file tree
Showing 24 changed files with 141 additions and 276 deletions.
1 change: 0 additions & 1 deletion lib/metasploit/framework/command/console.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 5 additions & 2 deletions lib/metasploit/framework/parsed_options/console.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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|
Expand Down
1 change: 0 additions & 1 deletion lib/metasploit/framework/parsed_options/remote_db.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
16 changes: 10 additions & 6 deletions lib/msf/ui/console/command_dispatcher/core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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
4 changes: 4 additions & 0 deletions lib/msf/ui/console/command_dispatcher/developer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
56 changes: 6 additions & 50 deletions lib/msf/ui/console/driver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>] 'Resources' ([]) A list of resource files to
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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

#
Expand Down Expand Up @@ -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

Expand Down
12 changes: 9 additions & 3 deletions lib/msf/ui/console/module_option_tab_completion.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -369,6 +369,12 @@ def option_values_target_ports(mod)

res.uniq
end

private

def input_library
::Reline
end
end
end
end
Expand Down
8 changes: 6 additions & 2 deletions lib/msf/ui/debug.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -940,6 +940,11 @@ def unexpand_path_for_suggestions(original_path, expanded_path, suggestions)
end
end

private

def input_library
::Reline
end
end

end
Expand Down
44 changes: 4 additions & 40 deletions lib/rex/post/sql/ui/console/interactive_sql_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions lib/rex/ui/text/dispatcher_shell.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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+$/)
Expand Down
Loading

0 comments on commit 83a07c5

Please sign in to comment.