hackedteam/rcs-db

View on GitHub
lib/rcs-db/rest_response.rb

Summary

Maintainability
C
1 day
Test Coverage
#
# response handling classes
#

require_relative 'em_streamer'

# from RCS::Common
require 'rcs-common/trace'
require 'rcs-common/rest'

require 'net/http'
require 'stringio'
require 'json'
require 'zlib'

module RCS
module DB

  HTTP_STATUS_CODES = {
    200 => 'OK',
    301 => 'Moved Permanently',
    302 => 'Found',
    304 => 'Not Modified',
    400 => 'Bad Request',
    401 => 'Unauthorized',
    403 => 'Forbidden',
    404 => 'Not Found',
    405 => 'Method Not Allowed',
    406 => 'Not Acceptable',
    408 => 'Request Timeout',
    409 => 'Conflict',
    500 => 'Internal Server Error',
    501 => 'Not Implemented',
    502 => 'Bad Gateway',
    503 => 'Service Unavailable',
    504 => 'Gateway Timeout',
    505 => 'HTTP Version Not Supported',
  }

class RESTResponse
  include RCS::Tracer
  include RCS::Common::Rest

  attr_accessor :status, :content, :content_type, :cookie

  def initialize(status, content = '', opts = {}, callback=proc{})
    @status = status
    @status = STATUS_SERVER_ERROR if @status.nil? or @status.class != Fixnum
    
    @content = content
    @content_type = opts[:content_type]
    @content_type ||= 'application/json'
    @location ||= opts[:location]
    @cookie ||= opts[:cookie]

    @opts = opts

    @callback = callback

    @response = nil
  end

  #
  # BEWARE: for any reason this method should raise an exception!
  # An exception raised here WILL NOT be cough, resulting in a crash.
  #
  def prepare_response(connection, request)

    @request = request
    @connection = connection
    @response = EM::DelegatedHttpResponse.new @connection

    @response.status = @status
    @response.status_string = HTTP_STATUS_CODES[@response.status]
    @cache_json ||= Config.instance.global['JSON_CACHE']

    begin
      start = Time.now

      final_content = if @content_type == 'application/json'
        @cache_json ? Cache::Manager.instance.process(@content, {uri: @request[:uri]}) : @content.to_json
      else
        @content
      end

      @request[:time][:json] = Time.now - start

      if @opts[:gzip]
        compressed = StringIO.open("", 'w')
        gzip = Zlib::GzipWriter.new(compressed)
        gzip.write final_content
        gzip.close
        @response.content = compressed.string
      else
        @response.content = final_content
      end

    rescue Exception => e
      @response.status = STATUS_SERVER_ERROR
      @response.content = 'JSON_SERIALIZATION_ERROR'
      trace :error, e.message
      trace :error, "CONTENT: #{@content}"
      trace :fatal, "EXCEPTION(#{e.class}): " + e.backtrace.join("\n")
    end

    # fake server reply
    @response.headers['Server'] = 'nginx'
    @response.headers['Date'] = Time.now.getutc.strftime("%a, %d %b %Y %H:%M:%S GMT")

    @response.headers['Content-Type'] = @content_type
    @response.headers['Content-Length'] = @response.content.bytesize

    # fixup_headers override to evade content-length reset
    metaclass = class << @response; self; end
    metaclass.send(:define_method, :fixup_headers, proc {})
    # override the generate_header_lines to NOT sort the headers in the reply
    metaclass.send(:define_method, :generate_header_lines, proc { |in_hash|
      out_ary = []
               in_hash.keys.each {|k|
                   v = in_hash[k]
                   if v.is_a?(Array)
                       v.each {|v1| out_ary << "#{k}: #{v1}\r\n" }
                   else
                       out_ary << "#{k}: #{v}\r\n"
                   end
               }
           out_ary
    })

    @response.headers['Set-Cookie'] = @cookie unless @cookie.nil?

    # used for redirects
    @response.headers['Location'] = @location unless @location.nil?

    if request[:headers][:connection] && request[:headers][:connection].downcase == 'keep-alive'
      # keep the connection open to allow multiple requests on the same connection
      # this will increase the speed of sync since it decrease the latency on the net
      @response.keep_connection_open true
      @response.headers['Connection'] = 'keep-alive'
    else
      @response.headers['Connection'] = 'close'
    end

    @response.headers['Content-Encoding'] = 'gzip' if @opts[:gzip]
    
    self
  end

  def size
    fail "response still not prepared" if @response.nil?
    return 0 if @response.content.nil?
    @response.content.bytesize
  end

  def content
    fail "response still not prepared" if @response.nil?
    @response.content
  end

  def headers
    fail "response still not prepared" if @response.nil?
    @response.headers
  end

  def send_response
    fail "response still not prepared" if @response.nil?
    @response.send_response
    @callback unless @callback.nil?
    trace :debug, "[#{@request[:peer]}] REP: [#{@request[:method]}] #{@request[:uri]} #{@request[:query]} (#{Time.now - @request[:time][:start]})" if @request and Config.instance.global['PERF']
  end

end # RESTResponse

class RESTFileStream
  include RCS::Tracer
  
  def initialize(filename, callback=proc{})
    @filename = filename
    @callback = callback
    @response = nil
  end

  def prepare_response(connection, request)

    @request = request
    @connection = connection
    @response = EM::DelegatedHttpResponse.new @connection

    @response.status = 200
    @response.status_string = HTTP_STATUS_CODES[@response.status]

    @response.headers["Content-Length"] = File.size @filename

    # fixup_headers override to evade content-length reset
    metaclass = class << @response; self; end
    metaclass.send(:define_method, :fixup_headers, proc {})

    @response.headers["Content-Type"] = RCS::MimeType.get @filename

    # fake server reply
    @response.headers['Server'] = 'nginx'

    # date header
    @response.headers['Date'] = Time.now.getutc.strftime("%a, %d %b %Y %H:%M:%S GMT")

    if request[:headers][:connection] && request[:headers][:connection].downcase == 'keep-alive'
      # keep the connection open to allow multiple requests on the same connection
      # this will increase the speed of sync since it decrease the latency on the net
      @response.keep_connection_open true
      @response.headers['Connection'] = 'keep-alive'
    else
      @response.headers['Connection'] = 'close'
    end

    self
  end

  def size
    fail "response still not prepared" if @response.nil?
    @response.headers["Content-Length"]
  end

  def content
    fail "response still not prepared" if @response.nil?
    @response.content
  end

  def headers
    fail "response still not prepared" if @response.nil?
    @response.headers
  end
  
  def send_response
    fail "response still not prepared" if @response.nil?
    @response.send_headers
    streamer = EM::FilesystemStreamer.new(@connection, @filename, :http_chunks => false )
    streamer.callback do
      @callback.call unless @callback.nil?
      trace :debug, "[#{@request[:peer]}] REP: [#{@request[:method]}] #{@request[:uri]} #{@request[:query]} (#{Time.now - @request[:time][:start]})" if Config.instance.global['PERF']
    end
  end
end # RESTFileStream

class RESTGridStream
  include RCS::Tracer
  
  def initialize(id, collection, callback)
    @grid_io = GridFS.get id, collection
    fail "grid object not found" if @grid_io.nil?
    
    @callback = callback
    @response = nil
  end

  def prepare_response(connection, request)
    
    @request = request
    @connection = connection
    @response = EM::DelegatedHttpResponse.new @connection
    
    @response.headers["Content-Length"] = @grid_io.file_length
    
    # fixup_headers override to evade content-length reset
    metaclass = class << @response; self; end
    metaclass.send(:define_method, :fixup_headers, proc {})
    
    @response.headers["Content-Type"] = @grid_io.content_type

    # fake server reply
    @response.headers['Server'] = 'nginx'

    # date header
    @response.headers['Date'] = Time.now.getutc.strftime("%a, %d %b %Y %H:%M:%S GMT")

    if request[:headers][:connection] && request[:headers][:connection].downcase == 'keep-alive'
      # keep the connection open to allow multiple requests on the same connection
      # this will increase the speed of sync since it decrease the latency on the net
      @response.keep_connection_open true
      @response.headers['Connection'] = 'keep-alive'
    else
      @response.headers['Connection'] = 'close'
    end
    
    self
  end

  def size
    fail "response still not prepared" if @response.nil?
    @response.headers["Content-Length"]
  end

  def content
    fail "response still not prepared" if @response.nil?
    @response.content
  end

  def headers
    fail "response still not prepared" if @response.nil?
    @response.headers
  end
  
  def send_response
    fail "response still not prepared" if @response.nil?
    @response.send_headers
    streamer = EM::GridStreamer.new(@connection, @grid_io, :http_chunks => false)
    streamer.callback do
      @callback.call unless @callback.nil?
      trace :debug, "[#{@request[:peer]}] REP: [#{@request[:method]}] #{@request[:uri]} #{@request[:query]} (#{Time.now - @request[:time][:start]})" if Config.instance.global['PERF']
    end
  end

end # RESTGridStream

end # ::DB
end # ::RCS