CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/lib/rex/ui/text/dispatcher_shell.rb
Views: 11655
1
# -*- coding: binary -*-
2
require 'pp'
3
require 'rex/text/table'
4
require 'erb'
5
6
module Rex
7
module Ui
8
module Text
9
10
###
11
#
12
# The dispatcher shell class is designed to provide a generic means
13
# of processing various shell commands that may be located in
14
# different modules or chunks of codes. These chunks are referred
15
# to as command dispatchers. The only requirement for command dispatchers is
16
# that they prefix every method that they wish to be mirrored as a command
17
# with the cmd_ prefix.
18
#
19
###
20
module DispatcherShell
21
22
include Resource
23
24
###
25
#
26
# Empty template base class for command dispatchers.
27
#
28
###
29
module CommandDispatcher
30
31
module ClassMethods
32
#
33
# Check whether or not the command dispatcher is capable of handling the
34
# specified command. The command may still be disabled through some means
35
# at runtime.
36
#
37
# @param [String] name The name of the command to check.
38
# @return [Boolean] true if the dispatcher can handle the command.
39
def has_command?(name)
40
self.method_defined?("cmd_#{name}")
41
end
42
43
def included(base)
44
# Propagate the included hook
45
CommandDispatcher.included(base)
46
end
47
end
48
49
def self.included(base)
50
# Install class methods so they are inheritable
51
base.extend(ClassMethods)
52
end
53
54
#
55
# Initializes the command dispatcher mixin.
56
#
57
def initialize(shell)
58
self.shell = shell
59
self.tab_complete_items = []
60
end
61
62
#
63
# Returns nil for an empty set of commands.
64
#
65
# This method should be overridden to return a Hash with command
66
# names for keys and brief help text for values.
67
#
68
def commands
69
end
70
71
#
72
# Returns an empty set of commands.
73
#
74
# This method should be overridden if the dispatcher has commands that
75
# should be treated as deprecated. Deprecated commands will not show up in
76
# help and will not tab-complete, but will still be callable.
77
#
78
def deprecated_commands
79
[]
80
end
81
82
#
83
# Wraps shell.print_error
84
#
85
def print_error(msg = '')
86
shell.print_error(msg)
87
end
88
89
alias_method :print_bad, :print_error
90
91
#
92
# Wraps shell.print_status
93
#
94
def print_status(msg = '')
95
shell.print_status(msg)
96
end
97
98
#
99
# Wraps shell.print_line
100
#
101
def print_line(msg = '')
102
shell.print_line(msg)
103
end
104
105
#
106
# Wraps shell.print_good
107
#
108
def print_good(msg = '')
109
shell.print_good(msg)
110
end
111
112
#
113
# Wraps shell.print_warning
114
#
115
def print_warning(msg = '')
116
shell.print_warning(msg)
117
end
118
119
#
120
# Wraps shell.print
121
#
122
def print(msg = '')
123
shell.print(msg)
124
end
125
126
#
127
# Print a warning that the called command is deprecated and optionally
128
# forward to the replacement +method+ (useful for when commands are
129
# renamed).
130
#
131
def deprecated_cmd(method=nil, *args)
132
cmd = caller[0].match(/`cmd_(.*)'/)[1]
133
print_error "The #{cmd} command is DEPRECATED"
134
if cmd == "db_autopwn"
135
print_error "See http://r-7.co/xY65Zr instead"
136
elsif method and self.respond_to?("cmd_#{method}", true)
137
print_error "Use #{method} instead"
138
self.send("cmd_#{method}", *args)
139
end
140
end
141
142
def deprecated_help(method=nil)
143
cmd = caller[0].match(/`cmd_(.*)_help'/)[1]
144
print_error "The #{cmd} command is DEPRECATED"
145
if cmd == "db_autopwn"
146
print_error "See http://r-7.co/xY65Zr instead"
147
elsif method and self.respond_to?("cmd_#{method}_help", true)
148
print_error "Use 'help #{method}' instead"
149
self.send("cmd_#{method}_help")
150
end
151
end
152
153
#
154
# Wraps shell.update_prompt
155
#
156
def update_prompt(*args)
157
shell.update_prompt(*args)
158
end
159
160
def cmd_help_help
161
print_line "There's only so much I can do"
162
end
163
164
#
165
# Displays the help banner. With no arguments, this is just a list of
166
# all commands grouped by dispatcher. Otherwise, tries to use a method
167
# named cmd_#{+cmd+}_help for the first dispatcher that has a command
168
# named +cmd+. If no such method exists, uses +cmd+ as a regex to
169
# compare against each enstacked dispatcher's name and dumps commands
170
# of any that match.
171
#
172
def cmd_help(cmd=nil, *ignored)
173
if cmd
174
help_found = false
175
cmd_found = false
176
shell.dispatcher_stack.each do |dispatcher|
177
next unless dispatcher.respond_to?(:commands)
178
next if (dispatcher.commands.nil?)
179
next if (dispatcher.commands.length == 0)
180
181
if dispatcher.respond_to?("cmd_#{cmd}", true)
182
cmd_found = true
183
break unless dispatcher.respond_to?("cmd_#{cmd}_help", true)
184
dispatcher.send("cmd_#{cmd}_help")
185
help_found = true
186
break
187
end
188
end
189
190
unless cmd_found
191
# We didn't find a cmd, try it as a dispatcher name
192
shell.dispatcher_stack.each do |dispatcher|
193
if dispatcher.name =~ /#{cmd}/i
194
print_line(dispatcher.help_to_s)
195
cmd_found = help_found = true
196
end
197
end
198
end
199
200
if docs_dir && File.exist?(File.join(docs_dir, cmd + '.md'))
201
print_line
202
print(File.read(File.join(docs_dir, cmd + '.md')))
203
end
204
print_error("No help for #{cmd}, try -h") if cmd_found and not help_found
205
print_error("No such command") if not cmd_found
206
else
207
print(shell.help_to_s)
208
if docs_dir && File.exist?(File.join(docs_dir + '.md'))
209
print_line
210
print(File.read(File.join(docs_dir + '.md')))
211
end
212
end
213
end
214
215
#
216
# Tab completion for the help command
217
#
218
# By default just returns a list of all commands in all dispatchers.
219
#
220
def cmd_help_tabs(str, words)
221
return [] if words.length > 1
222
223
tabs = []
224
shell.dispatcher_stack.each { |dispatcher|
225
tabs += dispatcher.commands.keys
226
}
227
return tabs
228
end
229
230
alias cmd_? cmd_help
231
232
#
233
# Return a pretty, user-readable table of commands provided by this
234
# dispatcher.
235
# The command column width can be modified by passing in :command_width.
236
#
237
def help_to_s(opts={})
238
# If this dispatcher has no commands, we can't do anything useful.
239
return "" if commands.nil? or commands.length == 0
240
241
# Display the commands
242
tbl = Rex::Text::Table.new(
243
'Header' => "#{self.name} Commands",
244
'Indent' => opts['Indent'] || 4,
245
'Columns' =>
246
[
247
'Command',
248
'Description'
249
],
250
'ColProps' =>
251
{
252
'Command' =>
253
{
254
'Width' => opts[:command_width]
255
}
256
})
257
258
commands.sort.each { |c|
259
tbl << c
260
}
261
262
return "\n" + tbl.to_s + "\n"
263
end
264
265
#
266
# Return the subdir of the `documentation/` directory that should be used
267
# to find usage documentation
268
#
269
# TODO: get this value from somewhere that doesn't invert a bunch of
270
# dependencies
271
#
272
def docs_dir
273
File.expand_path(File.join(__FILE__, '..', '..', '..', '..', '..', 'documentation', 'cli'))
274
end
275
276
#
277
# No tab completion items by default
278
#
279
attr_accessor :shell, :tab_complete_items
280
281
#
282
# Provide a generic tab completion for file names.
283
#
284
# If the only completion is a directory, this descends into that directory
285
# and continues completions with filenames contained within.
286
#
287
def tab_complete_filenames(str, words)
288
matches = ::Readline::FILENAME_COMPLETION_PROC.call(str)
289
if matches and matches.length == 1 and File.directory?(matches[0])
290
dir = matches[0]
291
dir += File::SEPARATOR if dir[-1,1] != File::SEPARATOR
292
matches = ::Readline::FILENAME_COMPLETION_PROC.call(dir)
293
end
294
matches.nil? ? [] : matches
295
end
296
297
#
298
# Return a list of possible directory for tab completion.
299
#
300
def tab_complete_directory(str, words)
301
directory = str[-1] == File::SEPARATOR ? str : File.dirname(str)
302
filename = str[-1] == File::SEPARATOR ? '' : File.basename(str)
303
entries = Dir.entries(directory).select { |fp| fp.start_with?(filename) }
304
dirs = entries - ['.', '..']
305
dirs = dirs.map { |fp| File.join(directory, fp).gsub(/\A\.\//, '') }
306
dirs = dirs.select { |x| File.directory?(x) }
307
dirs = dirs.map { |x| x + File::SEPARATOR }
308
if dirs.length == 1 && dirs[0] != str && dirs[0].end_with?(File::SEPARATOR)
309
# If Readline receives a single value from this function, it will assume we're done with the tab
310
# completing, and add an extra space at the end.
311
# This is annoying if we're recursively tab-traversing our way through subdirectories -
312
# we may want to continue traversing, but MSF will add a space, requiring us to back up to continue
313
# tab-completing our way through successive subdirectories.
314
::Readline.completion_append_character = nil
315
end
316
317
if dirs.length == 0 && File.directory?(str)
318
# we've hit the end of the road
319
dirs = [str]
320
end
321
322
dirs
323
end
324
325
#
326
# Provide a generic tab completion function based on the specification
327
# pass as fmt. The fmt argument in a hash where values are an array
328
# defining how the command should be completed. The first element of the
329
# array can be one of:
330
# nil - This argument is a flag and takes no option.
331
# true - This argument takes an option with no suggestions.
332
# :address - This option is a source address.
333
# :bool - This option is a boolean.
334
# :file - This option is a file path.
335
# Array - This option is an array of possible values.
336
#
337
def tab_complete_generic(fmt, str, words)
338
last_word = words[-1]
339
fmt = fmt.select { |key, value| last_word == key || !words.include?(key) }
340
341
val = fmt[last_word]
342
return fmt.keys if !val # the last word does not look like a fmtspec
343
arg = val[0]
344
return fmt.keys if !arg # the last word is a fmtspec that takes no argument
345
346
tabs = []
347
if arg.to_s.to_sym == :address
348
tabs = tab_complete_source_address
349
elsif arg.to_s.to_sym == :bool
350
tabs = ['true', 'false']
351
elsif arg.to_s.to_sym == :file
352
tabs = tab_complete_filenames(str, words)
353
elsif arg.kind_of?(Array)
354
tabs = arg.map {|a| a.to_s}
355
end
356
tabs
357
end
358
359
#
360
# Return a list of possible source addresses for tab completion.
361
#
362
def tab_complete_source_address
363
addresses = [Rex::Socket.source_address]
364
# getifaddrs was introduced in 2.1.2
365
if ::Socket.respond_to?(:getifaddrs)
366
ifaddrs = ::Socket.getifaddrs.select do |ifaddr|
367
ifaddr.addr && ifaddr.addr.ip?
368
end
369
addresses += ifaddrs.map { |ifaddr| ifaddr.addr.ip_address }
370
end
371
addresses
372
end
373
374
#
375
# A callback that can be used to handle unknown commands. This can for example, allow a dispatcher to mark a command
376
# as being disabled.
377
#
378
# @return [Symbol, nil] Returns a symbol specifying the action that was taken by the handler or `nil` if no action
379
# was taken. The only supported action at this time is `:handled`, signifying that the unknown command was handled
380
# by this dispatcher and no additional dispatchers should receive it.
381
def unknown_command(method, line)
382
nil
383
end
384
end
385
386
#
387
# DispatcherShell derives from shell.
388
#
389
include Shell
390
391
#
392
# Initialize the dispatcher shell.
393
#
394
def initialize(prompt, prompt_char = '>', histfile = nil, framework = nil, name = nil)
395
super
396
397
# Initialize the dispatcher array
398
self.dispatcher_stack = []
399
400
# Initialize the tab completion array
401
self.on_command_proc = nil
402
end
403
404
#
405
# This method accepts the entire line of text from the Readline
406
# routine, stores all completed words, and passes the partial
407
# word to the real tab completion function. This works around
408
# a design problem in the Readline module and depends on the
409
# Readline.basic_word_break_characters variable being set to \x00
410
#
411
def tab_complete(str)
412
::Readline.completion_append_character = ' '
413
::Readline.completion_case_fold = false
414
415
# Check trailing whitespace so we can tell 'x' from 'x '
416
str_match = str.match(/[^\\]([\\]{2})*\s+$/)
417
str_trail = (str_match.nil?) ? '' : str_match[0]
418
419
# Split the line up by whitespace into words
420
split_str = shellsplitex(str)
421
422
# Append an empty token if we had trailing whitespace
423
split_str[:tokens] << { begin: str.length, value: '' } if str_trail.length > 0
424
425
# Pop the last word and pass it to the real method
426
result = tab_complete_stub(str, split_str)
427
if result
428
result.uniq
429
else
430
result
431
end
432
end
433
434
# Performs tab completion of a command, if supported
435
#
436
def tab_complete_stub(original_str, split_str)
437
*preceding_tokens, current_token = split_str[:tokens]
438
return nil unless current_token
439
440
items = []
441
current_word = current_token[:value]
442
tab_words = preceding_tokens.map { |word| word[:value] }
443
444
# Next, try to match internal command or value completion
445
# Enumerate each entry in the dispatcher stack
446
dispatcher_stack.each do |dispatcher|
447
448
# If no command is set and it supports commands, add them all
449
if tab_words.empty? and dispatcher.respond_to?('commands')
450
items.concat(dispatcher.commands.keys)
451
end
452
453
# If the dispatcher exports a tab completion function, use it
454
if dispatcher.respond_to?('tab_complete_helper')
455
res = dispatcher.tab_complete_helper(current_word, tab_words)
456
else
457
res = tab_complete_helper(dispatcher, current_word, tab_words)
458
end
459
460
if res.nil?
461
# A nil response indicates no optional arguments
462
return [''] if items.empty?
463
else
464
if res.second == :override_completions
465
return res.first
466
else
467
# Otherwise we add the completion items to the list
468
items.concat(res)
469
end
470
end
471
end
472
473
# Match based on the partial word
474
matches = items.select do |word|
475
word.downcase.start_with?(current_word.downcase)
476
end
477
478
# Prepend the preceding string of the command (or it all gets replaced!)
479
preceding_str = original_str[0...current_token[:begin]]
480
quote = current_token[:quote]
481
matches_with_preceding_words_appended = matches.map do |word|
482
word = quote.nil? ? word.gsub('\\') { '\\\\' }.gsub(' ', '\\ ') : "#{quote}#{word}#{quote}"
483
preceding_str + word
484
end
485
486
matches_with_preceding_words_appended
487
end
488
489
#
490
# Provide command-specific tab completion
491
#
492
def tab_complete_helper(dispatcher, str, words)
493
tabs_meth = "cmd_#{words[0]}_tabs"
494
# Is the user trying to tab complete one of our commands?
495
if dispatcher.commands.include?(words[0]) and dispatcher.respond_to?(tabs_meth)
496
res = dispatcher.send(tabs_meth, str, words)
497
return [] if res.nil?
498
return res
499
end
500
501
# Avoid the default completion list for unknown commands
502
[]
503
end
504
505
#
506
# Run a single command line.
507
#
508
# @param [String] line The command string that should be executed.
509
# @param [Boolean] propagate_errors Whether or not to raise exceptions that are caught while executing the command.
510
#
511
# @return [Boolean] A boolean value signifying whether or not the command was handled. Value is `true` when the
512
# command line was handled.
513
def run_single(line, propagate_errors: false)
514
arguments = parse_line(line)
515
method = arguments.shift
516
cmd_status = nil # currently either nil or :handled, more statuses can be added in the future
517
error = false
518
519
# If output is disabled output will be nil
520
output.reset_color if (output)
521
522
if (method)
523
entries = dispatcher_stack.length
524
525
dispatcher_stack.each { |dispatcher|
526
next if not dispatcher.respond_to?('commands')
527
528
begin
529
if (dispatcher.commands.has_key?(method) or dispatcher.deprecated_commands.include?(method))
530
self.on_command_proc.call(line.strip) if self.on_command_proc
531
run_command(dispatcher, method, arguments)
532
cmd_status = :handled
533
elsif cmd_status.nil?
534
cmd_status = dispatcher.unknown_command(method, line)
535
end
536
rescue ::Interrupt
537
cmd_status = :handled
538
print_error("#{method}: Interrupted")
539
raise if propagate_errors
540
rescue OptionParser::ParseError => e
541
print_error("#{method}: #{e.message}")
542
raise if propagate_errors
543
rescue
544
error = $!
545
546
print_error(
547
"Error while running command #{method}: #{$!}" +
548
"\n\nCall stack:\n#{$@.join("\n")}")
549
550
raise if propagate_errors
551
rescue ::Exception => e
552
error = $!
553
554
print_error(
555
"Error while running command #{method}: #{$!}")
556
557
raise if propagate_errors
558
end
559
560
# If the dispatcher stack changed as a result of this command,
561
# break out
562
break if (dispatcher_stack.length != entries)
563
}
564
565
if (cmd_status.nil? && error == false)
566
unknown_command(method, line)
567
end
568
end
569
570
return cmd_status == :handled
571
end
572
573
#
574
# Runs the supplied command on the given dispatcher.
575
#
576
def run_command(dispatcher, method, arguments)
577
self.busy = true
578
579
if(blocked_command?(method))
580
print_error("The #{method} command has been disabled.")
581
else
582
dispatcher.send('cmd_' + method, *arguments)
583
end
584
ensure
585
self.busy = false
586
end
587
588
#
589
# If the command is unknown...
590
#
591
def unknown_command(method, line)
592
# Map each dispatchers commands to valid_commands
593
valid_commands = dispatcher_stack.flat_map { |dispatcher| dispatcher.commands.keys }
594
595
message = "Unknown command: #{method}."
596
suggestion = DidYouMean::SpellChecker.new(dictionary: valid_commands).correct(method).first
597
message << " Did you mean %grn#{suggestion}%clr?" if suggestion
598
message << ' Run the %grnhelp%clr command for more details.'
599
600
print_error(message)
601
end
602
603
#
604
# Push a dispatcher to the front of the stack.
605
#
606
def enstack_dispatcher(dispatcher)
607
self.dispatcher_stack.unshift(inst = dispatcher.new(self))
608
609
inst
610
end
611
612
#
613
# Pop a dispatcher from the front of the stacker.
614
#
615
def destack_dispatcher
616
self.dispatcher_stack.shift
617
end
618
619
#
620
# Adds the supplied dispatcher to the end of the dispatcher stack so that
621
# it doesn't affect any enstack'd dispatchers.
622
#
623
def append_dispatcher(dispatcher)
624
inst = dispatcher.new(self)
625
self.dispatcher_stack.each { |disp|
626
if (disp.name == inst.name)
627
raise "Attempting to load already loaded dispatcher #{disp.name}"
628
end
629
}
630
self.dispatcher_stack.push(inst)
631
632
inst
633
end
634
635
#
636
# Removes the supplied dispatcher instance.
637
#
638
def remove_dispatcher(name)
639
self.dispatcher_stack.delete_if { |inst|
640
(inst.name == name)
641
}
642
end
643
644
#
645
# Returns the current active dispatcher
646
#
647
def current_dispatcher
648
self.dispatcher_stack[0]
649
end
650
651
#
652
# Return a readable version of a help banner for all of the enstacked
653
# dispatchers.
654
#
655
# See +CommandDispatcher#help_to_s+
656
#
657
def help_to_s(opts = {})
658
str = ''
659
660
max_command_length = dispatcher_stack.flat_map { |dispatcher| dispatcher.commands.to_a }.map { |(name, _description)| name.length }.max
661
662
dispatcher_stack.reverse.each { |dispatcher|
663
str << dispatcher.help_to_s(opts.merge({ command_width: [max_command_length, 12].max }))
664
}
665
666
return str << "For more info on a specific command, use %grn<command> -h%clr or %grnhelp <command>%clr.\n\n"
667
end
668
669
670
#
671
# Returns nil for an empty set of blocked commands.
672
#
673
def blocked_command?(cmd)
674
return false if not self.blocked
675
self.blocked.has_key?(cmd)
676
end
677
678
#
679
# Block a specific command
680
#
681
def block_command(cmd)
682
self.blocked ||= {}
683
self.blocked[cmd] = true
684
end
685
686
#
687
# Unblock a specific command
688
#
689
def unblock_command(cmd)
690
self.blocked || return
691
self.blocked.delete(cmd)
692
end
693
694
#
695
# Split a line as Shellwords.split would however instead of raising an
696
# ArgumentError on unbalanced quotes return the remainder of the string as if
697
# the last character were the closing quote.
698
#
699
# This code was originally taken from https://github.com/ruby/ruby/blob/93420d34aaf8c30f11a66dd08eb186da922c831d/lib/shellwords.rb#L88
700
#
701
def shellsplitex(line)
702
tokens = []
703
field_value = String.new
704
field_begin = nil
705
706
line.scan(/\G(\s*)(?>([^\s\\\'\"]+)|'([^\']*)'|"((?:[^\"\\]|\\.)*)"|(\\.?)|(\S))(\s|\z)?/m) do |preceding_whitespace, word, sq, dq, esc, garbage, sep|
707
field_begin ||= Regexp.last_match.begin(0) + preceding_whitespace.length
708
if garbage
709
quote_start_begin = Regexp.last_match.begin(0) + preceding_whitespace.length
710
field_quote = garbage
711
field_value << line[quote_start_begin + 1..-1].gsub('\\\\', '\\')
712
713
tokens << { begin: field_begin, value: field_value, quote: field_quote }
714
break
715
end
716
717
field_value << (word || sq || (dq && dq.gsub(/\\([$`"\\\n])/, '\\1')) || esc.gsub(/\\(.)/, '\\1'))
718
if sep
719
tokens << { begin: field_begin, value: field_value, quote: ((sq && "'") || (dq && '"') || nil) }
720
field_value = String.new
721
field_begin = nil
722
end
723
end
724
725
{ tokens: tokens }
726
end
727
728
attr_accessor :dispatcher_stack # :nodoc:
729
attr_accessor :busy # :nodoc:
730
attr_accessor :blocked # :nodoc:
731
732
end
733
734
end
735
end
736
end
737
738