Skip to content

Commit d604d28

Browse files
authored
Land rapid7#19197, SQL sessions have correct history manager support
2 parents a9078b4 + adad32d commit d604d28

File tree

7 files changed

+255
-69
lines changed

7 files changed

+255
-69
lines changed

lib/msf/base/config.rb

Lines changed: 35 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ def self.get_config_root
7474
'PluginDirectory' => "plugins",
7575
'DataDirectory' => "data",
7676
'LootDirectory' => "loot",
77-
'LocalDirectory' => "local"
77+
'LocalDirectory' => "local",
78+
'HistoriesDirectory' => "histories"
7879
}
7980

8081
##
@@ -97,6 +98,13 @@ def self.config_directory
9798
self.new.config_directory
9899
end
99100

101+
# Returns the histories directory default.
102+
#
103+
# @return [String] the SQL session histories directory.
104+
def self.histories_directory
105+
self.new.histories_directory
106+
end
107+
100108
# Return the directory that logo files should be loaded from.
101109
#
102110
# @return [String] path to the logos directory.
@@ -228,25 +236,11 @@ def self.ldap_session_history
228236
self.new.ldap_session_history
229237
end
230238

231-
# Returns the full path to the PostgreSQL session history file.
232-
#
233-
# @return [String] path to the history file.
234-
def self.postgresql_session_history
235-
self.new.postgresql_session_history
236-
end
237-
238-
# Returns the full path to the MSSQL session history file.
239-
#
240-
# @return [String] path to the history file.
241-
def self.mssql_session_history
242-
self.new.mssql_session_history
243-
end
244-
245-
# Returns the full path to the MySQL session history file.
239+
# Returns the full path to the MySQL interactive query history file
246240
#
247-
# @return [String] path to the history file.
248-
def self.mysql_session_history
249-
self.new.mysql_session_history
241+
# @return [String] path to the interactive query history file.
242+
def self.history_file_for_session_type(opts)
243+
self.new.history_file_for_session_type(opts)
250244
end
251245

252246
def self.pry_history
@@ -336,6 +330,13 @@ def config_directory
336330
self['ConfigDirectory']
337331
end
338332

333+
# Returns the histories directory default.
334+
#
335+
# @return [String] the SQL session histories directory.
336+
def histories_directory
337+
config_directory + FileSep + self['HistoriesDirectory']
338+
end
339+
339340
# Returns the full path to the configuration file.
340341
#
341342
# @return [String] path to the configuration file.
@@ -362,16 +363,24 @@ def ldap_session_history
362363
config_directory + FileSep + "ldap_session_history"
363364
end
364365

365-
def postgresql_session_history
366-
config_directory + FileSep + "postgresql_session_history"
366+
def history_options_valid?(opts)
367+
return false if (opts[:session_type].nil? || opts[:interactive].nil?)
368+
369+
true
367370
end
368371

369-
def mysql_session_history
370-
config_directory + FileSep + "mysql_session_history"
372+
def interactive_to_string_map(interactive)
373+
# Check for true explicitly rather than just a value that is truthy.
374+
interactive == true ? '_interactive' : ''
371375
end
372376

373-
def mssql_session_history
374-
config_directory + FileSep + "mssql_session_history"
377+
def history_file_for_session_type(opts)
378+
return nil unless history_options_valid?(opts)
379+
380+
session_type_name = opts[:session_type]
381+
interactive = interactive_to_string_map(opts[:interactive])
382+
383+
histories_directory + FileSep + "#{session_type_name}_session#{interactive}_history"
375384
end
376385

377386
def pry_history
@@ -495,6 +504,7 @@ def init
495504
FileUtils.mkdir_p(user_module_directory)
496505
FileUtils.mkdir_p(user_plugin_directory)
497506
FileUtils.mkdir_p(user_data_directory)
507+
FileUtils.mkdir_p(histories_directory)
498508
end
499509

500510
# Loads configuration from the supplied file path, or the default one if

lib/rex/post/mssql/ui/console.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ def initialize(session, opts={})
3030
self.client = session.client
3131
envchange = ::Rex::Proto::MSSQL::ClientMixin::ENVCHANGE
3232
prompt = "%undMSSQL @ #{client.peerinfo} (#{client.initial_info_for_envchange(envchange: envchange::DATABASE)[:new]})%clr"
33-
history_manager = Msf::Config.mssql_session_history
34-
super(prompt, '>', history_manager, nil, :mssql)
33+
history_file = Msf::Config.history_file_for_session_type(session_type: session.type, interactive: false)
34+
super(prompt, '>', history_file, nil, :mssql)
3535

3636
# Queued commands array
3737
self.commands = []

lib/rex/post/mysql/ui/console.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ def initialize(session)
2525
self.session = session
2626
self.client = session.client
2727
prompt = "%undMySQL @ #{client.peerinfo} (#{current_database})%clr"
28-
history_manager = Msf::Config.mysql_session_history
29-
super(prompt, '>', history_manager, nil, :mysql)
28+
history_file = Msf::Config.history_file_for_session_type(session_type: session.type, interactive: false)
29+
super(prompt, '>', history_file, nil, :mysql)
3030

3131
# Queued commands array
3232
self.commands = []

lib/rex/post/postgresql/ui/console.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ def initialize(session)
2929
self.session = session
3030
self.client = session.client
3131
prompt = "%undPostgreSQL @ #{client.peerinfo} (#{current_database})%clr"
32-
history_manager = Msf::Config.postgresql_session_history
33-
super(prompt, '>', history_manager, nil, :postgresql)
32+
history_file = Msf::Config.history_file_for_session_type(session_type: session.type, interactive: false)
33+
super(prompt, '>', history_file, nil, :postgresql)
3434

3535
# Queued commands array
3636
self.commands = []

lib/rex/post/sql/ui/console/interactive_sql_client.rb

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,21 @@ def _winch
6767

6868
# Try getting multi-line input support provided by Reline, fall back to Readline.
6969
def _multiline_with_fallback
70-
query = _multiline
71-
query = _fallback if query[:status] == :fail
70+
name = session.type
71+
query = {}
72+
history_file = Msf::Config.history_file_for_session_type(session_type: name, interactive: true)
73+
return { status: :fail, errors: ["Unable to get history file for session type: #{name}"] } if history_file.nil?
74+
75+
# Multiline (Reline) and fallback (Readline) have separate history contexts as they are two different libraries.
76+
framework.history_manager.with_context(history_file: history_file , name: name, input_library: :reline) do
77+
query = _multiline
78+
end
79+
80+
if query[:status] == :fail
81+
framework.history_manager.with_context(history_file: history_file, name: name, input_library: :readline) do
82+
query = _fallback
83+
end
84+
end
7285

7386
query
7487
end
@@ -158,11 +171,21 @@ def _fallback
158171
break if line.end_with? ';'
159172
end
160173

161-
{ status: :success, result: line_buffer.join }
174+
{ status: :success, result: line_buffer.join(' ') }
162175
end
163176

164177
attr_accessor :on_log_proc, :client_dispatcher
165178

179+
private
180+
181+
def framework
182+
client_dispatcher.shell.framework
183+
end
184+
185+
def session
186+
client_dispatcher.shell.session
187+
end
188+
166189
end
167190
end
168191
end

lib/rex/ui/text/shell/history_manager.rb

Lines changed: 85 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ def initialize
2424
#
2525
# @param [String,nil] history_file The file to load and persist commands to
2626
# @param [String] name Human readable history context name
27+
# @param [Symbol] input_library The input library to provide context for. :reline, :readline
2728
# @param [Proc] block
2829
# @return [nil]
29-
def with_context(history_file: nil, name: nil, &block)
30-
push_context(history_file: history_file, name: name)
30+
def with_context(history_file: nil, name: nil, input_library: nil, &block)
31+
# Default to Readline for backwards compatibility.
32+
push_context(history_file: history_file, name: name, input_library: input_library || :readline)
3133

3234
begin
3335
block.call
@@ -59,15 +61,45 @@ def _debug=(value)
5961
@debug = value
6062
end
6163

64+
def _close
65+
event = { type: :close }
66+
@write_queue << event
67+
@remaining_work << event
68+
end
69+
6270
private
6371

6472
def debug?
6573
@debug
6674
end
6775

68-
def push_context(history_file: nil, name: nil)
76+
# A wrapper around mapping the input library to its history; this way we can mock the return value of this method.
77+
def map_library_to_history(input_library)
78+
case input_library
79+
when :readline
80+
::Readline::HISTORY
81+
when :reline
82+
::Reline::HISTORY
83+
else
84+
$stderr.puts("Unknown input library: #{input_library}") if debug?
85+
[]
86+
end
87+
end
88+
89+
def clear_library(input_library)
90+
case input_library
91+
when :readline
92+
clear_readline
93+
when :reline
94+
clear_reline
95+
else
96+
$stderr.puts("Unknown input library: #{input_library}") if debug?
97+
end
98+
end
99+
100+
def push_context(history_file: nil, name: nil, input_library: nil)
69101
$stderr.puts("Push context before\n#{JSON.pretty_generate(_contexts)}") if debug?
70-
new_context = { history_file: history_file, name: name }
102+
new_context = { history_file: history_file, name: name, input_library: input_library || :readline }
71103

72104
switch_context(new_context, @contexts.last)
73105
@contexts.push(new_context)
@@ -91,60 +123,93 @@ def readline_available?
91123
defined?(::Readline)
92124
end
93125

126+
def reline_available?
127+
begin
128+
require 'reline'
129+
defined?(::Reline)
130+
rescue ::LoadError => _e
131+
false
132+
end
133+
end
134+
94135
def clear_readline
95136
return unless readline_available?
96137

97138
::Readline::HISTORY.length.times { ::Readline::HISTORY.pop }
98139
end
99140

100-
def load_history_file(history_file)
101-
return unless readline_available?
141+
def clear_reline
142+
return unless reline_available?
102143

103-
clear_readline
104-
if File.exist?(history_file)
105-
File.readlines(history_file).each do |e|
106-
::Readline::HISTORY << e.chomp
144+
::Reline::HISTORY.length.times { ::Reline::HISTORY.pop }
145+
end
146+
147+
def load_history_file(context)
148+
history_file = context[:history_file]
149+
history = map_library_to_history(context[:input_library])
150+
151+
begin
152+
File.open(history_file, 'rb') do |f|
153+
clear_library(context[:input_library])
154+
f.each_line(chomp: true) do |line|
155+
if context[:input_library] == :reline && history.last&.end_with?("\\")
156+
history.last.delete_suffix!("\\")
157+
history.last << "\n" << line
158+
else
159+
history << line
160+
end
161+
end
107162
end
163+
rescue Errno::EACCES, Errno::ENOENT => e
164+
elog "Failed to open history file: #{history_file} with error: #{e}"
108165
end
109166
end
110167

111-
def store_history_file(history_file)
112-
return unless readline_available?
168+
def store_history_file(context)
169+
history_file = context[:history_file]
170+
history = map_library_to_history(context[:input_library])
171+
172+
history_diff = history.length < MAX_HISTORY ? history.length : MAX_HISTORY
173+
113174
cmds = []
114-
history_diff = ::Readline::HISTORY.length < MAX_HISTORY ? ::Readline::HISTORY.length : MAX_HISTORY
115175
history_diff.times do
116-
entry = ::Readline::HISTORY.pop
117-
cmds.push(entry) unless entry.nil?
176+
entry = history.pop
177+
cmds << entry.scrub.split("\n").join("\\\n")
118178
end
119179

120-
write_history_file(history_file, cmds)
180+
write_history_file(history_file, cmds.reverse)
121181
end
122182

123183
def switch_context(new_context, old_context=nil)
124184
if old_context && old_context[:history_file]
125-
store_history_file(old_context[:history_file])
185+
store_history_file(old_context)
126186
end
127187

128188
if new_context && new_context[:history_file]
129-
load_history_file(new_context[:history_file])
189+
load_history_file(new_context)
130190
else
131191
clear_readline
192+
clear_reline
132193
end
133-
rescue SignalException => e
194+
rescue SignalException => _e
134195
clear_readline
196+
clear_reline
135197
end
136198

137199
def write_history_file(history_file, cmds)
138200
write_queue_ref = @write_queue
139201
remaining_work_ref = @remaining_work
202+
140203
@write_thread ||= Rex::ThreadFactory.spawn("HistoryManagerWriter", false) do
141204
while (event = write_queue_ref.pop)
142205
begin
206+
break if event[:type] == :close
207+
143208
history_file = event[:history_file]
144209
cmds = event[:cmds]
145210

146211
File.open(history_file, 'wb+') do |f|
147-
f.puts(cmds.reverse)
212+
f.puts(cmds)
148213
end
149214

150215
rescue => e

0 commit comments

Comments
 (0)