Class: SpiderGazelle::Spider::Http1
- Inherits:
-
Object
- Object
- SpiderGazelle::Spider::Http1
- Defined in:
- lib/spider-gazelle/spider/http1.rb
Defined Under Namespace
Constant Summary collapse
- DUMMY_PROGRESS =
proc {}
- EMPTY_RESPONSE =
Request Processing
[''].freeze
- HTTP_STATUS_CODES =
Rack::Utils::HTTP_STATUS_CODES
- HTTP_STATUS_DEFAULT =
proc { 'CUSTOM' }
Instance Attribute Summary collapse
-
#parsing ⇒ Object
readonly
Returns the value of attribute parsing.
Instance Method Summary collapse
- #add_header(header, key, value) ⇒ Object
- #after_parsing(request) ⇒ Object
-
#critical_error ⇒ Object
—————- Error Management —————-.
- #fetch_code(status) ⇒ Object
- #finished_parsing ⇒ Object
- #headers_complete ⇒ Object
-
#initialize(return_method, callbacks, thread, logger, gazelles) ⇒ Http1
constructor
A new instance of Http1.
- #load(socket, port, app, tls) ⇒ Object
- #on_close ⇒ Object (also: #unlink)
- #parse(data) ⇒ Object
- #parsing_error ⇒ Object
- #process_next ⇒ Object
- #process_on_gazelle(request) ⇒ Object
- #reset ⇒ Object
- #send_internal_error ⇒ Object
-
#send_next_response ⇒ Object
—————- Response Sending —————-.
- #send_parsing_error ⇒ Object
-
#set_on_close(socket) ⇒ Object
Only close the socket we are meaning to close.
-
#start_parsing ⇒ Object
—————- Parser Callbacks —————-.
- #write_headers(keep_alive, status, headers) ⇒ Object
- #write_response(request, status, headers, body) ⇒ Object
Constructor Details
#initialize(return_method, callbacks, thread, logger, gazelles) ⇒ Http1
Returns a new instance of Http1.
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
# 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 |
Instance Attribute Details
#parsing ⇒ Object (readonly)
Returns the value of attribute parsing.
90 91 92 |
# File 'lib/spider-gazelle/spider/http1.rb', line 90 def parsing @parsing end |
Instance Method Details
#add_header(header, key, value) ⇒ Object
373 374 375 376 377 378 |
# 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) ⇒ Object
166 167 168 169 170 171 172 173 174 175 176 177 178 |
# 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 ⇒ Object
Error Management
402 403 404 405 |
# File 'lib/spider-gazelle/spider/http1.rb', line 402 def critical_error # Kill the process Reactor.instance.shutdown end |
#fetch_code(status) ⇒ Object
394 395 396 |
# File 'lib/spider-gazelle/spider/http1.rb', line 394 def fetch_code(status) HTTP_STATUS_CODES.fetch(status, &HTTP_STATUS_DEFAULT) end |
#finished_parsing ⇒ Object
157 158 159 160 161 162 163 164 |
# 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 ⇒ Object
153 154 155 |
# 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) ⇒ Object
94 95 96 97 98 99 100 101 102 103 |
# 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 ⇒ Object Also known as: unlink
110 111 112 113 114 115 116 |
# 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 |
#parse(data) ⇒ Object
140 141 142 143 144 |
# 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 ⇒ Object
407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 |
# 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 ⇒ Object
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 |
# 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) ⇒ Object
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 |
# 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 ⇒ Object
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
# 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 ⇒ Object
430 431 432 433 434 435 |
# 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 ⇒ Object
Response Sending
229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 |
# 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 ⇒ Object
423 424 425 426 427 428 |
# 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) ⇒ Object
Only close the socket we are meaning to close
106 107 108 |
# File 'lib/spider-gazelle/spider/http1.rb', line 106 def set_on_close(socket) socket.finally { on_close if socket == @socket } end |
#start_parsing ⇒ Object
Parser Callbacks
149 150 151 |
# 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) ⇒ Object
380 381 382 383 384 385 386 387 388 389 390 |
# 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) ⇒ Object
346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 |
# 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 |