spider-gazelle 1.2.0 → 2.0.0

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.
@@ -0,0 +1,86 @@
1
+ require 'thread'
2
+
3
+ module SpiderGazelle
4
+ class Gazelle
5
+ module AppStore
6
+ @apps = []
7
+ @loaded = {}
8
+ @critical = Mutex.new
9
+ @logger = Logger.instance
10
+
11
+ # Load an app and assign it an ID
12
+ def self.load(rackup, options)
13
+ begin
14
+ @critical.synchronize {
15
+ return if @loaded[rackup]
16
+
17
+ app, opts = ::Rack::Builder.parse_file(rackup)
18
+ tls = configure_tls(options)
19
+
20
+ val = [app, options[:app_mode], options[:port], tls]
21
+ @apps << val
22
+ @loaded[rackup] = val
23
+ }
24
+ rescue Exception => e
25
+ # Prevent other threads from trying to load this too (might be in threaded mode)
26
+ @loaded[rackup] = true
27
+ @logger.print_error(e, "loading rackup #{rackup}")
28
+ Reactor.instance.shutdown
29
+ end
30
+ end
31
+
32
+ # Add an already loaded application
33
+ def self.add(app, options)
34
+ @critical.synchronize {
35
+ obj_id = app.object_id
36
+ return if @loaded[obj_id]
37
+
38
+ id = @apps.length
39
+ tls = configure_tls(options)
40
+
41
+ val = [app, options[:app_mode], options[:port], tls]
42
+ @apps << val
43
+ @loaded[obj_id] = val
44
+
45
+ id
46
+ }
47
+ end
48
+
49
+ # Lookup an application
50
+ def self.lookup(app)
51
+ @loaded[app.to_s]
52
+ end
53
+
54
+ # Get an app using the id directly
55
+ def self.get(id)
56
+ @apps[id.to_i]
57
+ end
58
+
59
+ PROTOCOLS = ['h2'.freeze, 'http/1.1'.freeze].freeze
60
+ FALLBACK = 'http/1.1'.freeze
61
+ def self.configure_tls(opts)
62
+ return false unless opts[:tls]
63
+
64
+ tls = {
65
+ protocols: PROTOCOLS,
66
+ fallback: FALLBACK
67
+ }
68
+ tls[:verify_peer] = true if opts[:verify_peer]
69
+ tls[:ciphers] = opts[:ciphers] if opts[:ciphers]
70
+
71
+ # NOTE:: Blocking reads however only during load so it's OK
72
+ private_key = opts[:private_key]
73
+ if private_key
74
+ tls[:private_key] = ::FFI::MemoryPointer.from_string(File.read(private_key))
75
+ end
76
+
77
+ cert_chain = opts[:cert_chain]
78
+ if cert_chain
79
+ tls[:cert_chain] = ::FFI::MemoryPointer.from_string(File.read(cert_chain))
80
+ end
81
+
82
+ tls
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,496 @@
1
+
2
+ require 'http-parser' # C based, fast, http parser
3
+ require 'spider-gazelle/gazelle/request'
4
+
5
+
6
+ module SpiderGazelle
7
+ class Gazelle
8
+ class Http1
9
+ class Callbacks
10
+ def initialize
11
+ @parser = ::HttpParser::Parser.new self
12
+ @logger = Logger.instance
13
+ end
14
+
15
+
16
+ attr_accessor :connection
17
+ attr_reader :parser
18
+
19
+
20
+ def on_message_begin(parser)
21
+ @connection.start_parsing
22
+ end
23
+
24
+ def on_url(parser, url)
25
+ @connection.parsing.url << url
26
+ end
27
+
28
+ def on_header_field(parser, header)
29
+ req = @connection.parsing
30
+ req.header.frozen? ? req.header = header : req.header << header
31
+ end
32
+
33
+ DASH = '-'.freeze
34
+ UNDERSCORE = '_'.freeze
35
+ HTTP_META = 'HTTP_'.freeze
36
+ COMMA = ', '.freeze
37
+
38
+ def on_header_value(parser, value)
39
+ req = @connection.parsing
40
+ if req.header.frozen?
41
+ req.env[req.header] << value
42
+ else
43
+ header = req.header
44
+ header.upcase!
45
+ header.gsub!(DASH, UNDERSCORE)
46
+ header.prepend(HTTP_META)
47
+ header.freeze
48
+ if req.env[header]
49
+ req.env[header] << COMMA
50
+ req.env[header] << value
51
+ else
52
+ req.env[header] = value
53
+ end
54
+ end
55
+ end
56
+
57
+ def on_headers_complete(parser)
58
+ @connection.headers_complete
59
+ end
60
+
61
+ def on_body(parser, data)
62
+ @connection.parsing.body << data
63
+ end
64
+
65
+ def on_message_complete(parser)
66
+ @connection.finished_parsing
67
+ end
68
+ end
69
+
70
+
71
+ Hijack = Struct.new :socket, :env
72
+
73
+
74
+ def initialize(return_method, callbacks, thread, logger)
75
+ # The HTTP parser callbacks object for this thread
76
+ @return_method = return_method
77
+ @callbacks = callbacks
78
+ @thread = thread
79
+ @logger = logger
80
+
81
+ @work = method(:work)
82
+ @work_complete = proc { |result|
83
+ request = @processing
84
+ if request.is_async && !request.hijacked
85
+ if result.is_a? Fixnum
86
+ # TODO:: setup timeout for async response
87
+ end
88
+ else
89
+ # Complete the current request
90
+ request.defer.resolve(result)
91
+ end
92
+ }
93
+
94
+ @async_callback = method(:async_callback)
95
+ @queue_response = method(:queue_response)
96
+
97
+ # The parser state for this instance
98
+ @state = ::HttpParser::Parser.new_instance do |inst|
99
+ inst.type = :request
100
+ end
101
+
102
+ # The request and response queues
103
+ @requests = []
104
+ @responses = []
105
+ end
106
+
107
+
108
+ attr_reader :parsing
109
+
110
+
111
+ def self.on_progress(data, socket); end
112
+ DUMMY_PROGRESS = self.method :on_progress
113
+
114
+ HTTP = 'http'.freeze
115
+ HTTPS = 'https'.freeze
116
+
117
+ def load(socket, port, app, app_mode, tls)
118
+ @socket = socket
119
+ @port = port
120
+ @app = app
121
+ @mode = app_mode
122
+
123
+ case @mode
124
+ when :thread_pool
125
+ @exec = method :exec_on_thread_pool
126
+ when :fiber_pool
127
+ # TODO:: Implement these modes
128
+ @exec = method :critical_error
129
+ when :libuv
130
+ @exec = method :critical_error
131
+ when :eventmachine
132
+ @exec = method :critical_error
133
+ when :celluloid
134
+ @exec = method :critical_error
135
+ end
136
+
137
+ @remote_ip = socket.peername[0]
138
+ @scheme = tls ? HTTPS : HTTP
139
+
140
+ set_on_close(socket)
141
+ end
142
+
143
+ # Only close the socket we are meaning to close
144
+ def set_on_close(socket)
145
+ socket.finally { on_close if socket == @socket }
146
+ end
147
+
148
+ def on_close
149
+ # Unlink the progress callback (prevent funny business)
150
+ @socket.progress DUMMY_PROGRESS
151
+ @socket.storage = nil
152
+ reset
153
+ @return_method.call(self)
154
+ end
155
+ alias_method :unlink, :on_close
156
+
157
+ def reset
158
+ @app = nil
159
+ @socket = nil
160
+ @remote_ip = nil
161
+
162
+ # Safe to leave these
163
+ # @port = nil
164
+ # @mode = nil
165
+ # @scheme = nil
166
+
167
+ @processing = nil
168
+ @transmitting = nil
169
+
170
+ @requests.clear
171
+ @responses.clear
172
+ @state.reset!
173
+ end
174
+
175
+ def parse(data)
176
+ # This works as we only ever call this from a single thread
177
+ @callbacks.connection = self
178
+ parsing_error if @callbacks.parser.parse(@state, data)
179
+ end
180
+
181
+ # ----------------
182
+ # Parser Callbacks
183
+ # ----------------
184
+ def start_parsing
185
+ @parsing = Request.new @thread, @app, @port, @remote_ip, @scheme, @async_callback
186
+ end
187
+
188
+ REQUEST_METHOD = 'REQUEST_METHOD'.freeze
189
+ def headers_complete
190
+ @parsing.env[REQUEST_METHOD] = @state.http_method.to_s
191
+ end
192
+
193
+ def finished_parsing
194
+ request = @parsing
195
+ @parsing = nil
196
+
197
+ if !@state.keep_alive?
198
+ request.keep_alive = false
199
+ # We don't expect any more data
200
+ @socket.stop_read
201
+ end
202
+
203
+ request.upgrade = @state.upgrade?
204
+ @requests << request
205
+ process_next unless @processing
206
+ end
207
+
208
+ # ------------------
209
+ # Request Processing
210
+ # ------------------
211
+ def process_next
212
+ @processing = @requests.shift
213
+ if @processing
214
+ @exec.call
215
+ @processing.then @queue_response
216
+ end
217
+ end
218
+
219
+ WORKER_ERROR = proc { |error|
220
+ @logger.print_error error, 'critical error'
221
+ Reactor.instance.shutdown
222
+ }
223
+ def exec_on_thread_pool
224
+ promise = @thread.work @work
225
+ promise.then @work_complete
226
+ promise.catch WORKER_ERROR
227
+ end
228
+
229
+ EMPTY_RESPONSE = [''.freeze].freeze
230
+ def work
231
+ begin
232
+ @processing.execute!
233
+ rescue StandardError => e
234
+ @logger.print_error e, 'framework error'
235
+ @processing.keep_alive = false
236
+ [500, {}, EMPTY_RESPONSE]
237
+ end
238
+ end
239
+
240
+ # Process the async request in the same way as Mizuno
241
+ # See: http://polycrystal.org/2012/04/15/asynchronous_responses_in_rack.html
242
+ def async_callback(data)
243
+ @thread.schedule { callback(data) }
244
+ end
245
+
246
+ # Process a response that was marked as async. Save the data if the request hasn't responded yet
247
+ def callback(data)
248
+ request = @processing
249
+ if request && request.is_async
250
+ request.defer.resolve(data)
251
+ else
252
+ @logger.warn "Received async callback and there are no pending requests. Data was:\n#{data}"
253
+ end
254
+ end
255
+
256
+
257
+ # ----------------
258
+ # Response Sending
259
+ # ----------------
260
+ def queue_response(result)
261
+ @responses << [@processing, result]
262
+ send_next_response unless @transmitting
263
+
264
+ # Processing will be set to nil if the array is empty
265
+ process_next
266
+ end
267
+
268
+
269
+ HEAD = 'HEAD'.freeze
270
+ ETAG = 'ETag'.freeze
271
+ HTTP_ETAG = 'HTTP_ETAG'.freeze
272
+ CONTENT_LENGTH2 = 'Content-Length'.freeze
273
+ TRANSFER_ENCODING = 'Transfer-Encoding'.freeze
274
+ CHUNKED = 'chunked'.freeze
275
+ ZERO = '0'.freeze
276
+ NOT_MODIFIED_304 = "HTTP/1.1 304 Not Modified\r\n".freeze
277
+
278
+ def send_next_response
279
+ request, result = @responses.shift
280
+ @transmitting = request
281
+ return unless request
282
+
283
+ if request.hijacked
284
+ # Unlink the management of the socket
285
+ # Then forward the raw socket to the upgrade handler
286
+ socket = @socket
287
+ unlink
288
+ request.hijacked.resolve Hijack.new(socket, request.env)
289
+
290
+ elsif @socket.closed
291
+ body = result[2]
292
+ body.close if body.respond_to?(:close)
293
+ else
294
+ status, headers, body = result
295
+ send_body = request.env[REQUEST_METHOD] != HEAD
296
+
297
+ # If a file, stream the body in a non-blocking fashion
298
+ if body.respond_to? :to_path
299
+ file = @thread.file body.to_path, File::RDONLY
300
+
301
+ # Send the body in parallel without blocking the next request in dev
302
+ # Also if this is a head request we still want the body closed
303
+ body.close if body.respond_to?(:close)
304
+ data_written = false
305
+
306
+ file.progress do
307
+ statprom = file.stat
308
+ statprom.then do |stats|
309
+ #etag = ::Digest::MD5.hexdigest "#{stats[:st_mtim][:tv_sec]}#{body.to_path}"
310
+ #if etag == request.env[HTTP_ETAG]
311
+ # header = NOT_MODIFIED_304.dup
312
+ # add_header(header, ETAG, etag)
313
+ # header << LINE_END
314
+ # @socket.write header
315
+ # return
316
+ #end
317
+ #headers[ETAG] ||= etag
318
+
319
+ if headers[CONTENT_LENGTH2]
320
+ type = :raw
321
+ else
322
+ type = :http
323
+ headers[TRANSFER_ENCODING] = CHUNKED
324
+ end
325
+
326
+ data_written = true
327
+ write_headers request.keep_alive, status, headers
328
+
329
+ if send_body
330
+ # File is open and available for reading
331
+ promise = file.send_file(@socket, type)
332
+ promise.then do
333
+ file.close
334
+ @socket.shutdown if request.keep_alive == false
335
+ end
336
+ promise.catch do |err|
337
+ @logger.warn "Error sending file: #{err}"
338
+ @socket.close
339
+ file.close
340
+ end
341
+ else
342
+ file.close
343
+ @socket.shutdown unless request.keep_alive
344
+ end
345
+ end
346
+ statprom.catch do |err|
347
+ @logger.warn "Error reading file stats: #{err}"
348
+ file.close
349
+ send_internal_error
350
+ end
351
+ end
352
+
353
+ file.catch do |err|
354
+ @logger.warn "Error reading file: #{err}"
355
+
356
+ if data_written
357
+ file.close
358
+ @socket.shutdown
359
+ else
360
+ send_internal_error
361
+ end
362
+ end
363
+
364
+ # Request has completed - send the next one
365
+ file.finally do
366
+ send_next_response
367
+ end
368
+ else
369
+ # Optimize the response
370
+ begin
371
+ if body.size < 2
372
+ headers[CONTENT_LENGTH2] = body.size == 1 ? body[0].bytesize : ZERO
373
+ end
374
+ rescue # just in case
375
+ end
376
+
377
+ keep_alive = request.keep_alive
378
+
379
+ if send_body
380
+ write_response request, status, headers, body
381
+ else
382
+ body.close if body.respond_to?(:close)
383
+ write_headers keep_alive, status, headers
384
+ @socket.shutdown if keep_alive == false
385
+ end
386
+
387
+ send_next_response
388
+ end
389
+ end
390
+ end
391
+
392
+ CLOSE_CHUNKED = "0\r\n\r\n".freeze
393
+ def write_response(request, status, headers, body)
394
+ keep_alive = request.keep_alive
395
+
396
+ if headers[CONTENT_LENGTH2]
397
+ headers[CONTENT_LENGTH2] = headers[CONTENT_LENGTH2].to_s
398
+ write_headers keep_alive, status, headers
399
+
400
+ # Stream the response (pass directly into @socket.write)
401
+ body.each &@socket.method(:write)
402
+ @socket.shutdown if keep_alive == false
403
+ else
404
+ headers[TRANSFER_ENCODING] = CHUNKED
405
+ write_headers keep_alive, status, headers
406
+
407
+ # Stream the response
408
+ @write_chunk ||= method :write_chunk
409
+ body.each &@write_chunk
410
+
411
+ @socket.write CLOSE_CHUNKED
412
+ @socket.shutdown if keep_alive == false
413
+ end
414
+
415
+ body.close if body.respond_to?(:close)
416
+ end
417
+
418
+ COLON_SPACE = ': '.freeze
419
+ LINE_END = "\r\n".freeze
420
+ def add_header(header, key, value)
421
+ header << key
422
+ header << COLON_SPACE
423
+ header << value
424
+ header << LINE_END
425
+ end
426
+
427
+ CONNECTION = "Connection".freeze
428
+ NEWLINE = "\n".freeze
429
+ CLOSE = "close".freeze
430
+ RACK = "rack".freeze
431
+ def write_headers(keep_alive, status, headers)
432
+ headers[CONNECTION] = CLOSE if keep_alive == false
433
+
434
+ header = "HTTP/1.1 #{status} #{fetch_code(status)}\r\n"
435
+ headers.each do |key, value|
436
+ next if key.start_with? RACK
437
+ value.to_s.split(NEWLINE).each {|val| add_header(header, key, val)}
438
+ end
439
+ header << LINE_END
440
+ @socket.write header
441
+ end
442
+
443
+ HEX_ENCODED = 16
444
+ def write_chunk(part)
445
+ chunk = part.bytesize.to_s(HEX_ENCODED) << LINE_END << part << LINE_END
446
+ @socket.write chunk
447
+ end
448
+
449
+ HTTP_STATUS_CODES = Rack::Utils::HTTP_STATUS_CODES
450
+ HTTP_STATUS_DEFAULT = proc { 'CUSTOM'.freeze }
451
+ def fetch_code(status)
452
+ HTTP_STATUS_CODES.fetch(status, &HTTP_STATUS_DEFAULT)
453
+ end
454
+
455
+
456
+ # ----------------
457
+ # Error Management
458
+ # ----------------
459
+ def critical_error
460
+ # Kill the process
461
+ Reactor.instance.shutdown
462
+ end
463
+
464
+ def parsing_error
465
+ # Stop reading from the client
466
+ # Wait for existing requests to complete
467
+ # Send an error response for the current request
468
+ @socket.stop_read
469
+ previous = @requests[-1] || @processing
470
+
471
+ if previous
472
+ previous.finally do
473
+ send_parsing_error
474
+ end
475
+ else
476
+ send_parsing_error
477
+ end
478
+ end
479
+
480
+ ERROR_400_RESPONSE = "HTTP/1.1 400 Bad Request\r\n\r\n".freeze
481
+ def send_parsing_error
482
+ @logger.info "Parsing error!"
483
+ @socket.write ERROR_400_RESPONSE
484
+ @socket.shutdown
485
+ end
486
+
487
+ ERROR_500_RESPONSE = "HTTP/1.1 500 Internal Server Error\r\n\r\n".freeze
488
+ def send_internal_error
489
+ @logger.info "Internal error"
490
+ @socket.stop_read
491
+ @socket.write ERROR_500_RESPONSE
492
+ @socket.shutdown
493
+ end
494
+ end
495
+ end
496
+ end