heckpsi-lab/em-midori

View on GitHub
lib/midori/server.rb

Summary

Maintainability
A
55 mins
Test Coverage
##
# Logic to EventMachine TCP Server, running inside +Midori::Connection+
module Midori::Server
  # @!attribute request
  #   @return [Midori::Request] raw request
  # @!attribute api
  #   @return [Class] inherited from Midori::API
  # @!attribute websocket
  #   @return [Midori::WebSocket] defined websocket instance
  # @!attribute eventsource
  #   @return [Midori::EventSource] defined eventsource instance
  attr_accessor :request, :api, :websocket, :eventsource

  # Define server behaviour
  # @param [Class] api inherited from Midori::API
  # @param [Logger] logger global logger
  def server_initialize(api, logger)
    @api = api
    @logger = logger

    @request = Midori::Request.new
    @websocket = Midori::WebSocket.new(self)
    @eventsource = Midori::EventSource.new(self)

    # Add keep-alive parameters
    @keep_alive_timer = nil
    @keep_alive_count = 1
  end

  # Logic of receiving data
  # @param [Scoket] the socket able to read
  def receive_data(socket)
    begin
      @request.ip, @request.port = @peer_addr
      if @request.parsed? && !(@request.body_parsed?)
        data = socket.read @request.body.bytesize - @request.header['Content-Length']
      else
        data = socket.readline
      end

      if @request.websocket?
        websocket_request(StringIO.new(data))
      else
        @request.parse(data)
        receive_new_request if @request.parsed? && @request.body_parsed?
      end
    rescue EOFError, Errno::ENOTCONN => _e
      close_connection
      # Ignore client's disconnection
    rescue => e
      # :nocov:
      # Leave for corner cases
      close_connection
      @logger.warn "#{@request.ip} - - #{e.class} #{e.backtrace.join("\n")}".yellow
      # :nocov:
    end
  end

  # Logic of receiving new request
  def receive_new_request
    begin
      start_time = Time.now
      @response = @api.receive(request, self)
      now_time = Time.now
      @logger.info "#{@request.ip} - - \"#{@request.method} #{@request.path} HTTP/#{@request.protocol}\" #{@response.status} #{sprintf("%.6f", now_time.to_f - start_time.to_f)}".green
      call_event(:open) if @request.websocket?
    rescue Midori::Exception::NotFound => e
      @response = Midori::Sandbox.capture(e)
    rescue => e
      @response = Midori::Sandbox.capture(e)
      @logger.error e.inspect.red
      @logger.warn e.backtrace.join("\n").yellow
    end

    unless @request.websocket? || @request.eventsource?
      send_data @response
      proceed_keep_alive
    end
  end

  # Logic of receiving WebSocket request
  # @param [StringIO] data raw data
  def websocket_request(data)
    @websocket.decode(data)
    case @websocket.opcode
    when 0x1, 0x2
      call_event(:message, [@websocket.msg])
    when 0x9
      @websocket.pong(@websocket.msg)
      call_event(:ping)
    when 0xA
      call_event(:pong)
    end
  rescue Midori::Exception::FrameEnd => _e
    call_event(:close)
    send_data "\b" # Opcode 0x8
    close_connection_after_writing
  rescue Midori::Exception::PingPongSizeTooLarge => e
    @logger.warn e.inspect.yellow
    call_event(:error) # Too large ping request
    send_data "\b" # Opcode 0x8
    close_connection_after_writing
  rescue => e
    call_event(:error)
    @logger.error e.inspect.red
    @logger.warn e.backtrace.join("\n").yellow
    close_connection_after_writing
  end

  # To call a websocket event if it exist
  # @param [Symbol] event event name
  # @param [Array] args arg list
  def call_event(event, args = [])
    -> { @websocket.instance_exec(*args, &@websocket.events[event]) }.call unless @websocket.events[event].nil?
  end

  private def proceed_keep_alive
    # Detect if it should close connection
    if !Midori::Configure.keep_alive || (@keep_alive_count >= Midori::Configure.keep_alive_requests)
      close_connection_after_writing
      return
    end
    # Add timeout for keep-alive
    @keep_alive_count += 1
    Fiber.schedule do
      sleep Midori::Configure.keep_alive_timeout
      close_connection
    end
    # Reset request
    @request.reset!
  end
end