class SpiderGazelle::Spider::Http1
Constants
- DUMMY_PROGRESS
- EMPTY_RESPONSE
Request Processing
- HTTP_STATUS_CODES
- HTTP_STATUS_DEFAULT
- Hijack
Attributes
parsing[R]
Public Class Methods
new(return_method, callbacks, thread, logger, gazelles)
click to toggle source
# File lib/spider-gazelle/spider/http1.rb, line 70 def initialize(return_method, callbacks, thread, logger, gazelles) # The HTTP parser callbacks object for this thread @return_method = return_method @callbacks = callbacks @thread = thread @logger = logger @gazelles = gazelles # The parser state for this instance @state = ::HttpParser::Parser.new_instance do |inst| inst.type = :request end # The request and response queues @requests = [] @responses = [] end
Public Instance Methods
add_header(header, key, value)
click to toggle source
# File lib/spider-gazelle/spider/http1.rb, line 373 def add_header(header, key, value) header << key header << ': ' header << value header << "\r\n" end
after_parsing(request)
click to toggle source
# File lib/spider-gazelle/spider/http1.rb, line 166 def after_parsing(request) @socket.stop_read unless request.keep_alive # Process the async request in the same way as Mizuno # See: http://polycrystal.org/2012/04/15/asynchronous_responses_in_rack.html # Process a response that was marked as async. request.env['async.callback'] = proc { |data| @thread.schedule { request.defer.resolve([request, data]) } } @requests << request process_next unless @processing end
critical_error()
click to toggle source
Error Management
# File lib/spider-gazelle/spider/http1.rb, line 402 def critical_error # Kill the process Reactor.instance.shutdown end
fetch_code(status)
click to toggle source
# File lib/spider-gazelle/spider/http1.rb, line 394 def fetch_code(status) HTTP_STATUS_CODES.fetch(status, &HTTP_STATUS_DEFAULT) end
finished_parsing()
click to toggle source
# File lib/spider-gazelle/spider/http1.rb, line 157 def finished_parsing request = @parsing @parsing = nil request.keep_alive = @state.keep_alive? request.upgrade = @state.upgrade? @thread.next_tick { after_parsing(request) } end
headers_complete()
click to toggle source
# File lib/spider-gazelle/spider/http1.rb, line 153 def headers_complete @parsing.env['REQUEST_METHOD'] = @state.http_method.to_s end
load(socket, port, app, tls)
click to toggle source
# File lib/spider-gazelle/spider/http1.rb, line 94 def load(socket, port, app, tls) @socket = socket @port = port @app = app @remote_ip = socket.peername[0] @scheme = tls ? 'https' : 'http' set_on_close(socket) end
on_close()
click to toggle source
# File lib/spider-gazelle/spider/http1.rb, line 110 def on_close # Unlink the progress callback (prevent funny business) @socket.progress &DUMMY_PROGRESS @socket.storage = nil reset @return_method.call(self) end
Also aliased as: unlink
parse(data)
click to toggle source
# File lib/spider-gazelle/spider/http1.rb, line 140 def parse(data) # This works as we only ever call this from a single thread @callbacks.connection = self parsing_error if @callbacks.parser.parse(@state, data) end
parsing_error()
click to toggle source
# File lib/spider-gazelle/spider/http1.rb, line 407 def parsing_error # Stop reading from the client # Wait for existing requests to complete # Send an error response for the current request @socket.stop_read previous = @requests[-1] || @processing if previous previous.finally do send_parsing_error end else send_parsing_error end end
process_next()
click to toggle source
# File lib/spider-gazelle/spider/http1.rb, line 184 def process_next @processing = @requests.shift if @processing request = @processing # queue response request.then do |response| @responses << response send_next_response unless @transmitting # Processing will be set to nil if the array is empty process_next end @gazelles.next.schedule do process_on_gazelle(request) end end end
process_on_gazelle(request)
click to toggle source
# File lib/spider-gazelle/spider/http1.rb, line 204 def process_on_gazelle(request) result = begin request.execute! rescue StandardError => e Logger.instance.print_error e, 'framework error' request.keep_alive = false [500, {}, EMPTY_RESPONSE] end if request.is_async && !request.hijacked if result.nil? && !request.defer.resolved? # TODO:: setup timeout for async response end else # Complete the current request request.defer.resolve([request, result]) end rescue Exception => error Logger.instance.print_error error, 'critical error' Reactor.instance.shutdown end
reset()
click to toggle source
# File lib/spider-gazelle/spider/http1.rb, line 119 def reset @app = nil @socket = nil @remote_ip = nil # Safe to leave these # @port = nil # @mode = nil # @scheme = nil if @processing @processing.defer.reject(:socket_closed) end @processing = nil @transmitting = nil @requests.clear @responses.clear @state.reset! end
send_internal_error()
click to toggle source
# File lib/spider-gazelle/spider/http1.rb, line 430 def send_internal_error @logger.info "Internal error" @socket.stop_read @socket.write "HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\nContent-Length: 0\r\n\r\n" @socket.shutdown end
send_next_response()
click to toggle source
Response Sending
# File lib/spider-gazelle/spider/http1.rb, line 229 def send_next_response request, result = @responses.shift @transmitting = request return unless request if request.hijacked # Unlink the management of the socket # Then forward the raw socket to the upgrade handler socket = @socket unlink request.hijacked.resolve Hijack.new(socket, request.env) elsif @socket.closed body = result[2] body.close if body.respond_to?(:close) else status, headers, body = result send_body = request.env['REQUEST_METHOD'] != 'HEAD' # If a file, stream the body in a non-blocking fashion if body.respond_to? :to_path begin file = @thread.file body.to_path, File::RDONLY, wait: true file.catch do |err| @logger.warn "Error reading file: #{err}" if data_written file.close @socket.shutdown else send_internal_error end end # Request has completed - send the next one file.finally do send_next_response end # Send the body in parallel without blocking the next request in dev # Also if this is a head request we still want the body closed body.close if body.respond_to?(:close) data_written = false statprom = file.stat wait: false statprom.then do |stats| #etag = ::Digest::MD5.hexdigest "#{stats[:st_mtim][:tv_sec]}#{body.to_path}" #if etag == request.env[HTTP_ETAG] # header = NOT_MODIFIED_304.dup # add_header(header, ETAG, etag) # header << "\r\n" # @socket.write header # return #end #headers[ETAG] ||= etag if headers['Content-Length'] type = :raw else type = :http headers['Transfer-Encoding'] = 'chunked' end data_written = true write_headers request.keep_alive, status, headers if send_body # File is open and available for reading promise = file.send_file(@socket, using: type) promise.then do file.close @socket.shutdown if request.keep_alive == false end promise.catch do |err| @logger.warn "Error sending file: #{err}" @socket.close file.close end else file.close @socket.shutdown unless request.keep_alive end end statprom.catch do |err| @logger.warn "Error reading file stats: #{err}" file.close send_internal_error end rescue => err @logger.warn "Error reading file: #{err}" send_internal_error end else # Optimize the response begin if body.size < 2 headers['Content-Length'] = body.size == 1 ? body[0].bytesize.to_s : '0' end rescue # just in case end keep_alive = request.keep_alive if send_body write_response request, status, headers, body else body.close if body.respond_to?(:close) write_headers keep_alive, status, headers @socket.shutdown if keep_alive == false end send_next_response end end end
send_parsing_error()
click to toggle source
# File lib/spider-gazelle/spider/http1.rb, line 423 def send_parsing_error @logger.info "Parsing error!" @socket.stop_read @socket.write "HTTP/1.1 400 Bad Request\r\nConnection: close\r\nContent-Length: 0\r\n\r\n" @socket.shutdown end
set_on_close(socket)
click to toggle source
Only close the socket we are meaning to close
# File lib/spider-gazelle/spider/http1.rb, line 106 def set_on_close(socket) socket.finally { on_close if socket == @socket } end
start_parsing()
click to toggle source
Parser Callbacks
# File lib/spider-gazelle/spider/http1.rb, line 149 def start_parsing @parsing = Gazelle::Request.new @thread, @app, @port, @remote_ip, @scheme, @socket end
write_headers(keep_alive, status, headers)
click to toggle source
# File lib/spider-gazelle/spider/http1.rb, line 380 def write_headers(keep_alive, status, headers) headers['Connection'] = 'close' if keep_alive == false header = String.new("HTTP/1.1 #{status} #{fetch_code(status)}\r\n") headers.each do |key, value| next if key.start_with? 'rack' value.to_s.split("\n").each {|val| add_header(header, key, val)} end header << "\r\n" @socket.write header end
write_response(request, status, headers, body)
click to toggle source
# File lib/spider-gazelle/spider/http1.rb, line 346 def write_response(request, status, headers, body) keep_alive = request.keep_alive if headers['Content-Length'] headers['Content-Length'] = headers['Content-Length'].to_s write_headers keep_alive, status, headers # Stream the response (pass directly into @socket.write) body.each { |data| @socket.write(data) } @socket.shutdown if keep_alive == false else headers['Transfer-Encoding'] = 'chunked' write_headers keep_alive, status, headers # Stream the response body.each do |part| chunk = part.bytesize.to_s(16) << "\r\n" << part << "\r\n" @socket.write chunk end @socket.write "0\r\n\r\n" @socket.shutdown if keep_alive == false end body.close if body.respond_to?(:close) end