raptor-io 0.0.1
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 +15 -0
- data/LICENSE +30 -0
- data/README.md +51 -0
- data/lib/rack/handler/raptor-io.rb +130 -0
- data/lib/raptor-io.rb +11 -0
- data/lib/raptor-io/error.rb +19 -0
- data/lib/raptor-io/protocol.rb +6 -0
- data/lib/raptor-io/protocol/error.rb +10 -0
- data/lib/raptor-io/protocol/http.rb +34 -0
- data/lib/raptor-io/protocol/http/client.rb +685 -0
- data/lib/raptor-io/protocol/http/error.rb +16 -0
- data/lib/raptor-io/protocol/http/headers.rb +132 -0
- data/lib/raptor-io/protocol/http/message.rb +67 -0
- data/lib/raptor-io/protocol/http/request.rb +307 -0
- data/lib/raptor-io/protocol/http/request/manipulator.rb +117 -0
- data/lib/raptor-io/protocol/http/request/manipulators.rb +217 -0
- data/lib/raptor-io/protocol/http/request/manipulators/authenticator.rb +110 -0
- data/lib/raptor-io/protocol/http/request/manipulators/authenticators/basic.rb +36 -0
- data/lib/raptor-io/protocol/http/request/manipulators/authenticators/digest.rb +135 -0
- data/lib/raptor-io/protocol/http/request/manipulators/authenticators/negotiate.rb +69 -0
- data/lib/raptor-io/protocol/http/request/manipulators/authenticators/ntlm.rb +29 -0
- data/lib/raptor-io/protocol/http/request/manipulators/redirect_follower.rb +65 -0
- data/lib/raptor-io/protocol/http/response.rb +166 -0
- data/lib/raptor-io/protocol/http/server.rb +446 -0
- data/lib/raptor-io/ruby.rb +4 -0
- data/lib/raptor-io/ruby/hash.rb +24 -0
- data/lib/raptor-io/ruby/ipaddr.rb +15 -0
- data/lib/raptor-io/ruby/openssl.rb +23 -0
- data/lib/raptor-io/ruby/string.rb +27 -0
- data/lib/raptor-io/socket.rb +175 -0
- data/lib/raptor-io/socket/comm.rb +143 -0
- data/lib/raptor-io/socket/comm/local.rb +94 -0
- data/lib/raptor-io/socket/comm/sapni.rb +75 -0
- data/lib/raptor-io/socket/comm/socks.rb +237 -0
- data/lib/raptor-io/socket/comm_chain.rb +30 -0
- data/lib/raptor-io/socket/error.rb +45 -0
- data/lib/raptor-io/socket/switch_board.rb +183 -0
- data/lib/raptor-io/socket/switch_board/route.rb +42 -0
- data/lib/raptor-io/socket/tcp.rb +231 -0
- data/lib/raptor-io/socket/tcp/ssl.rb +77 -0
- data/lib/raptor-io/socket/tcp_server.rb +16 -0
- data/lib/raptor-io/socket/tcp_server/ssl.rb +52 -0
- data/lib/raptor-io/socket/udp.rb +0 -0
- data/lib/raptor-io/version.rb +6 -0
- data/lib/tasks/yard.rake +26 -0
- data/spec/rack/handler/raptor_spec.rb +140 -0
- data/spec/raptor-io/protocol/http/client_spec.rb +671 -0
- data/spec/raptor-io/protocol/http/headers_spec.rb +189 -0
- data/spec/raptor-io/protocol/http/message_spec.rb +5 -0
- data/spec/raptor-io/protocol/http/request/manipulators/authenticator_spec.rb +193 -0
- data/spec/raptor-io/protocol/http/request/manipulators/authenticators/basic_spec.rb +32 -0
- data/spec/raptor-io/protocol/http/request/manipulators/authenticators/digest_spec.rb +76 -0
- data/spec/raptor-io/protocol/http/request/manipulators/authenticators/negotiate_spec.rb +52 -0
- data/spec/raptor-io/protocol/http/request/manipulators/authenticators/ntlm_spec.rb +37 -0
- data/spec/raptor-io/protocol/http/request/manipulators/redirect_follower_spec.rb +51 -0
- data/spec/raptor-io/protocol/http/request/manipulators_spec.rb +202 -0
- data/spec/raptor-io/protocol/http/request_spec.rb +965 -0
- data/spec/raptor-io/protocol/http/response_spec.rb +236 -0
- data/spec/raptor-io/protocol/http/server_spec.rb +345 -0
- data/spec/raptor-io/ruby/hash_spec.rb +20 -0
- data/spec/raptor-io/ruby/string_spec.rb +20 -0
- data/spec/raptor-io/socket/comm/local_spec.rb +50 -0
- data/spec/raptor-io/socket/switch_board/route_spec.rb +49 -0
- data/spec/raptor-io/socket/switch_board_spec.rb +87 -0
- data/spec/raptor-io/socket/tcp/ssl_spec.rb +18 -0
- data/spec/raptor-io/socket/tcp_server/ssl_spec.rb +59 -0
- data/spec/raptor-io/socket/tcp_server_spec.rb +19 -0
- data/spec/raptor-io/socket/tcp_spec.rb +14 -0
- data/spec/raptor-io/socket_spec.rb +16 -0
- data/spec/raptor-io/version_spec.rb +10 -0
- data/spec/spec_helper.rb +56 -0
- data/spec/support/fixtures/raptor/protocol/http/request/manipulators/manifoolators/fooer.rb +25 -0
- data/spec/support/fixtures/raptor/protocol/http/request/manipulators/niccolo_machiavelli.rb +20 -0
- data/spec/support/fixtures/raptor/protocol/http/request/manipulators/options_validator.rb +28 -0
- data/spec/support/fixtures/raptor/socket/ssl_server.crt +18 -0
- data/spec/support/fixtures/raptor/socket/ssl_server.key +15 -0
- data/spec/support/lib/path_helpers.rb +11 -0
- data/spec/support/lib/webserver_option_parser.rb +26 -0
- data/spec/support/lib/webservers.rb +120 -0
- data/spec/support/shared/contexts/with_ssl_server.rb +70 -0
- data/spec/support/shared/contexts/with_tcp_server.rb +58 -0
- data/spec/support/shared/examples/raptor/comm_examples.rb +26 -0
- data/spec/support/shared/examples/raptor/protocols/http/message.rb +106 -0
- data/spec/support/shared/examples/raptor/socket_examples.rb +135 -0
- data/spec/support/webservers/raptor/protocols/http/client.rb +100 -0
- data/spec/support/webservers/raptor/protocols/http/client_close_connection.rb +29 -0
- data/spec/support/webservers/raptor/protocols/http/client_https.rb +43 -0
- data/spec/support/webservers/raptor/protocols/http/request/manipulators/authenticators/basic.rb +9 -0
- data/spec/support/webservers/raptor/protocols/http/request/manipulators/authenticators/digest.rb +22 -0
- data/spec/support/webservers/raptor/protocols/http/request/manipulators/redirect_follower.rb +11 -0
- metadata +336 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
require 'raptor-io/socket'
|
|
2
|
+
require 'logger'
|
|
3
|
+
|
|
4
|
+
module RaptorIO
|
|
5
|
+
module Protocol::HTTP
|
|
6
|
+
|
|
7
|
+
# HTTP Server class.
|
|
8
|
+
#
|
|
9
|
+
# @author Tasos Laskos <tasos_laskos@rapid7.com>
|
|
10
|
+
class Server
|
|
11
|
+
|
|
12
|
+
# IO#listen backlog, 5 seems to be the default setting in a lot of
|
|
13
|
+
# implementations.
|
|
14
|
+
LISTEN_BACKLOG = 5
|
|
15
|
+
|
|
16
|
+
# Default server options.
|
|
17
|
+
DEFAULT_OPTIONS = {
|
|
18
|
+
address: '0.0.0.0',
|
|
19
|
+
port: 4567,
|
|
20
|
+
ssl_context: nil,
|
|
21
|
+
request_mtu: 512,
|
|
22
|
+
response_mtu: 512,
|
|
23
|
+
timeout: 10,
|
|
24
|
+
logger: ::Logger.new( STDOUT ),
|
|
25
|
+
logger_level: Logger::INFO
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
# @return [String] Address of the server.
|
|
29
|
+
attr_reader :address
|
|
30
|
+
|
|
31
|
+
# @return [Integer] Port number of the server.
|
|
32
|
+
attr_reader :port
|
|
33
|
+
|
|
34
|
+
# @return [OpenSSL::SSL::SSLContext] SSL context to use.
|
|
35
|
+
attr_accessor :ssl_context
|
|
36
|
+
|
|
37
|
+
# @return [Integer] MTU for reading request bodies.
|
|
38
|
+
attr_reader :request_mtu
|
|
39
|
+
|
|
40
|
+
# @return [Integer] MTU for sending responses.
|
|
41
|
+
attr_reader :response_mtu
|
|
42
|
+
|
|
43
|
+
# @return [Integer] Configured connection timeout.
|
|
44
|
+
attr_reader :timeout
|
|
45
|
+
|
|
46
|
+
# @return [Integer] Amount of timed out connections.
|
|
47
|
+
attr_reader :timeouts
|
|
48
|
+
|
|
49
|
+
# @return [SwitchBoard]
|
|
50
|
+
# The routing table from which this {Server} will
|
|
51
|
+
# {Socket::SwitchBoard#create_tcp_server listen}.
|
|
52
|
+
attr_reader :switch_board
|
|
53
|
+
|
|
54
|
+
# @param [Hash{Symbol => String,nil}] options Request options.
|
|
55
|
+
# @option options [String] :address ('0.0.0.0')
|
|
56
|
+
# Address to bind to.
|
|
57
|
+
# @option options [Integer] :port (4567)
|
|
58
|
+
# Port number to listen on.
|
|
59
|
+
# @option options [OpenSSL::SSL::SSLContext] :ssl_context (nil)
|
|
60
|
+
# SSL context to use.
|
|
61
|
+
# @option options [Integer] :request_mtu (512)
|
|
62
|
+
# Buffer size for request reading -- only applies to requests with a
|
|
63
|
+
# Content-Length header.
|
|
64
|
+
# @option options [Integer] :response_mtu (512)
|
|
65
|
+
# Buffer size for response transmission -- helps keep the server responsive
|
|
66
|
+
# while transmitting large responses.
|
|
67
|
+
# @option options [#debug, #info, #warn, #error, #fatal ] :logger (Logger.new( STDOUT ))
|
|
68
|
+
# Timeout in seconds.
|
|
69
|
+
# @option options [Integer] :logger_level (Logger::INFO)
|
|
70
|
+
# Level of message severity for the `:logger`.
|
|
71
|
+
# @option options [Integer, Float] :timeout (10)
|
|
72
|
+
# Timeout (in seconds) for incoming requests.
|
|
73
|
+
#
|
|
74
|
+
# @param [#call] handler
|
|
75
|
+
# Handler to be passed each {Request} and populate an empty {Response}
|
|
76
|
+
# object.
|
|
77
|
+
def initialize( options = {}, &handler )
|
|
78
|
+
@switch_board = options.delete(:switch_board)
|
|
79
|
+
unless @switch_board.is_a?(::RaptorIO::Socket::SwitchBoard)
|
|
80
|
+
raise ArgumentError, 'Must provide a :switch_board'
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
DEFAULT_OPTIONS.merge( options ).each do |k, v|
|
|
84
|
+
begin
|
|
85
|
+
send( "#{k}=", try_dup( v ) )
|
|
86
|
+
rescue NoMethodError
|
|
87
|
+
instance_variable_set( "@#{k}".to_sym, try_dup( v ) )
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
@logger.level = @logger_level if @logger
|
|
92
|
+
|
|
93
|
+
@sockets = {
|
|
94
|
+
# Sockets ready to read from.
|
|
95
|
+
reads: [],
|
|
96
|
+
|
|
97
|
+
# Sockets ready to write to.
|
|
98
|
+
writes: [],
|
|
99
|
+
|
|
100
|
+
# IP address.
|
|
101
|
+
client_address: {}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# In progress/buffered requests.
|
|
105
|
+
@pending_requests = Hash.new do |h, socket|
|
|
106
|
+
h[socket] = {
|
|
107
|
+
# Buffered raw text request.
|
|
108
|
+
buffer: '',
|
|
109
|
+
|
|
110
|
+
# HTTP::Headers, parsed when in the :buffer.
|
|
111
|
+
headers: nil,
|
|
112
|
+
|
|
113
|
+
# Amount of the request body read, buffered to improve responsiveness
|
|
114
|
+
# when handling large requests based on the :request_mtu option.
|
|
115
|
+
body_bytes_read: 0,
|
|
116
|
+
|
|
117
|
+
timeout: @timeout
|
|
118
|
+
}
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# In progress/buffered responses.
|
|
122
|
+
@pending_responses = Hash.new do |h, socket|
|
|
123
|
+
h[socket] = {
|
|
124
|
+
# HTTP::Response object to transmit.
|
|
125
|
+
object: nil,
|
|
126
|
+
|
|
127
|
+
# Amount of HTTP::Response#to_s already sent, we buffer it for
|
|
128
|
+
# performance reasons based on the :response_mtu option.
|
|
129
|
+
bytes_sent: 0
|
|
130
|
+
}
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
@timeouts = 0
|
|
134
|
+
@stop = false
|
|
135
|
+
@running = false
|
|
136
|
+
@mutex = Mutex.new
|
|
137
|
+
|
|
138
|
+
@system_responses = {}
|
|
139
|
+
|
|
140
|
+
@handler = handler
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def ssl?
|
|
144
|
+
!!@ssl_context
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Starts the server.
|
|
148
|
+
def run
|
|
149
|
+
return if @server
|
|
150
|
+
|
|
151
|
+
@server = listen
|
|
152
|
+
synchronize { @running = true }
|
|
153
|
+
|
|
154
|
+
while !stop?
|
|
155
|
+
available_sockets = select_sockets
|
|
156
|
+
next if !available_sockets
|
|
157
|
+
|
|
158
|
+
# Go through the sockets which are available for reading.
|
|
159
|
+
available_sockets[:reads].each do |socket|
|
|
160
|
+
# Read and move to the next one if there are no new clients.
|
|
161
|
+
if socket != @server
|
|
162
|
+
read socket
|
|
163
|
+
next
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
begin
|
|
167
|
+
client = @server.accept_nonblock
|
|
168
|
+
rescue => e
|
|
169
|
+
log "#{e.class}: #{e}, #{client.inspect}", :error
|
|
170
|
+
e.backtrace.each { |l| log l, :error }
|
|
171
|
+
next
|
|
172
|
+
end
|
|
173
|
+
#$stderr.puts("http server accepted #{client.inspect}")
|
|
174
|
+
|
|
175
|
+
@sockets[:client_address][client] =
|
|
176
|
+
::Socket.unpack_sockaddr_in( client.getpeername ).last
|
|
177
|
+
@sockets[:reads] << client
|
|
178
|
+
|
|
179
|
+
log 'Connected', :debug, client
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Handle sockets which are ready to be written to.
|
|
183
|
+
available_sockets[:writes].each do |socket|
|
|
184
|
+
write socket
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Close sockets with errors.
|
|
188
|
+
available_sockets[:errors].each do |socket|
|
|
189
|
+
log 'Connection error', :error, socket
|
|
190
|
+
close socket
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
synchronize { @running = false }
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# {#run Runs} the server in a Thread and returns once it's ready.
|
|
198
|
+
def run_nonblock
|
|
199
|
+
ex = nil
|
|
200
|
+
Thread.new {
|
|
201
|
+
begin
|
|
202
|
+
run
|
|
203
|
+
rescue => e
|
|
204
|
+
log "#{e.class}: #{e}", :fatal
|
|
205
|
+
e.backtrace.each { |l| log l, :fatal }
|
|
206
|
+
|
|
207
|
+
synchronize { @running = true }
|
|
208
|
+
ex = e
|
|
209
|
+
end
|
|
210
|
+
}
|
|
211
|
+
sleep 0.1 while !running?
|
|
212
|
+
|
|
213
|
+
if ex
|
|
214
|
+
@running = false
|
|
215
|
+
raise ex
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# @return [Bool] `true` if the server is running, `false` otherwise.
|
|
220
|
+
def running?
|
|
221
|
+
synchronize { @running }
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Shuts down the server.
|
|
225
|
+
def stop
|
|
226
|
+
return if !@server
|
|
227
|
+
|
|
228
|
+
synchronize { @stop = true }
|
|
229
|
+
sleep 0.05 while running?
|
|
230
|
+
|
|
231
|
+
close @server
|
|
232
|
+
@server = nil
|
|
233
|
+
|
|
234
|
+
open_sockets.each { |socket| close socket }
|
|
235
|
+
|
|
236
|
+
true
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# @return [String] URL of the server.
|
|
240
|
+
def url
|
|
241
|
+
"http#{'s' if ssl?}://#{address}:#{port}/"
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
private
|
|
245
|
+
|
|
246
|
+
def select_sockets
|
|
247
|
+
clock = Time.now
|
|
248
|
+
sockets = Socket.select( [@server] | @sockets[:reads],
|
|
249
|
+
@sockets[:writes],
|
|
250
|
+
open_sockets,
|
|
251
|
+
@timeout )
|
|
252
|
+
waiting_time = Time.now - clock
|
|
253
|
+
|
|
254
|
+
# Adjust the timeouts for *all* sockets.
|
|
255
|
+
@pending_requests.each do |_, pending_request|
|
|
256
|
+
pending_request[:timeout] -= waiting_time
|
|
257
|
+
pending_request[:timeout] = 0 if pending_request[:timeout] < 0
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# One or more sockets timed out, find them and KILL them! Muahahaha!
|
|
261
|
+
if !sockets
|
|
262
|
+
@sockets[:reads].each do |socket|
|
|
263
|
+
# Close the socket if the client has exceeded their allotted time to
|
|
264
|
+
# make contact.
|
|
265
|
+
next if waiting_time < @pending_requests[socket][:timeout]
|
|
266
|
+
|
|
267
|
+
close socket
|
|
268
|
+
@timeouts += 1
|
|
269
|
+
|
|
270
|
+
log 'Timeout', :debug, socket
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
return
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
{
|
|
277
|
+
reads: sockets[0],
|
|
278
|
+
writes: sockets[1],
|
|
279
|
+
errors: sockets[2]
|
|
280
|
+
}
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def reset_timeout( socket )
|
|
284
|
+
@pending_requests[socket][:timeout] = @timeout
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def stop?
|
|
288
|
+
sleep 0.005
|
|
289
|
+
synchronize { @stop }
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def open_sockets
|
|
293
|
+
@sockets[:reads] | @sockets[:writes]
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def listen
|
|
297
|
+
server = @switch_board.create_tcp_server(
|
|
298
|
+
local_host: @address,
|
|
299
|
+
local_port: @port,
|
|
300
|
+
connect_timeout: @timeout,
|
|
301
|
+
ssl_context: @ssl_context
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
log "Listening on #{@address}:#{@port}."
|
|
305
|
+
|
|
306
|
+
server
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def read( socket )
|
|
310
|
+
reset_timeout( socket )
|
|
311
|
+
|
|
312
|
+
if (headers = @pending_requests[socket][:headers])
|
|
313
|
+
if (te = headers['Transfer-Encoding'])
|
|
314
|
+
if te.downcase == 'chunked'
|
|
315
|
+
read_size = socket.gets.to_s[0...-CRLF.size]
|
|
316
|
+
return if read_size.empty?
|
|
317
|
+
|
|
318
|
+
if (read_size = read_size.to_i( 16 )) > 0
|
|
319
|
+
@pending_requests[socket][:buffer] <<
|
|
320
|
+
socket.read( read_size + CRLF.size ).to_s[0...read_size]
|
|
321
|
+
return
|
|
322
|
+
end
|
|
323
|
+
else
|
|
324
|
+
@system_responses[socket] = Response.new(
|
|
325
|
+
code: 501,
|
|
326
|
+
body: 'Not implemented'
|
|
327
|
+
)
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
if (content_length = headers['content-length'])
|
|
332
|
+
content_length = content_length.to_i
|
|
333
|
+
remaining_ct = content_length - @pending_requests[socket][:body_bytes_read]
|
|
334
|
+
read_size = [remaining_ct, @request_mtu].min
|
|
335
|
+
|
|
336
|
+
@pending_requests[socket][:buffer] << socket.read( read_size )
|
|
337
|
+
@pending_requests[socket][:body_bytes_read] += read_size
|
|
338
|
+
|
|
339
|
+
return if content_length != @pending_requests[socket][:body_bytes_read]
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
handle_read_request( socket )
|
|
343
|
+
return
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
@pending_requests[socket][:buffer] << socket.gets.to_s
|
|
347
|
+
return if !(@pending_requests[socket][:buffer] =~ HEADER_SEPARATOR_PATTERN)
|
|
348
|
+
|
|
349
|
+
@pending_requests[socket][:headers] ||=
|
|
350
|
+
Request.parse( @pending_requests[socket][:buffer] ).headers
|
|
351
|
+
return if @pending_requests[socket][:headers].include?( 'content-length' )
|
|
352
|
+
return if @pending_requests[socket][:headers].include?( 'transfer-encoding' )
|
|
353
|
+
|
|
354
|
+
handle_read_request( socket )
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def handle_read_request( socket )
|
|
358
|
+
request = Request.parse( @pending_requests.delete( socket )[:buffer] )
|
|
359
|
+
request.client_address = @sockets[:client_address][socket]
|
|
360
|
+
|
|
361
|
+
if (sysres = @system_responses.delete( socket ))
|
|
362
|
+
sysres.request = request
|
|
363
|
+
@pending_responses[socket][:object] = sysres
|
|
364
|
+
else
|
|
365
|
+
@pending_responses[socket][:object] = handle_request( request )
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
@sockets[:writes] << @sockets[:reads].delete( socket )
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def handle_request( request )
|
|
372
|
+
response = Response.new( request: request )
|
|
373
|
+
|
|
374
|
+
if @handler
|
|
375
|
+
@handler.call response
|
|
376
|
+
else
|
|
377
|
+
response.code = 418
|
|
378
|
+
response.message = "I'm a teapot"
|
|
379
|
+
response.body = request.body
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
response
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def write( socket )
|
|
386
|
+
response = @pending_responses[socket][:object]
|
|
387
|
+
bytes_sent = @pending_responses[socket][:bytes_sent]
|
|
388
|
+
|
|
389
|
+
orig_response_string = response.to_s.repack
|
|
390
|
+
response_string = orig_response_string[bytes_sent..-1]
|
|
391
|
+
|
|
392
|
+
if response_string.size > @response_mtu
|
|
393
|
+
response_string = response_string[0...@response_mtu]
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
begin
|
|
397
|
+
@pending_responses[socket][:bytes_sent] += socket.write( response_string )
|
|
398
|
+
rescue IOError
|
|
399
|
+
@pending_responses.delete( socket )
|
|
400
|
+
close( socket )
|
|
401
|
+
return
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
return if @pending_responses[socket][:bytes_sent] != orig_response_string.size
|
|
405
|
+
|
|
406
|
+
@pending_responses.delete( socket )
|
|
407
|
+
request = response.request
|
|
408
|
+
|
|
409
|
+
log "#{request.http_method.upcase} #{request.resource} #{response.code}", :debug, socket
|
|
410
|
+
|
|
411
|
+
if request.keep_alive?
|
|
412
|
+
@sockets[:reads] << @sockets[:writes].delete( socket )
|
|
413
|
+
else
|
|
414
|
+
close( socket )
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def close( socket )
|
|
419
|
+
@sockets[:reads].delete( socket )
|
|
420
|
+
@sockets[:writes].delete( socket )
|
|
421
|
+
@sockets[:client_address].delete( socket )
|
|
422
|
+
socket.close
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def synchronize( &block )
|
|
426
|
+
@mutex.synchronize( &block )
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def log( message, severity = :info, socket = nil )
|
|
430
|
+
return if !@logger
|
|
431
|
+
|
|
432
|
+
if socket && @sockets[:client_address].include?( socket )
|
|
433
|
+
message += " [#{@sockets[:client_address][socket]}]"
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
@logger.send severity, message
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def try_dup( value )
|
|
440
|
+
value.dup rescue value
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
end
|
|
446
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Monkey-patches Ruby's stdlib `Hash` with a few convenience methods.
|
|
3
|
+
#
|
|
4
|
+
# @author Tasos Laskos <tasos_laskos@rapid7.com>
|
|
5
|
+
#
|
|
6
|
+
class Hash
|
|
7
|
+
|
|
8
|
+
# @return [Hash]
|
|
9
|
+
# Hash with +self+'s keys and values recursively converted to strings.
|
|
10
|
+
def stringify
|
|
11
|
+
stringified = {}
|
|
12
|
+
|
|
13
|
+
each do |k, v|
|
|
14
|
+
if v.is_a?( Hash )
|
|
15
|
+
stringified[k.to_s] = v.stringify
|
|
16
|
+
else
|
|
17
|
+
stringified[k.to_s] = v.to_s
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
stringified
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
end
|