Skip to content

Commit

Permalink
wip: Reline behind a feature flag
Browse files Browse the repository at this point in the history
  • Loading branch information
sjanusz-r7 committed Oct 15, 2024
1 parent d2b4175 commit a69e629
Show file tree
Hide file tree
Showing 21 changed files with 196 additions and 162 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 use the following link to tell us:\n"
message << ' https://github.com/rapid7/metasploit-framework/issues/19399'
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
8 changes: 8 additions & 0 deletions lib/msf/core/feature_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class FeatureManager
MSSQL_SESSION_TYPE = 'mssql_session_type'
LDAP_SESSION_TYPE = 'ldap_session_type'
SHOW_SUCCESSFUL_LOGINS = 'show_successful_logins'
USE_RELINE = 'use_reline'

DEFAULTS = [
{
Expand Down Expand Up @@ -124,6 +125,13 @@ class FeatureManager
requires_restart: false,
default_value: true,
developer_notes: 'Enabled in Metasploit 6.4.x'
}.freeze,
{
name: USE_RELINE,
description: 'When enabled, the new Reline library will be used instead of the legacy Readline library for input/output. New UI sessions will be using Reline, but sessions that existed before this feature has been set will continue to be Readline-based.',
requires_restart: false,
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

Expand Down
15 changes: 9 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,8 @@ def cmd_features_tabs(_str, words)
end

def cmd_history(*args)
length = Readline::HISTORY.length
history = Msf::Ui::Console::Driver.input_lib::HISTORY
length = history.length

if length < @history_limit
limit = length
Expand All @@ -780,10 +781,12 @@ 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?(:clear_history)
history.clear_history
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
Expand All @@ -808,7 +811,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
7 changes: 7 additions & 0 deletions lib/msf/ui/console/command_dispatcher/developer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ def cmd_irb(*args)

framework.history_manager.with_context(name: :irb) do
begin
reline_autocomplete = Reline.autocompletion if defined?(Reline)
if active_module
print_status("You are in #{active_module.fullname}\n")
Rex::Ui::Text::IrbShell.new(active_module).run
Expand All @@ -140,6 +141,8 @@ def cmd_irb(*args)
end
rescue
print_error("Error during IRB: #{$!}\n\n#{$@.join("\n")}")
ensure
Reline.autocompletion = reline_autocomplete if defined?(Reline)
end
end

Expand Down Expand Up @@ -515,6 +518,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
100 changes: 47 additions & 53 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 @@ -234,6 +222,49 @@ def initialize(prompt = DefaultPrompt, prompt_char = DefaultPromptChar, opts = {
end
end

def self.windows_legacy_console_deprecated?
# We can't need a Windows-specific workaround if we're not on Windows.
return false unless Rex::Compat::is_windows || Rex::Compat::is_cygwin

kernel32_path = Pathname.new(File.join('C:', 'Windows', 'System32', 'Kernel32.dll')).expand_path
# This should never really happen. If it does, we have a bigger problem than legacy console support.
return false unless kernel32_path.exist?

@@windows_calls_mutex ||= Mutex.new
@@windows_calls_mutex.synchronize do
# Windows 11 has deprecated legacy console support.
# Check if some relevant functions return an error, or an invalid value.
@@kernel32_handle ||= Fiddle.dlopen(kernel32_path)
@@get_std_handle ||= Fiddle::Function.new(@@kernel32_handle['GetStdHandle'], [Fiddle::TYPE_LONG], Fiddle::TYPE_LONG)
std_input_handle = -10
# It is not required to call CloseHandle on this handle: https://learn.microsoft.com/en-us/windows/console/getstdhandle
@@console_input_handle = @@get_std_handle.Call(std_input_handle)
@@get_console_mode ||= Fiddle::Function.new(@@kernel32_handle['GetConsoleMode'], [Fiddle::TYPE_LONG, Fiddle::TYPE_INTPTR_T], Fiddle::TYPE_LONG)

# The Fiddle::Pointer object will be freed at the end of this block, so we don't leak memory.
Fiddle::Pointer.malloc(4) do |console_mode_ptr|
console_mode_result = @@get_console_mode.Call(@@console_input_handle, console_mode_ptr)

# The function returns 0 on error, non-zero on success: https://learn.microsoft.com/en-us/windows/console/getconsolemode
# We know that if this fails, the legacy console support is not available, e.g. Windows 11, so use Reline.
return true if console_mode_result == 0 || console_mode_ptr.to_s(2) == "\x00\x00"
end

false
end
end

def self.using_reline?
return true if Msf::FeatureManager.instance.enabled?(Msf::FeatureManager::USE_RELINE)
return true if self.windows_legacy_console_deprecated?

false
end

def self.input_lib
self.using_reline? ? ::Reline : ::Readline
end

#
# Loads configuration that needs to be analyzed before the framework
# instance is created.
Expand Down Expand Up @@ -323,11 +354,12 @@ def save_config
# Saves the recent history to the specified file
#
def save_recent_history(path)
num = Readline::HISTORY.length - hist_last_saved - 1
history = self.input_lib::HISTORY
num = history.length - hist_last_saved - 1

tmprc = ""
num.times { |x|
tmprc << Readline::HISTORY[hist_last_saved + x] + "\n"
tmprc << history[hist_last_saved + x] + "\n"
}

if tmprc.length > 0
Expand All @@ -339,7 +371,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

#
Expand Down Expand Up @@ -702,44 +734,6 @@ 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

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
Expand Down
7 changes: 4 additions & 3 deletions lib/msf/ui/console/module_option_tab_completion.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,22 +49,23 @@ def tab_complete_module_datastore_names(mod, str, words)
# Tab completion options values
#
def tab_complete_option(mod, str, words)
lib = Msf::Ui::Console::Driver.input_lib
if str.end_with?('=')
option_name = str.chop
option_value = ''

::Readline.completion_append_character = ' '
lib.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 = ' '
lib.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 = ''
lib.completion_append_character = ''
tab_complete_option_names(mod, str, words).map { |name| "#{name}=" }
end

Expand Down
5 changes: 3 additions & 2 deletions lib/msf/ui/debug.rb
Original file line number Diff line number Diff line change
Expand Up @@ -220,13 +220,14 @@ def self.framework_config(framework)
end

def self.history(driver)
end_pos = Readline::HISTORY.length - 1
lib = Msf::Ui::Console::Driver.input_lib
end_pos = lib::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} #{lib::HISTORY[start_pos]}\n"
start_pos += 1
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -901,8 +901,9 @@ def tab_complete_cdirectory(str, words)
end

def tab_complete_path(str, words, dir_only)
lib = Msf::Ui::Console::Driver.input_lib
if client.platform == 'windows'
::Readline.completion_case_fold = true
lib.completion_case_fold = true
end
if client.commands.include?(COMMAND_ID_STDAPI_FS_LS)
expanded = str
Expand All @@ -915,7 +916,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
lib.completion_append_character = nil
end
results
else
Expand Down
20 changes: 11 additions & 9 deletions lib/rex/ui/text/dispatcher_shell.rb
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,8 @@ 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
lib = Msf::Ui::Console::Driver.input_lib
lib.completion_append_character = nil
end

if dirs.length == 0 && File.directory?(str)
Expand Down Expand Up @@ -408,9 +409,14 @@ 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: {})
lib = Msf::Ui::Console::Driver.input_lib
lib.completion_append_character = ' '
lib.completion_case_fold = false

if opts[:preposing] && Msf::Ui::Console::Driver.using_reline?
str = "#{opts[:preposing]}#{str}"
end

# Check trailing whitespace so we can tell 'x' from 'x '
str_match = str.match(/[^\\]([\\]{2})*\s+$/)
Expand All @@ -424,11 +430,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
Expand Down
Loading

0 comments on commit a69e629

Please sign in to comment.