arachni-reactor 0.1.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.md +29 -0
- data/README.md +79 -0
- data/Rakefile +53 -0
- data/lib/arachni/reactor.rb +679 -0
- data/lib/arachni/reactor/connection.rb +302 -0
- data/lib/arachni/reactor/connection/callbacks.rb +73 -0
- data/lib/arachni/reactor/connection/error.rb +114 -0
- data/lib/arachni/reactor/connection/peer_info.rb +92 -0
- data/lib/arachni/reactor/connection/tls.rb +107 -0
- data/lib/arachni/reactor/global.rb +26 -0
- data/lib/arachni/reactor/iterator.rb +251 -0
- data/lib/arachni/reactor/queue.rb +91 -0
- data/lib/arachni/reactor/tasks.rb +107 -0
- data/lib/arachni/reactor/tasks/base.rb +59 -0
- data/lib/arachni/reactor/tasks/delayed.rb +35 -0
- data/lib/arachni/reactor/tasks/one_off.rb +30 -0
- data/lib/arachni/reactor/tasks/periodic.rb +60 -0
- data/lib/arachni/reactor/tasks/persistent.rb +31 -0
- data/lib/arachni/reactor/version.rb +15 -0
- data/spec/arachni/reactor/connection/tls_spec.rb +332 -0
- data/spec/arachni/reactor/connection_spec.rb +58 -0
- data/spec/arachni/reactor/iterator_spec.rb +203 -0
- data/spec/arachni/reactor/queue_spec.rb +91 -0
- data/spec/arachni/reactor/tasks/base.rb +8 -0
- data/spec/arachni/reactor/tasks/delayed_spec.rb +54 -0
- data/spec/arachni/reactor/tasks/one_off_spec.rb +51 -0
- data/spec/arachni/reactor/tasks/periodic_spec.rb +40 -0
- data/spec/arachni/reactor/tasks/persistent_spec.rb +39 -0
- data/spec/arachni/reactor/tasks_spec.rb +136 -0
- data/spec/arachni/reactor_spec.rb +20 -0
- data/spec/arachni/reactor_tls_spec.rb +20 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/support/fixtures/handlers/echo_client.rb +34 -0
- data/spec/support/fixtures/handlers/echo_client_tls.rb +10 -0
- data/spec/support/fixtures/handlers/echo_server.rb +12 -0
- data/spec/support/fixtures/handlers/echo_server_tls.rb +8 -0
- data/spec/support/fixtures/pems/cacert.pem +37 -0
- data/spec/support/fixtures/pems/client/cert.pem +37 -0
- data/spec/support/fixtures/pems/client/foo-cert.pem +39 -0
- data/spec/support/fixtures/pems/client/foo-key.pem +51 -0
- data/spec/support/fixtures/pems/client/key.pem +51 -0
- data/spec/support/fixtures/pems/server/cert.pem +37 -0
- data/spec/support/fixtures/pems/server/key.pem +51 -0
- data/spec/support/helpers/paths.rb +23 -0
- data/spec/support/helpers/utilities.rb +117 -0
- data/spec/support/lib/server_option_parser.rb +29 -0
- data/spec/support/lib/servers.rb +133 -0
- data/spec/support/lib/servers/runner.rb +13 -0
- data/spec/support/servers/echo.rb +14 -0
- data/spec/support/servers/echo_tls.rb +22 -0
- data/spec/support/servers/echo_unix.rb +14 -0
- data/spec/support/servers/echo_unix_tls.rb +22 -0
- data/spec/support/shared/connection.rb +778 -0
- data/spec/support/shared/reactor.rb +785 -0
- data/spec/support/shared/task.rb +21 -0
- metadata +141 -0
@@ -0,0 +1,302 @@
|
|
1
|
+
=begin
|
2
|
+
|
3
|
+
This file is part of the Arachni::Reactor project and may be subject to
|
4
|
+
redistribution and commercial restrictions. Please see the Arachni::Reactor
|
5
|
+
web site for more information on licensing and terms of use.
|
6
|
+
|
7
|
+
=end
|
8
|
+
|
9
|
+
require_relative 'connection/error'
|
10
|
+
require_relative 'connection/peer_info'
|
11
|
+
require_relative 'connection/callbacks'
|
12
|
+
require_relative 'connection/tls'
|
13
|
+
|
14
|
+
module Arachni
|
15
|
+
class Reactor
|
16
|
+
|
17
|
+
# @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
|
18
|
+
class Connection
|
19
|
+
include PeerInfo
|
20
|
+
include Callbacks
|
21
|
+
|
22
|
+
# Maximum amount of data to be written or read at a time.
|
23
|
+
#
|
24
|
+
# We set this to the same max block size as the OpenSSL buffers because more
|
25
|
+
# than this tends to cause SSL errors and broken #select behavior --
|
26
|
+
# 1024 * 16 at the time of writing.
|
27
|
+
BLOCK_SIZE = OpenSSL::Buffering::BLOCK_SIZE
|
28
|
+
|
29
|
+
# @return [Socket]
|
30
|
+
# Ruby `Socket` associated with this connection.
|
31
|
+
attr_reader :socket
|
32
|
+
|
33
|
+
# @return [Reactor]
|
34
|
+
# Reactor associated with this connection.
|
35
|
+
attr_accessor :reactor
|
36
|
+
|
37
|
+
# @return [Symbol]
|
38
|
+
# `:client` or `:server`
|
39
|
+
attr_reader :role
|
40
|
+
|
41
|
+
# @return [Bool, nil]
|
42
|
+
# `true` when using a UNIX-domain socket, `nil` if no {#socket} is
|
43
|
+
# available, `false` otherwise.
|
44
|
+
def unix?
|
45
|
+
return if !to_io
|
46
|
+
return false if !Arachni::Reactor.supports_unix_sockets?
|
47
|
+
to_io.is_a?( UNIXServer ) || to_io.is_a?( UNIXSocket )
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [Bool]
|
51
|
+
# `true` when using an Internet socket, `nil` if no {#socket} is
|
52
|
+
# available, `false` otherwise.
|
53
|
+
def inet?
|
54
|
+
return if !to_io
|
55
|
+
to_io.is_a?( TCPServer ) || to_io.is_a?( TCPSocket ) || to_io.is_a?( Socket )
|
56
|
+
end
|
57
|
+
|
58
|
+
# @return [IO, nil]
|
59
|
+
# IO stream or `nil` if no {#socket} is available.
|
60
|
+
def to_io
|
61
|
+
return if !@socket
|
62
|
+
@socket.to_io
|
63
|
+
end
|
64
|
+
|
65
|
+
# @return [Bool]
|
66
|
+
# `true` if the connection is a server listener.
|
67
|
+
def listener?
|
68
|
+
return if !to_io
|
69
|
+
to_io.is_a?( TCPServer ) || (unix? && to_io.is_a?( UNIXServer ))
|
70
|
+
end
|
71
|
+
|
72
|
+
# @note The data will be buffered and sent in future {Reactor} ticks.
|
73
|
+
#
|
74
|
+
# @param [String] data
|
75
|
+
# Data to send to the peer.
|
76
|
+
def write( data )
|
77
|
+
@reactor.schedule do
|
78
|
+
write_buffer << data
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# @return [Bool]
|
83
|
+
# `true` if the connection is {Reactor#attached?} to a {#reactor},
|
84
|
+
# `false` otherwise.
|
85
|
+
def attached?
|
86
|
+
@reactor && @reactor.attached?( self )
|
87
|
+
end
|
88
|
+
|
89
|
+
# @return [Bool]
|
90
|
+
# `true` if the connection is not {Reactor#attached?} to a {#reactor},
|
91
|
+
# `false` otherwise.
|
92
|
+
def detached?
|
93
|
+
!attached?
|
94
|
+
end
|
95
|
+
|
96
|
+
# @note Will first detach if already {#attached?}.
|
97
|
+
# @note Sets {#reactor}.
|
98
|
+
# @note Calls {#on_attach}.
|
99
|
+
#
|
100
|
+
# @param [Reactor] reactor
|
101
|
+
# {Reactor} to which to attach {Reactor#attach}.
|
102
|
+
#
|
103
|
+
# @return [Bool]
|
104
|
+
# `true` if the connection was attached, `nil` if the connection was
|
105
|
+
# already attached.
|
106
|
+
def attach( reactor )
|
107
|
+
return if reactor.attached?( self )
|
108
|
+
detach if attached?
|
109
|
+
|
110
|
+
reactor.attach self
|
111
|
+
|
112
|
+
true
|
113
|
+
end
|
114
|
+
|
115
|
+
# @note Removes {#reactor}.
|
116
|
+
# @note Calls {#on_detach}.
|
117
|
+
#
|
118
|
+
# {Reactor#detach Detaches} `self` from the {#reactor}.
|
119
|
+
#
|
120
|
+
# @return [Bool]
|
121
|
+
# `true` if the connection was detached, `nil` if the connection was
|
122
|
+
# already detached.
|
123
|
+
def detach
|
124
|
+
return if detached?
|
125
|
+
|
126
|
+
@reactor.detach self
|
127
|
+
|
128
|
+
true
|
129
|
+
end
|
130
|
+
|
131
|
+
# @note Will not call {#on_close}.
|
132
|
+
#
|
133
|
+
# Closes the connection and {#detach detaches} it from the {Reactor}.
|
134
|
+
def close_without_callback
|
135
|
+
return if closed?
|
136
|
+
@closed = true
|
137
|
+
|
138
|
+
if listener? && unix? && (path = to_io.path) && File.exist?( path )
|
139
|
+
File.delete( path )
|
140
|
+
end
|
141
|
+
|
142
|
+
if @socket
|
143
|
+
@socket.close rescue nil
|
144
|
+
end
|
145
|
+
|
146
|
+
detach
|
147
|
+
|
148
|
+
nil
|
149
|
+
end
|
150
|
+
|
151
|
+
# @return [Bool]
|
152
|
+
# `true` if the connection has been {#close closed}, `false` otherwise.
|
153
|
+
def closed?
|
154
|
+
!!@closed
|
155
|
+
end
|
156
|
+
|
157
|
+
# @return [Bool]
|
158
|
+
# `true` if the connection has {#write outgoing data} that have not
|
159
|
+
# yet been {#write written}, `false` otherwise.
|
160
|
+
def has_outgoing_data?
|
161
|
+
!write_buffer.empty?
|
162
|
+
end
|
163
|
+
|
164
|
+
# @note Will call {#on_close} right before closing the socket and detaching
|
165
|
+
# from the Reactor.
|
166
|
+
#
|
167
|
+
# Closes the connection and {Reactor#detach detaches} it from the {Reactor}.
|
168
|
+
#
|
169
|
+
# @param [Exception] reason
|
170
|
+
# Reason for the close.
|
171
|
+
def close( reason = nil )
|
172
|
+
return if closed?
|
173
|
+
|
174
|
+
on_close reason
|
175
|
+
close_without_callback
|
176
|
+
nil
|
177
|
+
end
|
178
|
+
|
179
|
+
# @note Will call {#on_write} every time any of the buffer is consumed,
|
180
|
+
# can be multiple times when performing partial writes.
|
181
|
+
# @note Will call {#on_flush} once all of the buffer has been consumed.
|
182
|
+
#
|
183
|
+
# Processes a `write` event for this connection.
|
184
|
+
#
|
185
|
+
# Consumes and writes {BLOCK_SIZE} amount of data from the the beginning of
|
186
|
+
# the {#write} buffer to the socket.
|
187
|
+
#
|
188
|
+
# @return [Integer]
|
189
|
+
# Amount of the buffer consumed.
|
190
|
+
#
|
191
|
+
# @private
|
192
|
+
def _write
|
193
|
+
chunk = write_buffer.slice( 0, BLOCK_SIZE )
|
194
|
+
total_written = 0
|
195
|
+
|
196
|
+
begin
|
197
|
+
Error.translate do
|
198
|
+
# Send out the buffer, **all** of it, or at least try to.
|
199
|
+
loop do
|
200
|
+
total_written += written = @socket.write_nonblock( chunk )
|
201
|
+
write_buffer.slice!( 0, written )
|
202
|
+
|
203
|
+
# Call #on_write every time any of the buffer is consumed.
|
204
|
+
on_write
|
205
|
+
|
206
|
+
break if written == chunk.size
|
207
|
+
chunk.slice!( 0, written )
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
# Not ready to read or write yet, we'll catch it on future Reactor ticks.
|
212
|
+
rescue IO::WaitReadable, IO::WaitWritable
|
213
|
+
end
|
214
|
+
|
215
|
+
if write_buffer.empty?
|
216
|
+
@socket.flush
|
217
|
+
on_flush
|
218
|
+
end
|
219
|
+
|
220
|
+
total_written
|
221
|
+
rescue Error => e
|
222
|
+
close e
|
223
|
+
end
|
224
|
+
|
225
|
+
# @note If this is a server {#listener?} it will delegate to {#accept}.
|
226
|
+
# @note If this is a normal socket it will read {BLOCK_SIZE} amount of data.
|
227
|
+
# and pass it to {#on_read}.
|
228
|
+
#
|
229
|
+
# Processes a `read` event for this connection.
|
230
|
+
#
|
231
|
+
# @private
|
232
|
+
def _read
|
233
|
+
return accept if listener?
|
234
|
+
|
235
|
+
Error.translate do
|
236
|
+
on_read @socket.read_nonblock( BLOCK_SIZE )
|
237
|
+
end
|
238
|
+
|
239
|
+
# Not ready to read or write yet, we'll catch it on future Reactor ticks.
|
240
|
+
rescue IO::WaitReadable, IO::WaitWritable
|
241
|
+
rescue Error => e
|
242
|
+
close e
|
243
|
+
end
|
244
|
+
|
245
|
+
# Accepts a new client connection.
|
246
|
+
#
|
247
|
+
# @return [Connection, nil]
|
248
|
+
# New connection or `nil` if the socket isn't ready to accept new
|
249
|
+
# connections yet.
|
250
|
+
#
|
251
|
+
# @private
|
252
|
+
def accept
|
253
|
+
return if !(accepted = socket_accept)
|
254
|
+
|
255
|
+
connection = @server_handler.call
|
256
|
+
connection.configure accepted, :server
|
257
|
+
@reactor.attach connection
|
258
|
+
connection
|
259
|
+
end
|
260
|
+
|
261
|
+
# @param [Socket] socket
|
262
|
+
# Ruby `Socket` associated with this connection.
|
263
|
+
# @param [Symbol] role
|
264
|
+
# `:server` or `:client`.
|
265
|
+
# @param [Block] server_handler
|
266
|
+
# Block that generates a handler as specified in {Reactor#listen}.
|
267
|
+
#
|
268
|
+
# @private
|
269
|
+
def configure( socket, role, server_handler = nil )
|
270
|
+
@socket = socket
|
271
|
+
@role = role
|
272
|
+
@server_handler = server_handler
|
273
|
+
|
274
|
+
on_connect
|
275
|
+
|
276
|
+
nil
|
277
|
+
end
|
278
|
+
|
279
|
+
private
|
280
|
+
|
281
|
+
def write_buffer
|
282
|
+
@write_buffer ||= ''
|
283
|
+
end
|
284
|
+
|
285
|
+
# Accepts a new client connection.
|
286
|
+
#
|
287
|
+
# @return [Socket, nil]
|
288
|
+
# New connection or `nil` if the socket isn't ready to accept new
|
289
|
+
# connections yet.
|
290
|
+
#
|
291
|
+
# @private
|
292
|
+
def socket_accept
|
293
|
+
begin
|
294
|
+
@socket.accept_nonblock
|
295
|
+
rescue IO::WaitReadable, IO::WaitWritable
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
end
|
300
|
+
|
301
|
+
end
|
302
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
=begin
|
2
|
+
|
3
|
+
This file is part of the Arachni::Reactor project and may be subject to
|
4
|
+
redistribution and commercial restrictions. Please see the Arachni::Reactor
|
5
|
+
web site for more information on licensing and terms of use.
|
6
|
+
|
7
|
+
=end
|
8
|
+
|
9
|
+
module Arachni
|
10
|
+
class Reactor
|
11
|
+
class Connection
|
12
|
+
|
13
|
+
# Callbacks to be invoked per event.
|
14
|
+
#
|
15
|
+
# @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
|
16
|
+
module Callbacks
|
17
|
+
|
18
|
+
# Called after the connection has been established.
|
19
|
+
#
|
20
|
+
# @abstract
|
21
|
+
def on_connect
|
22
|
+
end
|
23
|
+
|
24
|
+
# Called after the connection has been attached to a {#reactor}.
|
25
|
+
#
|
26
|
+
# @abstract
|
27
|
+
def on_attach
|
28
|
+
end
|
29
|
+
|
30
|
+
# Called right the connection is detached from the {#reactor}.
|
31
|
+
#
|
32
|
+
# @abstract
|
33
|
+
def on_detach
|
34
|
+
end
|
35
|
+
|
36
|
+
# @note If a connection could not be established no {#socket} may be
|
37
|
+
# available.
|
38
|
+
#
|
39
|
+
# Called when the connection gets closed.
|
40
|
+
#
|
41
|
+
# @param [Exception] reason
|
42
|
+
# Reason for the close.
|
43
|
+
#
|
44
|
+
# @abstract
|
45
|
+
def on_close( reason )
|
46
|
+
end
|
47
|
+
|
48
|
+
# Called when data are available.
|
49
|
+
#
|
50
|
+
# @param [String] data
|
51
|
+
# Incoming data.
|
52
|
+
#
|
53
|
+
# @abstract
|
54
|
+
def on_read( data )
|
55
|
+
end
|
56
|
+
|
57
|
+
# Called after each {#write} call.
|
58
|
+
#
|
59
|
+
# @abstract
|
60
|
+
def on_write
|
61
|
+
end
|
62
|
+
|
63
|
+
# Called after the {#write buffered data} have all been sent to the peer.
|
64
|
+
#
|
65
|
+
# @abstract
|
66
|
+
def on_flush
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
=begin
|
2
|
+
|
3
|
+
This file is part of the Arachni::Reactor project and may be subject to
|
4
|
+
redistribution and commercial restrictions. Please see the Arachni::Reactor
|
5
|
+
web site for more information on licensing and terms of use.
|
6
|
+
|
7
|
+
=end
|
8
|
+
|
9
|
+
module Arachni
|
10
|
+
class Reactor
|
11
|
+
class Connection
|
12
|
+
|
13
|
+
# {Connection} error namespace.
|
14
|
+
#
|
15
|
+
# All {Connection} errors inherit from and live under it.
|
16
|
+
#
|
17
|
+
# @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
|
18
|
+
class Error < Arachni::Reactor::Error
|
19
|
+
|
20
|
+
class << self
|
21
|
+
# Captures Ruby exceptions and converts them to an appropriate
|
22
|
+
# subclass of {Error}.
|
23
|
+
#
|
24
|
+
# @param [Block] block Block to run.
|
25
|
+
def translate( &block )
|
26
|
+
block.call
|
27
|
+
rescue IOError, Errno::ENOTCONN => e
|
28
|
+
raise_with_proper_backtrace( e, Closed )
|
29
|
+
rescue SocketError, Errno::ENOENT => e
|
30
|
+
raise_with_proper_backtrace( e, HostNotFound )
|
31
|
+
rescue Errno::EPIPE => e
|
32
|
+
raise_with_proper_backtrace( e, BrokenPipe )
|
33
|
+
rescue Errno::ECONNREFUSED,
|
34
|
+
# JRuby throws Errno::EADDRINUSE when trying to connect to a
|
35
|
+
# non-existent server.
|
36
|
+
Errno::EADDRINUSE => e
|
37
|
+
raise_with_proper_backtrace( e, Refused )
|
38
|
+
rescue Errno::ECONNRESET => e
|
39
|
+
raise_with_proper_backtrace( e, Reset )
|
40
|
+
rescue Errno::EACCES => e
|
41
|
+
raise_with_proper_backtrace( e, Permission )
|
42
|
+
|
43
|
+
# Catch and forward these before handling OpenSSL::OpenSSLError because
|
44
|
+
# all SSL errors inherit from it, including OpenSSL::SSL::SSLErrorWaitReadable
|
45
|
+
# and OpenSSL::SSL::SSLErrorWaitWritable which also inherit from
|
46
|
+
# IO::WaitReadable and IO::WaitWritable and need special treatment.
|
47
|
+
rescue IO::WaitReadable, IO::WaitWritable, Errno::EINPROGRESS
|
48
|
+
raise
|
49
|
+
|
50
|
+
# We're mainly interested in translating SSL handshake errors but there
|
51
|
+
# aren't any specific exceptions for these.
|
52
|
+
#
|
53
|
+
# Why make things easy and clean, right?
|
54
|
+
rescue OpenSSL::OpenSSLError => e
|
55
|
+
raise_with_proper_backtrace( e, SSL )
|
56
|
+
end
|
57
|
+
|
58
|
+
def raise_with_proper_backtrace( ruby, arachni )
|
59
|
+
e = arachni.new( ruby.to_s )
|
60
|
+
e.set_backtrace ruby.backtrace
|
61
|
+
raise e
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Like a `SocketError.getaddrinfo` exception.
|
66
|
+
#
|
67
|
+
# @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
|
68
|
+
class HostNotFound < Error
|
69
|
+
end
|
70
|
+
|
71
|
+
# Like a `Errno::EACCES` exception.
|
72
|
+
#
|
73
|
+
# @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
|
74
|
+
class Permission < Error
|
75
|
+
end
|
76
|
+
|
77
|
+
# Like a `Errno::ECONNREFUSED` exception.
|
78
|
+
#
|
79
|
+
# @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
|
80
|
+
class Refused < Error
|
81
|
+
end
|
82
|
+
|
83
|
+
# Like a `Errno::ECONNRESET` exception.
|
84
|
+
#
|
85
|
+
# @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
|
86
|
+
class Reset < Error
|
87
|
+
end
|
88
|
+
|
89
|
+
# Like a `Errno::EPIPE` exception.
|
90
|
+
#
|
91
|
+
# @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
|
92
|
+
class BrokenPipe < Error
|
93
|
+
end
|
94
|
+
|
95
|
+
# @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
|
96
|
+
class Timeout < Error
|
97
|
+
end
|
98
|
+
|
99
|
+
# Like a `IOError` exception.
|
100
|
+
#
|
101
|
+
# @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
|
102
|
+
class Closed < Error
|
103
|
+
end
|
104
|
+
|
105
|
+
# Like a `OpenSSL::OpenSSLError` exception.
|
106
|
+
#
|
107
|
+
# @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
|
108
|
+
class SSL < Error
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|