Skip to content

Commit

Permalink
Place Reline behind a feature flag
Browse files Browse the repository at this point in the history
  • Loading branch information
sjanusz-r7 committed Nov 20, 2024
1 parent e6615d3 commit c743203
Show file tree
Hide file tree
Showing 27 changed files with 261 additions and 162 deletions.
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@ GEM
nokogiri
redcarpet (3.6.0)
regexp_parser (2.9.2)
reline (0.5.10)
reline (0.5.11)
io-console (~> 0.5)
require_all (3.0.0)
rex-arch (0.1.16)
Expand Down
6 changes: 4 additions & 2 deletions lib/msf/base/sessions/command_shell.rb
Original file line number Diff line number Diff line change
Expand Up @@ -547,9 +547,11 @@ def cmd_irb(*args)
if expressions.empty?
print_status('Starting IRB shell...')
print_status("You are in the \"self\" (session) object\n")
framework.history_manager.with_context(name: :irb) do
Msf::Ui::Console::MsfReadline.instance.cache_current_config
framework.history_manager.with_context(name: :irb, input_library: :reline) do
Rex::Ui::Text::IrbShell.new(self).run
end
Msf::Ui::Console::MsfReadline.instance.restore_cached_config
else
# XXX: No vprint_status here
if framework.datastore['VERBOSE'].to_s == 'true'
Expand Down Expand Up @@ -586,7 +588,7 @@ def cmd_pry(*args)
print_status('Starting Pry shell...')
print_status("You are in the \"self\" (session) object\n")
Pry.config.history_load = false
framework.history_manager.with_context(history_file: Msf::Config.pry_history, name: :pry) do
framework.history_manager.with_context(history_file: Msf::Config.pry_history, name: :pry, input_library: Pry.input) do
self.pry
end
end
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 @@ -29,6 +29,7 @@ class FeatureManager
LDAP_SESSION_TYPE = 'ldap_session_type'
SHOW_SUCCESSFUL_LOGINS = 'show_successful_logins'
DISPLAY_MODULE_ACTION = 'display_module_action'
USE_RELINE = 'use_reline'

DEFAULTS = [
{
Expand Down Expand Up @@ -132,6 +133,13 @@ class FeatureManager
requires_restart: false,
default_value: true,
developer_notes: 'Added as a feature so users can turn it off if they wish to reduce clutter in their terminal'
}.freeze,
{
name: USE_RELINE,
description: 'When enabled, the new Reline library will be used instead of the legacy Readline library for input/output.',
requires_restart: true,
default_value: false,
developer_notes: 'To be enabled by default after sufficient testing and Reline fixes the issues raised here: https://github.com/ruby/reline/issues/created_by/sjanusz-r7'
}.freeze
].freeze

Expand Down
13 changes: 7 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::MsfReadline.instance.history
length = history.length

if length < @history_limit
limit = length
Expand All @@ -780,10 +781,10 @@ def cmd_history(*args)
limit = val.to_i
end
when '-c'
if Readline::HISTORY.respond_to?(:clear)
Readline::HISTORY.clear
elsif defined?(RbReadline)
RbReadline.clear_history
if history.respond_to?(:clear)
history.clear
elsif history.respond_to?(:pop) && history.respond_to?(:length)
history.length.times { |_i| history.pop }
else
print_error('Could not clear history, skipping file')
return false
Expand All @@ -808,7 +809,7 @@ def cmd_history(*args)

(start..length-1).each do |pos|
cmd_num = (pos + 1).to_s
print_line "#{cmd_num.ljust(pad_len)} #{Readline::HISTORY[pos]}"
print_line "#{cmd_num.ljust(pad_len)} #{history[pos]}"
end
end

Expand Down
11 changes: 9 additions & 2 deletions lib/msf/ui/console/command_dispatcher/developer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,9 @@ def cmd_irb(*args)
if expressions.empty?
print_status('Starting IRB shell...')

framework.history_manager.with_context(name: :irb) do
framework.history_manager.with_context(name: :irb, input_library: :reline) do
begin
Msf::Ui::Console::MsfReadline.instance.cache_current_config
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
Msf::Ui::Console::MsfReadline.instance.restore_cached_config
end
end

Expand Down Expand Up @@ -192,7 +195,7 @@ def cmd_pry(*args)
print_status('Starting Pry shell...')

Pry.config.history_load = false
framework.history_manager.with_context(history_file: Msf::Config.pry_history, name: :pry) do
framework.history_manager.with_context(history_file: Msf::Config.pry_history, name: :pry, input_library: Pry.input) do
if active_module
print_status("You are in the \"#{active_module.fullname}\" module object\n")
active_module.pry
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
6 changes: 4 additions & 2 deletions lib/msf/ui/console/command_dispatcher/session.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,11 @@ def cmd_irb(*args)
if expressions.empty?
print_status('Starting IRB shell...')
print_status("You are in the session object\n")
framework.history_manager.with_context(name: :irb) do
Msf::Ui::Console::MsfReadline.instance.cache_current_config
framework.history_manager.with_context(name: :irb, input_library: :reline) do
Rex::Ui::Text::IrbShell.new(session).run
end
Msf::Ui::Console::MsfReadline.instance.restore_cached_config
else
# XXX: No vprint_status here
if framework.datastore['VERBOSE'].to_s == 'true'
Expand Down Expand Up @@ -136,7 +138,7 @@ def cmd_pry(*args)
print_status("You are in the session object\n")

Pry.config.history_load = false
session.framework.history_manager.with_context(history_file: Msf::Config.pry_history, name: :pry) do
session.framework.history_manager.with_context(history_file: Msf::Config.pry_history, name: :pry, input_library: Pry.input) do
session.pry
end
end
Expand Down
11 changes: 4 additions & 7 deletions lib/msf/ui/console/driver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -313,12 +313,9 @@ def save_config
# Saves the recent history to the specified file
#
def save_recent_history(path)
num = Readline::HISTORY.length - hist_last_saved - 1

tmprc = ""
num.times { |x|
tmprc << Readline::HISTORY[hist_last_saved + x] + "\n"
}
history = Msf::Ui::Console::MsfReadline.instance.history
num = history.length - hist_last_saved - 1
tmprc = history.entries[hist_last_saved..].join("\n")

if tmprc.length > 0
print_status("Saving last #{num} commands to #{path} ...")
Expand All @@ -329,7 +326,7 @@ def save_recent_history(path)

# Always update this, even if we didn't save anything. We do this
# so that we don't end up saving the "makerc" command itself.
self.hist_last_saved = Readline::HISTORY.length
self.hist_last_saved = history.length
end

#
Expand Down
6 changes: 3 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 = ' '
Msf::Ui::Console::MsfReadline.instance.completion_append_character = ' '
return tab_complete_option_values(mod, option_value, words, opt: option_name).map { |value| "#{str}#{value}" }
elsif str.include?('=')
str_split = str.split('=')
option_name = str_split[0].strip
option_value = str_split[1].strip

::Readline.completion_append_character = ' '
Msf::Ui::Console::MsfReadline.instance.completion_append_character = ' '
return tab_complete_option_values(mod, option_value, words, opt: option_name).map { |value| "#{option_name}=#{value}" }
end

::Readline.completion_append_character = ''
Msf::Ui::Console::MsfReadline.instance.completion_append_character = ''
tab_complete_option_names(mod, str, words).map { |name| "#{name}=" }
end

Expand Down
127 changes: 127 additions & 0 deletions lib/msf/ui/console/msf_readline.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#
# This class is responsible for handling Readline/Reline-agnostic user input.
#
class Msf::Ui::Console::MsfReadline
require 'singleton'
# Required to check the Reline flag.
require 'msf/core/feature_manager'
require 'readline'
require 'reline'

include Singleton

attr_reader :history

def initialize
@backend = using_reline? ? ::Reline : ::Readline
@history = @backend::HISTORY
end

def method_missing(sym, *args, &block)
if @backend.respond_to?(sym)
@backend.send(sym, *args, &block)
else
msg = "Method '#{sym}' not found in #{@backend.class}"
elog(msg)
raise NoMethodError, msg
end
end

# IRB changes propagate out of the context of IRB. We store the current state and restore it on exit.
# TODO: Once IRB fixes this behaviour, we should be able to remove this patch.
def cache_current_config
@current_config = {}
@current_config[:autocompletion] = self.autocompletion
@current_config[:core] = @backend.core.dup if using_reline?
end

def restore_cached_config
self.autocompletion = @current_config[:autocompletion]
@backend.instance_variable_set(:@core, @current_config[:core]) if using_reline?
end

# Read a line from the user, and return it.
# @param prompt [String] The prompt to show to the user.
# @param add_history [Boolean] True if the user's input should be saved to the history.
# @param opts [Hash] Options
# @return String The line that the user has entered.
def readline(prompt, add_history = false, opts: {})
input(prompt, add_history, opts: opts)
end

def using_reline?
return @using_reline unless @using_reline.nil?

@using_reline = Msf::FeatureManager.instance.enabled?(Msf::FeatureManager::USE_RELINE)
end

private

attr_accessor :using_reline, :backend, :current_config

def input(*args, opts: {})
using_reline? ? input_reline(*args, opts: opts) : input_rbreadline(*args, opts: opts)
end

def input_rbreadline(prompt, add_history = false, opts: {})
# rb-readlines's Readline.readline hardcodes the input and output to
# $stdin and $stdout, which means setting `Readline.input` or
# `Readline.output` has no effect when running `Readline.readline` with
# rb-readline, so need to reimplement
# []`Readline.readline`](https://github.com/luislavena/rb-readline/blob/ce4908dae45dbcae90a6e42e3710b8c3a1f2cd64/lib/readline.rb#L36-L58)
# for rb-readline to support setting input and output. Output needs to
# be set so that colorization works for the prompt on Windows.

input_on_entry = RbReadline.rl_instream
output_on_entry = RbReadline.rl_outstream

begin
RbReadline.rl_instream = opts[:fd]
RbReadline.rl_outstream = opts[:output]
line = RbReadline.readline(prompt.to_s)
rescue ::StandardError => e
RbReadline.rl_instream = input_on_entry
RbReadline.rl_outstream = output_on_entry
RbReadline.rl_cleanup_after_signal
RbReadline.rl_deprep_terminal

raise e
end

if add_history && line && !line.start_with?(' ')
# Don't add duplicate lines to history
if ::Readline::HISTORY.empty? || line.strip != ::Readline::HISTORY[-1]
RbReadline.add_history(line.strip)
end
end

line.dup
end

def input_reline(prompt, add_history = false, opts: {})
input_on_entry = Reline::IOGate.instance_variable_get(:@input)
output_on_entry = Reline::IOGate.instance_variable_get(:@output)

begin
# TODO: Currently we can't pase in non-ASCII chars. The code below fixes that but breaks out rab completion due
# to encoding issues.
# input_external_encoding = opts[:fd].external_encoding
# input_internal_encoding = opts[:fd].internal_encoding
# opts[:fd].set_encoding(::Encoding::UTF_8)
Reline.input = opts[:fd]
Reline.output = opts[:output]
line = Reline.readline(prompt.to_s, add_history)
ensure
# opts[:fd].set_encoding(input_external_encoding, input_internal_encoding)
Reline.input = input_on_entry
Reline.output = output_on_entry
end

# Don't add duplicate lines to history
if Reline::HISTORY.length > 1 && line == Reline::HISTORY[-2]
Reline::HISTORY.pop
end

line.dup
end
end
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
history = Msf::Ui::Console::MsfReadline.instance.history
end_pos = history.length - 1
start_pos = end_pos - COMMAND_HISTORY_TOTAL > driver.hist_last_saved ? end_pos - (COMMAND_HISTORY_TOTAL - 1) : driver.hist_last_saved

commands = ''
while start_pos <= end_pos
# Formats command position in history to 6 characters in length
commands += "#{'%-6.6s' % start_pos.to_s} #{Readline::HISTORY[start_pos]}\n"
commands += "#{'%-6.6s' % start_pos.to_s} #{history[start_pos]}\n"
start_pos += 1
end

Expand Down
14 changes: 14 additions & 0 deletions lib/msf_autoload.rb
Original file line number Diff line number Diff line change
Expand Up @@ -347,3 +347,17 @@ def finalize_loader(loader)

# XXX: Should be removed once the `lib/metasploit` folder is loaded by Zeitwerk
require 'metasploit/framework/hashes'

# TODO: Remove this monkey-patch once the following issue is addressed over on Reline's repository here:
# https://github.com/ruby/reline/issues/756
require 'reline'
class ::Reline::Core
alias old_completion_append_character= completion_append_character=
alias old_completion_append_character completion_append_character

def completion_append_character=(v)
self.old_completion_append_character = v
# Additionally keep the line_editor in sync
line_editor.completion_append_character = self.old_completion_append_character
end
end
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
Msf::Ui::Console::MsfReadline.instance.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
Msf::Ui::Console::MsfReadline.instance.completion_append_character = nil
end
results
else
Expand Down
2 changes: 1 addition & 1 deletion lib/rex/post/sql/ui/console/interactive_sql_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def _multiline_with_fallback
return { status: :fail, errors: ["Unable to get history file for session type: #{name}"] } if history_file.nil?

# Multiline (Reline) and fallback (Readline) have separate history contexts as they are two different libraries.
framework.history_manager.with_context(history_file: history_file , name: name, input_library: :reline) do
framework.history_manager.with_context(history_file: history_file, name: name, input_library: :reline) do
query = _multiline
end

Expand Down
Loading

0 comments on commit c743203

Please sign in to comment.