librex 0.0.6 → 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +3 -5
- data/Rakefile +26 -0
- data/lib/rex/compat.rb +1 -1
- data/lib/rex/exploitation/javascriptosdetect.rb +125 -62
- data/lib/rex/file.rb +15 -0
- data/lib/rex/io/stream.rb +1 -1
- data/lib/rex/parser/nmap_xml.rb +6 -0
- data/lib/rex/poly/block.rb +9 -0
- data/lib/rex/post/meterpreter/client.rb +0 -8
- data/lib/rex/post/meterpreter/extensions/priv/priv.rb +6 -0
- data/lib/rex/post/meterpreter/extensions/stdapi/fs/file.rb +1 -1
- data/lib/rex/post/meterpreter/extensions/stdapi/railgun/def/def_advapi32.rb +49 -35
- data/lib/rex/post/meterpreter/extensions/stdapi/railgun/def/def_netapi32.rb +26 -0
- data/lib/rex/post/meterpreter/extensions/stdapi/railgun/railgun.rb +9 -2
- data/lib/rex/post/meterpreter/extensions/stdapi/railgun/util.rb +630 -0
- data/lib/rex/post/meterpreter/packet.rb +3 -1
- data/lib/rex/post/meterpreter/ui/console/command_dispatcher/core.rb +143 -57
- data/lib/rex/post/meterpreter/ui/console/command_dispatcher/stdapi/fs.rb +6 -0
- data/lib/rex/post/meterpreter/ui/console/command_dispatcher/stdapi/net.rb +9 -3
- data/lib/rex/post/meterpreter/ui/console/command_dispatcher/stdapi/sys.rb +6 -4
- data/lib/rex/proto.rb +1 -0
- data/lib/rex/proto/dhcp/server.rb +4 -2
- data/lib/rex/proto/http/packet.rb +5 -6
- data/lib/rex/proto/ntlm.rb +7 -0
- data/lib/rex/proto/ntlm.rb.ut.rb +177 -0
- data/lib/rex/proto/ntlm/base.rb +326 -0
- data/lib/rex/proto/ntlm/constants.rb +74 -0
- data/lib/rex/proto/ntlm/crypt.rb +340 -0
- data/lib/rex/proto/ntlm/exceptions.rb +9 -0
- data/lib/rex/proto/ntlm/message.rb +533 -0
- data/lib/rex/proto/ntlm/utils.rb +358 -0
- data/lib/rex/proto/smb/client.rb +548 -86
- data/lib/rex/proto/smb/client.rb.ut.rb +4 -4
- data/lib/rex/proto/smb/constants.rb +7 -24
- data/lib/rex/proto/smb/crypt.rb +12 -71
- data/lib/rex/proto/smb/exceptions.rb +12 -0
- data/lib/rex/proto/smb/simpleclient.rb +17 -5
- data/lib/rex/proto/smb/utils.rb +3 -460
- data/lib/rex/proto/tftp/server.rb +2 -2
- data/lib/rex/script/base.rb +2 -2
- data/lib/rex/socket.rb +12 -0
- data/lib/rex/socket.rb.ut.rb +31 -10
- data/lib/rex/socket/ssl_tcp_server.rb.ut.rb +15 -5
- data/lib/rex/text.rb +55 -4
- data/lib/rex/ui/output.rb +0 -2
- data/lib/rex/ui/text/dispatcher_shell.rb +95 -10
- data/lib/rex/ui/text/output/buffer.rb +0 -4
- data/lib/rex/ui/text/shell.rb +8 -0
- data/lib/rex/ui/text/table.rb +21 -1
- metadata +15 -19
- data/lib/rex/proto/smb/crypt.rb.ut.rb +0 -20
@@ -1,4 +1,4 @@
|
|
1
|
-
# $Id: server.rb
|
1
|
+
# $Id: server.rb 11636 2011-01-25 02:24:37Z hdm $
|
2
2
|
require 'rex/socket'
|
3
3
|
require 'rex/proto/tftp'
|
4
4
|
|
@@ -194,7 +194,7 @@ protected
|
|
194
194
|
if @output_dir
|
195
195
|
fn = tr[:file][:name].split(File::SEPARATOR)[-1]
|
196
196
|
if fn
|
197
|
-
fn = ::File.join(@output_dir, fn)
|
197
|
+
fn = ::File.join(@output_dir, Rex::FileUtils.clean_path(fn))
|
198
198
|
::File.open(fn, "wb") { |fd|
|
199
199
|
fd.write(tr[:file][:data])
|
200
200
|
}
|
data/lib/rex/script/base.rb
CHANGED
data/lib/rex/socket.rb
CHANGED
@@ -131,6 +131,18 @@ module Socket
|
|
131
131
|
(addr =~ /^(?:(?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2}))$/) ? true : false
|
132
132
|
end
|
133
133
|
|
134
|
+
#
|
135
|
+
# Return true if +addr+ is within the ranges specified in RFC1918, or
|
136
|
+
# RFC5735/RFC3927
|
137
|
+
#
|
138
|
+
def self.is_internal?(addr)
|
139
|
+
if self.dotted_ip?(addr)
|
140
|
+
addr =~ /^(?:10\.|192\.168|172.(?:1[6-9]|2[0-9]|3[01])\.|169\.254)/
|
141
|
+
else
|
142
|
+
false
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
134
146
|
#
|
135
147
|
# Wrapper for Resolv.getaddress that takes special care to see if the
|
136
148
|
# supplied address is already a dotted quad, for instance. This is
|
data/lib/rex/socket.rb.ut.rb
CHANGED
@@ -42,25 +42,35 @@ class Rex::Socket::UnitTest < Test::Unit::TestCase
|
|
42
42
|
end
|
43
43
|
|
44
44
|
def test_to_sockaddr
|
45
|
-
assert_equal("
|
46
|
-
|
47
|
-
|
45
|
+
assert_equal(([2] + [0]*14).pack("sC*"), Rex::Socket.to_sockaddr(0, 0), "null sockaddr")
|
46
|
+
=begin
|
47
|
+
# This is platform dependent, pain to test
|
48
|
+
if (Rex::Socket.support_ipv6?)
|
49
|
+
# Use the constant for AF_INET6 since it is different per platform
|
50
|
+
# (10 on linux and 28 on BSD)
|
51
|
+
inaddr_any_sockaddr = ([::Socket::AF_INET6, 22] + [0]*24).pack('sSC*')
|
52
|
+
else
|
53
|
+
inaddr_any_sockaddr = ([2, 22] + [0]*12).pack('snC*')
|
54
|
+
end
|
55
|
+
=end
|
56
|
+
assert_equal(([2, 0x16, 1, 2, 3, 4] + [0]*8).pack('snC*'), Rex::Socket.to_sockaddr("1.2.3.4", 22), "1.2.3.4 addr, port 22 sockaddr")
|
48
57
|
end
|
49
58
|
|
50
59
|
def test_from_sockaddr
|
51
|
-
|
52
|
-
|
60
|
+
# 1.9.1 raises ArgumentError if we don't have an af == AF_INET or AF_INET6
|
61
|
+
af, host, port = Rex::Socket.from_sockaddr(([2, 0] + [0]*12).pack('snC*'))
|
62
|
+
assert_equal(2, af, "af = 2")
|
53
63
|
assert_equal('0.0.0.0', host, "zero host")
|
54
64
|
assert_equal(0, port, "zero port")
|
55
65
|
|
56
|
-
af, host, port = Rex::Socket.from_sockaddr([2].pack('
|
66
|
+
af, host, port = Rex::Socket.from_sockaddr(([2, 22]+[0]*12).pack('snC*'))
|
57
67
|
assert_equal(2, af, "af = 2")
|
58
|
-
assert_equal('0.0.0.0', host, "zero host")
|
59
68
|
assert_equal(22, port, "port = 22")
|
69
|
+
assert_equal('0.0.0.0', host, "zero host")
|
60
70
|
|
61
|
-
af, host, port = Rex::Socket.from_sockaddr([2].pack('
|
71
|
+
af, host, port = Rex::Socket.from_sockaddr(([2, 22, 1, 2, 3, 4] + [0]*8).pack('snC*') )
|
62
72
|
assert_equal(2, af, "af = 2")
|
63
|
-
assert_equal('1.2.3.4', host, "
|
73
|
+
assert_equal('1.2.3.4', host, "host = '1.2.3.4'")
|
64
74
|
assert_equal(22, port, "port = 22")
|
65
75
|
end
|
66
76
|
|
@@ -83,4 +93,15 @@ class Rex::Socket::UnitTest < Test::Unit::TestCase
|
|
83
93
|
assert_equal("255.255.0.0", Rex::Socket.bit2netmask(16))
|
84
94
|
end
|
85
95
|
|
86
|
-
|
96
|
+
def test_is_internal
|
97
|
+
assert( ! Rex::Socket.is_internal?("1.2.3.4"))
|
98
|
+
assert( ! Rex::Socket.is_internal?("172.15.3.4"))
|
99
|
+
assert( ! Rex::Socket.is_internal?("172.32.3.4"))
|
100
|
+
assert(Rex::Socket.is_internal?("10.2.3.4"))
|
101
|
+
assert(Rex::Socket.is_internal?("192.168.3.4"))
|
102
|
+
16.upto(31) do |octet|
|
103
|
+
assert(Rex::Socket.is_internal?("172.#{octet}.3.4"))
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
@@ -5,17 +5,23 @@ $:.unshift(File.join(File.dirname(__FILE__), '..', '..'))
|
|
5
5
|
require 'test/unit'
|
6
6
|
require 'rex/socket/ssl_tcp_server'
|
7
7
|
require 'rex/socket/ssl_tcp'
|
8
|
+
require 'rex/text'
|
8
9
|
|
9
10
|
class Rex::Socket::SslTcpServer::UnitTest < Test::Unit::TestCase
|
10
11
|
|
11
12
|
# XXX. The client data is sent & decrypted just fine. The server data is not. the client thread just spins. BAH.
|
13
|
+
#
|
14
|
+
# As of 2011-03-04, works fine on 1.8.6-p399, 1.8.7-p330, 1.9.1-p378
|
15
|
+
#
|
12
16
|
def test_tcp_server
|
13
|
-
return;
|
17
|
+
#return;
|
14
18
|
|
15
19
|
serv_port = 65433
|
16
20
|
c = nil
|
17
21
|
|
18
22
|
threads = []
|
23
|
+
|
24
|
+
# Server thread
|
19
25
|
threads << Thread.new() {
|
20
26
|
serv = Rex::Socket.create_tcp_server('LocalPort' => serv_port, 'SSL' => true)
|
21
27
|
assert_kind_of(Rex::Socket::SslTcpServer, serv, "type => ssl")
|
@@ -24,12 +30,16 @@ class Rex::Socket::SslTcpServer::UnitTest < Test::Unit::TestCase
|
|
24
30
|
s = serv.accept
|
25
31
|
assert_equal("client_data\n", s.get_once(), "s: get_once")
|
26
32
|
assert_equal(3, s.write("Yo\n"), "s: put Yo")
|
27
|
-
|
28
|
-
|
29
|
-
|
33
|
+
# Make sure methods are Strings for 1.9 compat (which returns
|
34
|
+
# symbols)
|
35
|
+
meths = s.methods.map {|m| m.to_s}
|
36
|
+
assert(meths.include?("<<"), "Has <<")
|
37
|
+
assert(meths.include?(">>"), "Has >>")
|
38
|
+
assert(meths.include?("has_read_data?"), "Has has_read_data?")
|
30
39
|
serv.close
|
31
40
|
}
|
32
41
|
|
42
|
+
# Client thread
|
33
43
|
threads << Thread.new() {
|
34
44
|
sleep(2)
|
35
45
|
assert_nothing_raised {
|
@@ -48,4 +58,4 @@ class Rex::Socket::SslTcpServer::UnitTest < Test::Unit::TestCase
|
|
48
58
|
threads.each { |aThread| aThread.join }
|
49
59
|
end
|
50
60
|
|
51
|
-
end
|
61
|
+
end
|
data/lib/rex/text.rb
CHANGED
@@ -237,7 +237,7 @@ module Text
|
|
237
237
|
#
|
238
238
|
def self.to_hex_ascii(str, prefix = "\\x", count = 1, suffix=nil)
|
239
239
|
raise ::RuntimeError, "unable to chunk into #{count} byte chunks" if ((str.length % count) > 0)
|
240
|
-
return str.unpack('H*')[0].gsub(Regexp.new(".{#{count * 2}}", nil, 'n')) { |s|
|
240
|
+
return str.unpack('H*')[0].gsub(Regexp.new(".{#{count * 2}}", nil, 'n')) { |s|
|
241
241
|
(0x20..0x7e) === s.to_i(16) ? s.to_i(16).chr : prefix + s + suffix.to_s
|
242
242
|
}
|
243
243
|
end
|
@@ -414,6 +414,33 @@ module Text
|
|
414
414
|
end
|
415
415
|
end
|
416
416
|
|
417
|
+
#
|
418
|
+
# Converts a unicode string to standard ASCII text.
|
419
|
+
#
|
420
|
+
def self.to_ascii(str='', type = 'utf-16le', mode = '', size = '')
|
421
|
+
return '' if not str
|
422
|
+
case type
|
423
|
+
when 'utf-16le'
|
424
|
+
return str.unpack('v*').pack('C*')
|
425
|
+
when 'utf-16be'
|
426
|
+
return str.unpack('n*').pack('C*')
|
427
|
+
when 'utf-32le'
|
428
|
+
return str.unpack('V*').pack('C*')
|
429
|
+
when 'utf-32be'
|
430
|
+
return str.unpack('N*').pack('C*')
|
431
|
+
when 'utf-7'
|
432
|
+
raise TypeError, 'invalid utf type, not yet implemented'
|
433
|
+
when 'utf-8'
|
434
|
+
raise TypeError, 'invalid utf type, not yet implemented'
|
435
|
+
when 'uhwtfms' # suggested name from HD :P
|
436
|
+
raise TypeError, 'invalid utf type, not yet implemented'
|
437
|
+
when 'uhwtfms-half' # suggested name from HD :P
|
438
|
+
raise TypeError, 'invalid utf type, not yet implemented'
|
439
|
+
else
|
440
|
+
raise TypeError, 'invalid utf type'
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
417
444
|
#
|
418
445
|
# Encode a string in a manor useful for HTTP URIs and URI Parameters.
|
419
446
|
#
|
@@ -795,7 +822,7 @@ module Text
|
|
795
822
|
sets.size.times { counter << 0}
|
796
823
|
0.upto(len-1) do |i|
|
797
824
|
setnum = i % sets.size
|
798
|
-
|
825
|
+
|
799
826
|
puts counter.inspect
|
800
827
|
end
|
801
828
|
|
@@ -891,7 +918,8 @@ module Text
|
|
891
918
|
raise RuntimeError, "Invalid gzip compression level" if (level < 1 or level > 9)
|
892
919
|
|
893
920
|
s = ""
|
894
|
-
|
921
|
+
s.force_encoding('ASCII-8BIT') if s.respond_to?(:encoding)
|
922
|
+
gz = Zlib::GzipWriter.new(StringIO.new(s, 'wb'), level)
|
895
923
|
gz << str
|
896
924
|
gz.close
|
897
925
|
return s
|
@@ -904,7 +932,8 @@ module Text
|
|
904
932
|
raise RuntimeError, "Gzip support is not present." if (!zlib_present?)
|
905
933
|
|
906
934
|
s = ""
|
907
|
-
|
935
|
+
s.force_encoding('ASCII-8BIT') if s.respond_to?(:encoding)
|
936
|
+
gz = Zlib::GzipReader.new(StringIO.new(str, 'rb'))
|
908
937
|
s << gz.read
|
909
938
|
gz.close
|
910
939
|
return s
|
@@ -1034,6 +1063,28 @@ module Text
|
|
1034
1063
|
[bits.join].pack("B32").unpack("N")[0]
|
1035
1064
|
end
|
1036
1065
|
|
1066
|
+
#
|
1067
|
+
# Split a string by n charachter into an array
|
1068
|
+
#
|
1069
|
+
def self.split_to_a(str, n)
|
1070
|
+
if n > 0
|
1071
|
+
s = str.dup
|
1072
|
+
until s.empty?
|
1073
|
+
(ret ||= []).push s.slice!(0, n)
|
1074
|
+
end
|
1075
|
+
else
|
1076
|
+
ret = str
|
1077
|
+
end
|
1078
|
+
ret
|
1079
|
+
end
|
1080
|
+
|
1081
|
+
#
|
1082
|
+
#Pack a value as 64 bit litle endian; does not exist for Array.pack
|
1083
|
+
#
|
1084
|
+
def self.pack_int64le(val)
|
1085
|
+
[val & 0x00000000ffffffff, val >> 32].pack("V2")
|
1086
|
+
end
|
1087
|
+
|
1037
1088
|
|
1038
1089
|
protected
|
1039
1090
|
|
data/lib/rex/ui/output.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'rex/ui'
|
2
|
+
require 'pp'
|
2
3
|
|
3
4
|
module Rex
|
4
5
|
module Ui
|
@@ -79,11 +80,75 @@ module DispatcherShell
|
|
79
80
|
shell.update_prompt(prompt)
|
80
81
|
end
|
81
82
|
|
83
|
+
#
|
84
|
+
# Displays the help banner. With no arguments, this is just a list of
|
85
|
+
# all commands grouped by dispatcher. Otherwise, tries to use a method
|
86
|
+
# named cmd_#{+cmd+}_help for the first dispatcher that has a command
|
87
|
+
# named +cmd+.
|
88
|
+
#
|
89
|
+
def cmd_help(cmd=nil, *ignored)
|
90
|
+
if cmd
|
91
|
+
help_found = false
|
92
|
+
cmd_found = false
|
93
|
+
shell.dispatcher_stack.each do |dispatcher|
|
94
|
+
next unless dispatcher.respond_to?(:commands)
|
95
|
+
next if (dispatcher.commands.nil?)
|
96
|
+
next if (dispatcher.commands.length == 0)
|
97
|
+
|
98
|
+
if dispatcher.respond_to?("cmd_#{cmd}")
|
99
|
+
cmd_found = true
|
100
|
+
break unless dispatcher.respond_to? "cmd_#{cmd}_help"
|
101
|
+
dispatcher.send("cmd_#{cmd}_help")
|
102
|
+
help_found = true
|
103
|
+
break
|
104
|
+
end
|
105
|
+
end
|
106
|
+
print_error("No help for #{cmd}, try -h") if cmd_found and not help_found
|
107
|
+
print_error("No such command") if not cmd_found
|
108
|
+
else
|
109
|
+
print(shell.help_to_s)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
#
|
114
|
+
# Tab completion for the help command
|
115
|
+
#
|
116
|
+
# By default just returns a list of all commands in all dispatchers.
|
117
|
+
#
|
118
|
+
def cmd_help_tabs(str, words)
|
119
|
+
return [] if words.length > 1
|
120
|
+
|
121
|
+
tabs = []
|
122
|
+
shell.dispatcher_stack.each { |dispatcher|
|
123
|
+
tabs += dispatcher.commands.keys
|
124
|
+
}
|
125
|
+
return tabs
|
126
|
+
end
|
127
|
+
|
128
|
+
alias cmd_? cmd_help
|
129
|
+
|
130
|
+
|
82
131
|
#
|
83
132
|
# No tab completion items by default
|
84
133
|
#
|
85
134
|
attr_accessor :shell, :tab_complete_items
|
86
135
|
|
136
|
+
#
|
137
|
+
# Provide a generic tab completion for file names.
|
138
|
+
#
|
139
|
+
# If the only completion is a directory, this descends into that directory
|
140
|
+
# and continues completions with filenames contained within.
|
141
|
+
#
|
142
|
+
def tab_complete_filenames(str, words)
|
143
|
+
matches = ::Readline::FILENAME_COMPLETION_PROC.call(str)
|
144
|
+
if matches and matches.length == 1 and File.directory?(matches[0])
|
145
|
+
dir = matches[0]
|
146
|
+
dir += File::SEPARATOR if dir[-1,1] != File::SEPARATOR
|
147
|
+
matches = ::Readline::FILENAME_COMPLETION_PROC.call(dir)
|
148
|
+
end
|
149
|
+
matches
|
150
|
+
end
|
151
|
+
|
87
152
|
end
|
88
153
|
|
89
154
|
#
|
@@ -91,8 +156,6 @@ module DispatcherShell
|
|
91
156
|
#
|
92
157
|
include Shell
|
93
158
|
|
94
|
-
attr_accessor :on_command_proc
|
95
|
-
|
96
159
|
#
|
97
160
|
# Initialize the dispatcher shell.
|
98
161
|
#
|
@@ -148,20 +211,22 @@ module DispatcherShell
|
|
148
211
|
|
149
212
|
# If no command is set and it supports commands, add them all
|
150
213
|
if (tab_words.empty? and dispatcher.respond_to?('commands'))
|
151
|
-
items.concat(dispatcher.commands.
|
214
|
+
items.concat(dispatcher.commands.keys)
|
152
215
|
end
|
153
216
|
|
154
217
|
# If the dispatcher exports a tab completion function, use it
|
155
218
|
if(dispatcher.respond_to?('tab_complete_helper'))
|
156
219
|
res = dispatcher.tab_complete_helper(str, tab_words)
|
220
|
+
else
|
221
|
+
res = tab_complete_helper(dispatcher, str, tab_words)
|
222
|
+
end
|
157
223
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
end
|
224
|
+
if (res.nil?)
|
225
|
+
# A nil response indicates no optional arguments
|
226
|
+
return [''] if items.empty?
|
227
|
+
else
|
228
|
+
# Otherwise we add the completion items to the list
|
229
|
+
items.concat(res)
|
165
230
|
end
|
166
231
|
}
|
167
232
|
|
@@ -184,6 +249,26 @@ module DispatcherShell
|
|
184
249
|
}
|
185
250
|
end
|
186
251
|
|
252
|
+
#
|
253
|
+
# Provide command-specific tab completion
|
254
|
+
#
|
255
|
+
def tab_complete_helper(dispatcher, str, words)
|
256
|
+
items = []
|
257
|
+
|
258
|
+
tabs_meth = "cmd_#{words[0]}_tabs"
|
259
|
+
# Is the user trying to tab complete one of our commands?
|
260
|
+
if (dispatcher.commands.include?(words[0]) and dispatcher.respond_to?(tabs_meth))
|
261
|
+
res = dispatcher.send(tabs_meth, str, words)
|
262
|
+
return [] if res.nil?
|
263
|
+
items.concat(res)
|
264
|
+
else
|
265
|
+
# Avoid the default completion list for known commands
|
266
|
+
return []
|
267
|
+
end
|
268
|
+
|
269
|
+
return items
|
270
|
+
end
|
271
|
+
|
187
272
|
#
|
188
273
|
# Run a single command line.
|
189
274
|
#
|
data/lib/rex/ui/text/shell.rb
CHANGED
@@ -204,6 +204,7 @@ module Shell
|
|
204
204
|
def print_error(msg='')
|
205
205
|
return if (output.nil?)
|
206
206
|
|
207
|
+
self.on_print_proc.call(msg) if self.on_print_proc
|
207
208
|
# Errors are not subject to disabled output
|
208
209
|
log_output(output.print_error(msg))
|
209
210
|
end
|
@@ -214,6 +215,7 @@ module Shell
|
|
214
215
|
def print_status(msg='')
|
215
216
|
return if (disable_output == true)
|
216
217
|
|
218
|
+
self.on_print_proc.call(msg) if self.on_print_proc
|
217
219
|
log_output(output.print_status(msg))
|
218
220
|
end
|
219
221
|
|
@@ -223,6 +225,7 @@ module Shell
|
|
223
225
|
def print_good(msg='')
|
224
226
|
return if (disable_output == true)
|
225
227
|
|
228
|
+
self.on_print_proc.call(msg) if self.on_print_proc
|
226
229
|
log_output(output.print_good(msg))
|
227
230
|
end
|
228
231
|
|
@@ -232,6 +235,7 @@ module Shell
|
|
232
235
|
def print_line(msg='')
|
233
236
|
return if (disable_output == true)
|
234
237
|
|
238
|
+
self.on_print_proc.call(msg) if self.on_print_proc
|
235
239
|
log_output(output.print_line(msg))
|
236
240
|
end
|
237
241
|
|
@@ -240,6 +244,7 @@ module Shell
|
|
240
244
|
#
|
241
245
|
def print(msg='')
|
242
246
|
return if (disable_output == true)
|
247
|
+
self.on_print_proc.call(msg) if self.on_print_proc
|
243
248
|
log_output(output.print(msg))
|
244
249
|
end
|
245
250
|
|
@@ -256,6 +261,9 @@ module Shell
|
|
256
261
|
#
|
257
262
|
attr_reader :output
|
258
263
|
|
264
|
+
attr_accessor :on_command_proc
|
265
|
+
attr_accessor :on_print_proc
|
266
|
+
|
259
267
|
protected
|
260
268
|
|
261
269
|
#
|