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.
Files changed (91) hide show
  1. checksums.yaml +15 -0
  2. data/LICENSE +30 -0
  3. data/README.md +51 -0
  4. data/lib/rack/handler/raptor-io.rb +130 -0
  5. data/lib/raptor-io.rb +11 -0
  6. data/lib/raptor-io/error.rb +19 -0
  7. data/lib/raptor-io/protocol.rb +6 -0
  8. data/lib/raptor-io/protocol/error.rb +10 -0
  9. data/lib/raptor-io/protocol/http.rb +34 -0
  10. data/lib/raptor-io/protocol/http/client.rb +685 -0
  11. data/lib/raptor-io/protocol/http/error.rb +16 -0
  12. data/lib/raptor-io/protocol/http/headers.rb +132 -0
  13. data/lib/raptor-io/protocol/http/message.rb +67 -0
  14. data/lib/raptor-io/protocol/http/request.rb +307 -0
  15. data/lib/raptor-io/protocol/http/request/manipulator.rb +117 -0
  16. data/lib/raptor-io/protocol/http/request/manipulators.rb +217 -0
  17. data/lib/raptor-io/protocol/http/request/manipulators/authenticator.rb +110 -0
  18. data/lib/raptor-io/protocol/http/request/manipulators/authenticators/basic.rb +36 -0
  19. data/lib/raptor-io/protocol/http/request/manipulators/authenticators/digest.rb +135 -0
  20. data/lib/raptor-io/protocol/http/request/manipulators/authenticators/negotiate.rb +69 -0
  21. data/lib/raptor-io/protocol/http/request/manipulators/authenticators/ntlm.rb +29 -0
  22. data/lib/raptor-io/protocol/http/request/manipulators/redirect_follower.rb +65 -0
  23. data/lib/raptor-io/protocol/http/response.rb +166 -0
  24. data/lib/raptor-io/protocol/http/server.rb +446 -0
  25. data/lib/raptor-io/ruby.rb +4 -0
  26. data/lib/raptor-io/ruby/hash.rb +24 -0
  27. data/lib/raptor-io/ruby/ipaddr.rb +15 -0
  28. data/lib/raptor-io/ruby/openssl.rb +23 -0
  29. data/lib/raptor-io/ruby/string.rb +27 -0
  30. data/lib/raptor-io/socket.rb +175 -0
  31. data/lib/raptor-io/socket/comm.rb +143 -0
  32. data/lib/raptor-io/socket/comm/local.rb +94 -0
  33. data/lib/raptor-io/socket/comm/sapni.rb +75 -0
  34. data/lib/raptor-io/socket/comm/socks.rb +237 -0
  35. data/lib/raptor-io/socket/comm_chain.rb +30 -0
  36. data/lib/raptor-io/socket/error.rb +45 -0
  37. data/lib/raptor-io/socket/switch_board.rb +183 -0
  38. data/lib/raptor-io/socket/switch_board/route.rb +42 -0
  39. data/lib/raptor-io/socket/tcp.rb +231 -0
  40. data/lib/raptor-io/socket/tcp/ssl.rb +77 -0
  41. data/lib/raptor-io/socket/tcp_server.rb +16 -0
  42. data/lib/raptor-io/socket/tcp_server/ssl.rb +52 -0
  43. data/lib/raptor-io/socket/udp.rb +0 -0
  44. data/lib/raptor-io/version.rb +6 -0
  45. data/lib/tasks/yard.rake +26 -0
  46. data/spec/rack/handler/raptor_spec.rb +140 -0
  47. data/spec/raptor-io/protocol/http/client_spec.rb +671 -0
  48. data/spec/raptor-io/protocol/http/headers_spec.rb +189 -0
  49. data/spec/raptor-io/protocol/http/message_spec.rb +5 -0
  50. data/spec/raptor-io/protocol/http/request/manipulators/authenticator_spec.rb +193 -0
  51. data/spec/raptor-io/protocol/http/request/manipulators/authenticators/basic_spec.rb +32 -0
  52. data/spec/raptor-io/protocol/http/request/manipulators/authenticators/digest_spec.rb +76 -0
  53. data/spec/raptor-io/protocol/http/request/manipulators/authenticators/negotiate_spec.rb +52 -0
  54. data/spec/raptor-io/protocol/http/request/manipulators/authenticators/ntlm_spec.rb +37 -0
  55. data/spec/raptor-io/protocol/http/request/manipulators/redirect_follower_spec.rb +51 -0
  56. data/spec/raptor-io/protocol/http/request/manipulators_spec.rb +202 -0
  57. data/spec/raptor-io/protocol/http/request_spec.rb +965 -0
  58. data/spec/raptor-io/protocol/http/response_spec.rb +236 -0
  59. data/spec/raptor-io/protocol/http/server_spec.rb +345 -0
  60. data/spec/raptor-io/ruby/hash_spec.rb +20 -0
  61. data/spec/raptor-io/ruby/string_spec.rb +20 -0
  62. data/spec/raptor-io/socket/comm/local_spec.rb +50 -0
  63. data/spec/raptor-io/socket/switch_board/route_spec.rb +49 -0
  64. data/spec/raptor-io/socket/switch_board_spec.rb +87 -0
  65. data/spec/raptor-io/socket/tcp/ssl_spec.rb +18 -0
  66. data/spec/raptor-io/socket/tcp_server/ssl_spec.rb +59 -0
  67. data/spec/raptor-io/socket/tcp_server_spec.rb +19 -0
  68. data/spec/raptor-io/socket/tcp_spec.rb +14 -0
  69. data/spec/raptor-io/socket_spec.rb +16 -0
  70. data/spec/raptor-io/version_spec.rb +10 -0
  71. data/spec/spec_helper.rb +56 -0
  72. data/spec/support/fixtures/raptor/protocol/http/request/manipulators/manifoolators/fooer.rb +25 -0
  73. data/spec/support/fixtures/raptor/protocol/http/request/manipulators/niccolo_machiavelli.rb +20 -0
  74. data/spec/support/fixtures/raptor/protocol/http/request/manipulators/options_validator.rb +28 -0
  75. data/spec/support/fixtures/raptor/socket/ssl_server.crt +18 -0
  76. data/spec/support/fixtures/raptor/socket/ssl_server.key +15 -0
  77. data/spec/support/lib/path_helpers.rb +11 -0
  78. data/spec/support/lib/webserver_option_parser.rb +26 -0
  79. data/spec/support/lib/webservers.rb +120 -0
  80. data/spec/support/shared/contexts/with_ssl_server.rb +70 -0
  81. data/spec/support/shared/contexts/with_tcp_server.rb +58 -0
  82. data/spec/support/shared/examples/raptor/comm_examples.rb +26 -0
  83. data/spec/support/shared/examples/raptor/protocols/http/message.rb +106 -0
  84. data/spec/support/shared/examples/raptor/socket_examples.rb +135 -0
  85. data/spec/support/webservers/raptor/protocols/http/client.rb +100 -0
  86. data/spec/support/webservers/raptor/protocols/http/client_close_connection.rb +29 -0
  87. data/spec/support/webservers/raptor/protocols/http/client_https.rb +43 -0
  88. data/spec/support/webservers/raptor/protocols/http/request/manipulators/authenticators/basic.rb +9 -0
  89. data/spec/support/webservers/raptor/protocols/http/request/manipulators/authenticators/digest.rb +22 -0
  90. data/spec/support/webservers/raptor/protocols/http/request/manipulators/redirect_follower.rb +11 -0
  91. 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,4 @@
1
+ require 'raptor-io/ruby/string'
2
+ require 'raptor-io/ruby/hash'
3
+ require 'raptor-io/ruby/ipaddr'
4
+ require 'raptor-io/ruby/openssl'
@@ -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