raptor-io 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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