Skip to content

Commit c0d66fd

Browse files
authored
Land rapid7#18933, update SQL sessions to correctly manage history
2 parents 37d3c88 + e2814d6 commit c0d66fd

File tree

4 files changed

+117
-39
lines changed

4 files changed

+117
-39
lines changed

lib/msf/base/config.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,20 +228,41 @@ def self.postgresql_session_history
228228
self.new.postgresql_session_history
229229
end
230230

231+
# Returns the full path to the PostgreSQL interactive query history file
232+
#
233+
# @return [String] path to the interactive query history file.
234+
def self.postgresql_session_history_interactive
235+
self.new.postgresql_session_history_interactive
236+
end
237+
231238
# Returns the full path to the MSSQL session history file.
232239
#
233240
# @return [String] path to the history file.
234241
def self.mssql_session_history
235242
self.new.mssql_session_history
236243
end
237244

245+
# Returns the full path to the MSSQL interactive query history file
246+
#
247+
# @return [String] path to the interactive query history file.
248+
def self.mssql_session_history_interactive
249+
self.new.mssql_session_history_interactive
250+
end
251+
238252
# Returns the full path to the MySQL session history file.
239253
#
240254
# @return [String] path to the history file.
241255
def self.mysql_session_history
242256
self.new.mysql_session_history
243257
end
244258

259+
# Returns the full path to the MySQL interactive query history file
260+
#
261+
# @return [String] path to the interactive query history file.
262+
def self.mysql_session_history_interactive
263+
self.new.mysql_session_history_interactive
264+
end
265+
245266
def self.pry_history
246267
self.new.pry_history
247268
end
@@ -355,14 +376,26 @@ def postgresql_session_history
355376
config_directory + FileSep + "postgresql_session_history"
356377
end
357378

379+
def postgresql_session_history_interactive
380+
postgresql_session_history + "_interactive"
381+
end
382+
358383
def mysql_session_history
359384
config_directory + FileSep + "mysql_session_history"
360385
end
361386

387+
def mysql_session_history_interactive
388+
mysql_session_history + "_interactive"
389+
end
390+
362391
def mssql_session_history
363392
config_directory + FileSep + "mssql_session_history"
364393
end
365394

395+
def mssql_session_history_interactive
396+
mssql_session_history + "_interactive"
397+
end
398+
366399
def pry_history
367400
config_directory + FileSep + "pry_history"
368401
end

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

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,19 @@ 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+
73+
# Multiline (Reline) and fallback (Readline) have separate history contexts as they are two different libraries.
74+
framework.history_manager.with_context(history_file: Msf::Config.send("#{name}_session_history_interactive"), name: name, input_library: :reline) do
75+
query = _multiline
76+
end
77+
78+
if query[:status] == :fail
79+
framework.history_manager.with_context(history_file: Msf::Config.send("#{name}_session_history_interactive"), name: name, input_library: :readline) do
80+
query = _fallback
81+
end
82+
end
7283

7384
query
7485
end
@@ -163,6 +174,16 @@ def _fallback
163174

164175
attr_accessor :on_log_proc, :client_dispatcher
165176

177+
private
178+
179+
def framework
180+
client_dispatcher.shell.framework
181+
end
182+
183+
def session
184+
client_dispatcher.shell.session
185+
end
186+
166187
end
167188
end
168189
end

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

Lines changed: 46 additions & 22 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
@@ -65,9 +67,9 @@ def debug?
6567
@debug
6668
end
6769

68-
def push_context(history_file: nil, name: nil)
70+
def push_context(history_file: nil, name: nil, input_library: nil)
6971
$stderr.puts("Push context before\n#{JSON.pretty_generate(_contexts)}") if debug?
70-
new_context = { history_file: history_file, name: name }
72+
new_context = { history_file: history_file, name: name, input_library: input_library || :readline }
7173

7274
switch_context(new_context, @contexts.last)
7375
@contexts.push(new_context)
@@ -91,47 +93,69 @@ def readline_available?
9193
defined?(::Readline)
9294
end
9395

96+
def reline_available?
97+
begin
98+
require 'reline'
99+
defined?(::Reline)
100+
rescue ::LoadError => _e
101+
false
102+
end
103+
end
104+
94105
def clear_readline
95106
return unless readline_available?
96107

97108
::Readline::HISTORY.length.times { ::Readline::HISTORY.pop }
98109
end
99110

100-
def load_history_file(history_file)
101-
return unless readline_available?
111+
def clear_reline
112+
return unless reline_available?
113+
114+
::Reline::HISTORY.length.times { ::Reline::HISTORY.pop }
115+
end
116+
117+
def load_history_file(context)
118+
history_file = context[:history_file]
119+
history = context[:input_library] == :reline ? ::Reline::HISTORY : ::Readline::HISTORY
102120

103-
clear_readline
104121
if File.exist?(history_file)
105-
File.readlines(history_file).each do |e|
106-
::Readline::HISTORY << e.chomp
122+
File.open(history_file, 'r') do |f|
123+
f.each do |line|
124+
chomped_line = line.chomp
125+
if context[:input_library] == :reline && history.last&.end_with?("\\")
126+
history.last.delete_suffix!("\\")
127+
history.last << "\n" << chomped_line
128+
else
129+
history << chomped_line
130+
end
131+
end
107132
end
108133
end
109134
end
110135

111-
def store_history_file(history_file)
112-
return unless readline_available?
113-
cmds = []
114-
history_diff = ::Readline::HISTORY.length < MAX_HISTORY ? ::Readline::HISTORY.length : MAX_HISTORY
115-
history_diff.times do
116-
entry = ::Readline::HISTORY.pop
117-
cmds.push(entry) unless entry.nil?
118-
end
136+
def store_history_file(context)
137+
history_file = context[:history_file]
138+
history = context[:input_library] == :reline ? ::Reline::HISTORY : ::Readline::HISTORY
139+
140+
history_to_save = history.map { |line| line.scrub.split("\n").join("\\\n") }
119141

120-
write_history_file(history_file, cmds)
142+
write_history_file(history_file, history_to_save)
121143
end
122144

123145
def switch_context(new_context, old_context=nil)
124146
if old_context && old_context[:history_file]
125-
store_history_file(old_context[:history_file])
147+
store_history_file(old_context)
126148
end
127149

128150
if new_context && new_context[:history_file]
129-
load_history_file(new_context[:history_file])
151+
load_history_file(new_context)
130152
else
131153
clear_readline
154+
clear_reline
132155
end
133-
rescue SignalException => e
156+
rescue SignalException => _e
134157
clear_readline
158+
clear_reline
135159
end
136160

137161
def write_history_file(history_file, cmds)
@@ -144,7 +168,7 @@ def write_history_file(history_file, cmds)
144168
cmds = event[:cmds]
145169

146170
File.open(history_file, 'wb+') do |f|
147-
f.puts(cmds.reverse)
171+
f.puts(cmds)
148172
end
149173

150174
rescue => e

spec/lib/rex/ui/text/shell/history_manager_spec.rb

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
(expect do |block|
2626
subject.with_context(name: 'a') do
2727
expected_contexts = [
28-
{ history_file: nil, name: 'a' },
28+
{ history_file: nil, input_library: :readline, name: 'a' },
2929
]
3030
expect(subject._contexts).to eq(expected_contexts)
3131
block.to_proc.call
@@ -36,15 +36,15 @@
3636

3737
context 'when there is an existing stack' do
3838
before(:each) do
39-
subject.send(:push_context, history_file: nil, name: 'a')
39+
subject.send(:push_context, history_file: nil, input_library: :readline, name: 'a')
4040
end
4141

4242
it 'continues to have the previous existing stack' do
4343
subject.with_context {
4444
# noop
4545
}
4646
expected_contexts = [
47-
{ history_file: nil, name: 'a' },
47+
{ history_file: nil, input_library: :readline, name: 'a' },
4848
]
4949
expect(subject._contexts).to eq(expected_contexts)
5050
end
@@ -53,8 +53,8 @@
5353
(expect do |block|
5454
subject.with_context(name: 'b') do
5555
expected_contexts = [
56-
{ history_file: nil, name: 'a' },
57-
{ history_file: nil, name: 'b' },
56+
{ history_file: nil, input_library: :readline, name: 'a' },
57+
{ history_file: nil, input_library: :readline, name: 'b' },
5858
]
5959
expect(subject._contexts).to eq(expected_contexts)
6060
block.to_proc.call
@@ -69,7 +69,7 @@
6969
}
7070
end.to raise_exception ArgumentError, 'Mock error'
7171
expected_contexts = [
72-
{ history_file: nil, name: 'a' },
72+
{ history_file: nil, input_library: :readline, name: 'a' },
7373
]
7474
expect(subject._contexts).to eq(expected_contexts)
7575
end
@@ -79,9 +79,9 @@
7979
describe '#push_context' do
8080
context 'when the stack is empty' do
8181
it 'stores the history contexts' do
82-
subject.send(:push_context, history_file: nil, name: 'a')
82+
subject.send(:push_context, history_file: nil, input_library: :readline, name: 'a')
8383
expected_contexts = [
84-
{ history_file: nil, name: 'a' }
84+
{ history_file: nil, input_library: :readline, name: 'a' }
8585
]
8686
expect(subject._contexts).to eq(expected_contexts)
8787
end
@@ -90,12 +90,12 @@
9090
context 'when multiple values are pushed' do
9191
it 'stores the history contexts' do
9292
subject.send(:push_context, history_file: nil, name: 'a')
93-
subject.send(:push_context, history_file: nil, name: 'b')
94-
subject.send(:push_context, history_file: nil, name: 'c')
93+
subject.send(:push_context, history_file: nil, input_library: :readline, name: 'b')
94+
subject.send(:push_context, history_file: nil, input_library: :reline, name: 'c')
9595
expected_contexts = [
96-
{ history_file: nil, name: 'a' },
97-
{ history_file: nil, name: 'b' },
98-
{ history_file: nil, name: 'c' },
96+
{ history_file: nil, input_library: :readline, name: 'a' },
97+
{ history_file: nil, input_library: :readline, name: 'b' },
98+
{ history_file: nil, input_library: :reline, name: 'c' },
9999
]
100100
expect(subject._contexts).to eq(expected_contexts)
101101
end
@@ -113,12 +113,12 @@
113113
end
114114

115115
context 'when the stack is not empty' do
116-
it 'continues to have an empty stack' do
116+
it 'continues to have a non-empty stack' do
117117
subject.send(:push_context, history_file: nil, name: 'a')
118118
subject.send(:push_context, history_file: nil, name: 'b')
119119
subject.send(:pop_context)
120120
expected_contexts = [
121-
{ history_file: nil, name: 'a' },
121+
{ history_file: nil, input_library: :readline, name: 'a' },
122122
]
123123
expect(subject._contexts).to eq(expected_contexts)
124124
end

0 commit comments

Comments
 (0)