rex-core 0.1.34 → 0.1.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6710f1bd644f60c0caf4796979be01791425c0d5a2092105e9038debd835ea9a
4
- data.tar.gz: 1b1631796274f047cc79c502423bfee9e6dea5b0d0cf29e82ce82521bd07d25e
3
+ metadata.gz: 722c1d73aa2712e77940801d1f02e44565e0234944e7c50d8d82f6db845bce0f
4
+ data.tar.gz: 295ecbe54da2c7b54f6f19b7c8676140fcb2f18dcc660eaedfef7aa3c5633b9a
5
5
  SHA512:
6
- metadata.gz: 0fab85212fe9d2148d61e07640d33d28b39a0b0beedb2684efa0ac119f336f03c2248eddceba6dd9c310f98c50585393ccd3ebebb79471c7f35c52c2215617d6
7
- data.tar.gz: 7b499c51ead53b5e0acd7e15e6f0cdc94d988f96a37f2bb38c02b3f6a5877bf72ee5aabcfc3d69cbcccf53745a6a9737961dfb1c59c51badfe67adb1cfcd6226
6
+ metadata.gz: e2e5510ad24f1770fb127efd2cee8b0b08653be57eb15b7c44e51ef03a1fc6d23c5636259c632544a1d7ed17fa08dca289ee2dc3161c730803ab852a63ec677f
7
+ data.tar.gz: a5998d868389e099d9108dcdf9a4f86d0e91b6053f2cc3cb2fea75e84e3f5ddfa86136d018f70cd150817e86d12a455f4bce9ddcf0794f1ab512a64c606c4f40
@@ -1,5 +1,5 @@
1
1
  module Rex
2
2
  module Core
3
- VERSION = "0.1.34"
3
+ VERSION = "0.1.35"
4
4
  end
5
5
  end
@@ -0,0 +1,156 @@
1
+ require 'fiber'
2
+ require 'io/nonblock'
3
+ require 'rex/compat'
4
+
5
+ module Rex
6
+ module IO
7
+
8
+ # This fiber scheduler is heavily base on
9
+ # https://blog.monotone.dev/ruby/2020/12/25/ruby-3-fiber.html
10
+ class FiberScheduler
11
+ def initialize
12
+ @readable = {}
13
+ @writable = {}
14
+ @waiting = {}
15
+ @ready = []
16
+ @pending = []
17
+ @blocking = 0
18
+ @urgent = Rex::Compat.pipe
19
+ @mutex = Mutex.new
20
+ end
21
+
22
+ # This allows the fiber to be scheduled in the #run thread from another thread
23
+ def schedule_fiber(&block)
24
+ @mutex.synchronize do
25
+ @pending << block
26
+ end
27
+ # Wake up the scheduler
28
+ @urgent.last.write_nonblock('.') rescue nil
29
+ end
30
+
31
+ def run
32
+ while @readable.any? or @writable.any? or @waiting.any? or @blocking.positive? or @ready.any? or @pending.any?
33
+ # Start any pending fibers
34
+ pending_blocks = []
35
+ @mutex.synchronize do
36
+ pending_blocks = @pending.dup
37
+ @pending.clear
38
+ end
39
+
40
+ pending_blocks.each do |block|
41
+ fiber = Fiber.new(blocking: false, &block)
42
+ fiber.resume
43
+ end
44
+
45
+ begin
46
+ readable, writable = ::IO.select(@readable.keys + [@urgent.first], @writable.keys, [], 0.1)
47
+ rescue ::IOError
48
+ cleanup_closed_ios
49
+ next
50
+ end
51
+
52
+ # Drain the urgent pipe
53
+ if readable&.include?(@urgent.first)
54
+ @urgent.first.read_nonblock(1024) rescue nil
55
+ end
56
+
57
+ readable&.each do |io|
58
+ next if io == @urgent.first
59
+
60
+ if fiber = @readable.delete(io)
61
+ fiber.resume
62
+ end
63
+ end
64
+
65
+ writable&.each do |io|
66
+ if fiber = @writable.delete(io)
67
+ fiber.resume
68
+ end
69
+ end
70
+
71
+ @waiting.keys.each do |fiber|
72
+ if current_time > @waiting[fiber]
73
+ @waiting.delete(fiber)
74
+ fiber.resume
75
+ end
76
+ end
77
+
78
+ ready, @ready = @ready, []
79
+ ready.each do |fiber|
80
+ fiber.resume
81
+ end
82
+ end
83
+ end
84
+
85
+ def io_wait(io, events, timeout)
86
+ unless (events & ::IO::READABLE).zero?
87
+ @readable[io] = Fiber.current
88
+ end
89
+ unless (events & ::IO::WRITABLE).zero?
90
+ @writable[io] = Fiber.current
91
+ end
92
+
93
+ Fiber.yield
94
+ events
95
+ end
96
+
97
+ def kernel_sleep(duration = nil)
98
+ block(:sleep, duration)
99
+ true
100
+ end
101
+
102
+ def block(blocker, timeout = nil)
103
+ if timeout
104
+ @waiting[Fiber.current] = current_time + timeout
105
+ begin
106
+ Fiber.yield
107
+ ensure
108
+ @waiting.delete(Fiber.current)
109
+ end
110
+ else
111
+ @blocking += 1
112
+ begin
113
+ Fiber.yield
114
+ ensure
115
+ @blocking -= 1
116
+ end
117
+ end
118
+ end
119
+
120
+ def unblock(blocker, fiber)
121
+ @ready << fiber
122
+ io = @urgent.last
123
+ io.write_nonblock('.')
124
+ end
125
+
126
+ def close
127
+ run
128
+ @urgent.each(&:close)
129
+ @urgent = nil
130
+ end
131
+
132
+ def closed?
133
+ @urgent.nil?
134
+ end
135
+
136
+ def fiber(&block)
137
+ fiber = Fiber.new(blocking: false, &block)
138
+ fiber.resume
139
+ fiber
140
+ end
141
+
142
+ private
143
+
144
+ def current_time
145
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
146
+ end
147
+
148
+ def cleanup_closed_ios
149
+ @readable.delete_if { |io, _| io.closed? rescue true }
150
+ @writable.delete_if { |io, _| io.closed? rescue true }
151
+ @waiting.delete_if { |fiber, _| !fiber || !fiber.alive? rescue true }
152
+ end
153
+ end
154
+
155
+ end
156
+ end
@@ -0,0 +1,113 @@
1
+ require 'rex/io/fiber_scheduler'
2
+
3
+ module Rex
4
+ module IO
5
+
6
+ # An IO RelayManager which will read data from a socket and write it to a sink
7
+ # using a background thread.
8
+ class RelayManager
9
+ attr_reader :thread
10
+
11
+ def initialize
12
+ @thread = nil
13
+ @scheduler = FiberScheduler.new
14
+ end
15
+
16
+ # Add a IO relay to the manager. This will start relaying data from the source
17
+ # socket to the destination sink immediately. An optional "on_exit" callback
18
+ # can be provided which will be called when the socket is closed.
19
+ #
20
+ # @param sock [::Socket] The source socket that data will be read from.
21
+ # @param sink [#write, #call] A data destination where read data will be sent. It is called
22
+ # with one parameter, the data to be transferred. If the object exposes a #write method, it will
23
+ # be called repeatedly all data is processed and the return value will be used to determine how
24
+ # much of the data was written. If the object exposes a #call method, it will be called once and
25
+ # must handle processing all the data it is provided.
26
+ # @param name [String] A human-friendly name for the relay used in debug output.
27
+ # @param on_exit [#call] A callback to be invoked when sink can no longer be read from.
28
+ def add_relay(sock, sink: nil, name: nil, on_exit: nil)
29
+ @scheduler.schedule_fiber do
30
+ relay_fiber(sock, sink, name, on_exit: on_exit)
31
+ end
32
+
33
+ start unless running?
34
+ end
35
+
36
+ # Write all data to the specified IO. This is intended to be used in scenarios
37
+ # where partial writes are possible but not desirable.
38
+ #
39
+ # @param io [#write] An object to write the data to. It must return a number indicating
40
+ # how many bytes of the provided data were processed.
41
+ # @param data [String] The data that should be written.
42
+ def self.io_write_all(io, data)
43
+ offset = 0
44
+ while offset < data.bytesize
45
+ written = io.write(data.byteslice(offset..-1))
46
+ offset += written
47
+ end
48
+ data.bytesize
49
+ end
50
+
51
+ private
52
+
53
+ def running?
54
+ @thread && @thread.alive?
55
+ end
56
+
57
+ def start
58
+ return false if running?
59
+
60
+ @thread = Thread.new { run }
61
+ true
62
+ end
63
+
64
+ def run
65
+ old_scheduler = Fiber.scheduler
66
+ # A fiber scheduler can be set per-thread
67
+ Fiber.set_scheduler(@scheduler)
68
+
69
+ # Run the scheduler (blocks here)
70
+ @scheduler.run
71
+ ensure
72
+ Fiber.set_scheduler(old_scheduler)
73
+ end
74
+
75
+ def relay_fiber(sock, sink, name, on_exit: nil)
76
+ loop do
77
+ break if sock.closed?
78
+
79
+ buf = sock.readpartial(32_768)
80
+ write_to_sink(sink, buf)
81
+ end
82
+ rescue EOFError
83
+ nil
84
+ rescue => e
85
+ message = "#{self.class.name}#relay_fiber(name: #{name}): #{e.class} #{e}"
86
+ if defined?(elog) # elog is defined by framework, otherwise use stderr
87
+ elog(message, error: e)
88
+ else
89
+ $stderr.puts message
90
+ end
91
+ ensure
92
+ unless sock.closed?
93
+ sock.close rescue nil
94
+ end
95
+
96
+ on_exit.call if on_exit
97
+ end
98
+
99
+ def write_to_sink(sink, data)
100
+ if sink.respond_to?(:write)
101
+ self.class.io_write_all(sink, data)
102
+
103
+ elsif sink.respond_to?(:call)
104
+ sink.call(data)
105
+
106
+ else
107
+ raise ArgumentError, "Unsupported sink type: #{sink.inspect}"
108
+ end
109
+ end
110
+ end
111
+
112
+ end
113
+ end
@@ -1,7 +1,8 @@
1
1
  # -*- coding: binary -*-
2
2
 
3
3
  require 'socket'
4
- require 'fcntl'
4
+
5
+ require 'rex/io/relay_manager'
5
6
 
6
7
  module Rex
7
8
  module IO
@@ -12,6 +13,7 @@ module Rex
12
13
  #
13
14
  ###
14
15
  module SocketAbstraction
16
+
15
17
  ###
16
18
  #
17
19
  # Extension information for required Stream interface.
@@ -54,7 +56,7 @@ module Rex
54
56
  def cleanup_abstraction
55
57
  lsock.close if lsock and !lsock.closed?
56
58
 
57
- monitor_thread.join if monitor_thread&.alive?
59
+ monitor_thread.join if monitor_thread&.alive? && monitor_thread&.object_id != Thread.current.object_id
58
60
 
59
61
  rsock.close if rsock and !rsock.closed?
60
62
 
@@ -114,105 +116,27 @@ module Rex
114
116
  #
115
117
  attr_reader :rsock
116
118
 
117
- module MonitoredRSock
118
- def close
119
- @close_requested = true
120
- @monitor_thread.join
121
- nil
122
- end
119
+ protected
123
120
 
124
- def sysclose
125
- self.class.instance_method(:close).bind(self).call
121
+ def monitor_rsock(name = 'MonitorRemote')
122
+ if respond_to?(:close_write)
123
+ on_exit = method(:close_write)
124
+ else
125
+ on_exit = nil
126
126
  end
127
127
 
128
- attr_reader :close_requested
129
- attr_writer :monitor_thread
128
+ monitor_sock(rsock, sink: self, name: name, on_exit: on_exit)
130
129
  end
131
130
 
132
- protected
131
+ def monitor_sock(sock, sink:, name:, on_exit: nil)
132
+ @relay_manager ||= Rex::IO::RelayManager.new
133
+ @relay_manager.add_relay(sock, sink: sink, name: name, on_exit: on_exit)
134
+ end
133
135
 
134
- def monitor_rsock(threadname = 'SocketMonitorRemote')
135
- rsock.extend(MonitoredRSock)
136
- rsock.monitor_thread = self.monitor_thread = Rex::ThreadFactory.spawn(threadname, false) do
137
- loop do
138
- closed = rsock.nil? || rsock.close_requested
139
-
140
- if closed
141
- wlog('monitor_rsock: the remote socket has been closed, exiting loop')
142
- break
143
- end
144
-
145
- buf = nil
146
-
147
- begin
148
- s = Rex::ThreadSafe.select([rsock], nil, nil, 0.2)
149
- next if s.nil? || s[0].nil?
150
- rescue Exception => e
151
- wlog("monitor_rsock: exception during select: #{e.class} #{e}")
152
- closed = true
153
- end
154
-
155
- unless closed
156
- begin
157
- buf = rsock.sysread(32_768)
158
- if buf.nil?
159
- closed = true
160
- wlog('monitor_rsock: closed remote socket due to nil read')
161
- end
162
- rescue EOFError => e
163
- closed = true
164
- dlog('monitor_rsock: EOF in rsock')
165
- rescue ::Exception => e
166
- closed = true
167
- wlog("monitor_rsock: exception during read: #{e.class} #{e}")
168
- end
169
- end
170
-
171
- unless closed
172
- total_sent = 0
173
- total_length = buf.length
174
- while total_sent < total_length
175
- begin
176
- data = buf[total_sent, buf.length]
177
-
178
- # Note that this must be write() NOT syswrite() or put() or anything like it.
179
- # Using syswrite() breaks SSL streams.
180
- sent = write(data)
181
-
182
- # sf: Only remove the data off the queue is write was successful.
183
- # This way we naturally perform a resend if a failure occurred.
184
- # Catches an edge case with meterpreter TCP channels where remote send
185
- # fails gracefully and a resend is required.
186
- if sent.nil?
187
- closed = true
188
- wlog('monitor_rsock: failed writing, socket must be dead')
189
- break
190
- elsif sent > 0
191
- total_sent += sent
192
- end
193
- rescue ::IOError, ::EOFError => e
194
- closed = true
195
- wlog("monitor_rsock: exception during write: #{e.class} #{e}")
196
- break
197
- end
198
- end
199
- end
200
-
201
- next unless closed
202
-
203
- begin
204
- close_write if respond_to?('close_write')
205
- rescue StandardError
206
- end
207
-
208
- break
209
- end
210
-
211
- rsock.sysclose
212
- end
136
+ def monitor_thread
137
+ @relay_manager&.thread
213
138
  end
214
139
 
215
- attr_accessor :monitor_thread
216
140
  attr_writer :lsock, :rsock
217
141
  end
218
142
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rex-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.34
4
+ version: 0.1.35
5
5
  platform: ruby
6
6
  authors:
7
7
  - Metasploit Hackers
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-05-02 00:00:00.000000000 Z
11
+ date: 2025-12-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -62,6 +62,8 @@ files:
62
62
  - lib/rex/exceptions.rb
63
63
  - lib/rex/file.rb
64
64
  - lib/rex/io/datagram_abstraction.rb
65
+ - lib/rex/io/fiber_scheduler.rb
66
+ - lib/rex/io/relay_manager.rb
65
67
  - lib/rex/io/ring_buffer.rb
66
68
  - lib/rex/io/socket_abstraction.rb
67
69
  - lib/rex/io/stream.rb