lib/midb/serverengine_controller.rb

Summary

Maintainability
D
2 days
Test Coverage
require 'midb/server_controller'
require 'midb/server_model'
require 'midb/server_view'
require 'midb/errors_view'
require 'midb/security_controller'
require 'midb/hooks'

require 'yaml'
require 'socket'
require 'uri'
require 'json'
require 'sqlite3'

module MIDB
  module API
    # @author unrar
    # This class handles runs the server engine using sockets and a loop.
    class Engine
      # Attribute declaration here
      # @!attribute config
      #   @return [Hash] Contains the project's configuration, saved in .midb.yaml
      # @!attribute db
      #   @return [String] Database name (if SQLite is the engine, file name without extension)
      # @!attribute http_status
      #   @return [String] HTTP status code and string representation for the header
      # @!attribute h
      #   @return [Object] MIDB::API::Hooks instance
      attr_accessor :config, :db, :http_status, :hooks

      # Handle an unauthorized request
      def unauth_request
        @http_status = "401 Unauthorized"
        MIDB::Interface::Server.info(:no_auth)
        MIDB::Interface::Server.json_error(401, "Unauthorized").to_json
      end

      # Constructor
      #
      # @param db   [String] Database to which the server will bind.
      # @param stat [Fixnum] HTTP Status
      # @param cnf  [Hash] Config from the server controller.
      def initialize(db, stat, cnf, hooks=nil)
        @config = cnf
        @db = db
        @http_status = stat
        if hooks == nil
          @hooks = MIDB::API::Hooks.new
        else
          @hooks = hooks
        end
      end

      # Starts the server on a given port (default: 8081)
      #
      # @param port [Fixnum] Port to which the server will listen.
      def start(port=8081)
        serv = TCPServer.new("localhost", port)
        MIDB::Interface::Server.info(:start, port)

        # Manage the requests
        loop do
          socket = serv.accept
          MIDB::Interface::Server.info(:incoming_request, socket.addr[3])

          request = self.parse_request(socket.gets)

          # Get a hash with the headers
          headers = {}
          while line = socket.gets.split(' ', 2)
            break if line[0] == "" 
            headers[line[0].chop] = line[1].strip
          end
          data = socket.read(headers["Content-Length"].to_i)


          MIDB::Interface::Server.info(:request, request)
          response_json = Hash.new()

          # Endpoint syntax: ["", FILE, ID, (ACTION)]
          endpoint = request[1].split("/")
          if endpoint.length >= 2
            ep_file = endpoint[1].split("?")[0]
          else
            ep_file = ""
          end

          method = request[0]
          endpoints = [] # Valid endpoints

          # Load the JSON served files
          @config["serves"].each do |js|
            # The filename is a valid endpoint
            endpoints.push File.basename(js, ".*")
          end

          # Load the endpoints
          found = false
          endpoints.each do |ep|
            if ep_file == ep
              found = true
              MIDB::Interface::Server.info(:match_json, ep)
              # Create the model
              dbop = MIDB::API::Model.new(ep, @db, self)
              # Analyze the request and pass it to the model
              # Is the method accepted?
              accepted_methods = ["GET", "POST", "PUT", "DELETE"]
              unless accepted_methods.include? method
                @http_status = "405 Method Not Allowed"
                response_json = MIDB::Interface::Server.json_error(405, "Method Not Allowed").to_json
              else
                # Do we need authentication?
                auth_req = false
                unauthenticated = false
                if @config["privacy#{method.downcase}"] == true
                  MIDB::Interface::Server.info(:auth_required)
                  auth_req = true
                  
                  # For GET and DELETE requests, the object of the digest is the endpoint
                  if (method == "GET") || (method == "DELETE")
                    data = ep_file
                  end

                  # If it's a GET request and we have a different key for GET methods...
                  if (@config["apigetkey"] != nil) && (method == "GET")
                    unauthenticated = (not headers.has_key? "Authentication") ||
                     (not MIDB::API::Security.check?(headers["Authentication"], data, @config["apigetkey"]))
                  else
                    unauthenticated = (not headers.has_key? "Authentication") ||
                       (not MIDB::API::Security.check?(headers["Authentication"], data, @config["apikey"]))
                  end
                end
                # Proceed to handle the request
                if unauthenticated
                  response_json = self.unauth_request
                  puts ">> has header: #{headers.has_key? "Authentication"}"
                else
                  MIDB::Interface::Server.info(:auth_success) if (not unauthenticated) && auth_req
                  if method == "GET"
                    case endpoint.length
                    when 2
                      # No ID has been specified. Return all the entries
                      # Pass it to the model and get the JSON
                      MIDB::Interface::Server.info(:fetch, "get_all_entries()")
                      response_json = dbop.get_all_entries().to_json
                    when 3
                      # This regular expression checks if it contains an integer
                      if /\A[-+]?\d+\z/ === endpoint[2]
                        # An ID has been specified. Should it exist, return all of its entries.
                        MIDB::Interface::Server.info(:fetch, "get_entries(#{endpoint[2]})")
                        response_json = dbop.get_entries(endpoint[2].to_i).to_json
                      else
                        # A row has been specified, but no pattern
                        MIDB::Interface::Server.info(:fetch, "get_column_entries(#{endpoint[2]})")
                        response_json = dbop.get_column_entries(endpoint[2]).to_json
                      end
                    when 4
                      if (endpoint[2].is_a? String) && (endpoint[3].is_a? String) then
                        # A row and a pattern have been specified
                        MIDB::Interface::Server.info(:fetch, "get_matching_rows(#{endpoint[2]}, #{endpoint[3]})")
                        response_json = dbop.get_matching_rows(endpoint[2], endpoint[3]).to_json
                      end
                    end
                  elsif method == "POST"
                    MIDB::Interface::Server.info(:fetch, "post(#{data})")
                    response_json = dbop.post(data).to_json
                  else
                    if endpoint.length >= 3
                      if method == "DELETE"
                        MIDB::Interface::Server.info(:fetch, "delete(#{endpoint[2]})")
                        response_json = dbop.delete(endpoint[2]).to_json 
                      elsif method == "PUT"
                        MIDB::Interface::Server.info(:fetch, "put(#{endpoint[2]}, data)")
                        response_json = dbop.put(endpoint[2], data).to_json
                      end
                    else
                      @http_status = "404 Not Found"
                      response_json = MIDB::Interface::Server.json_error(404, "Must specify an ID.").to_json
                    end
                  end
                end
              end 
              MIDB::Interface::Server.info(:response, response_json)
              # Return the results via HTTP
              socket.print "HTTP/1.1 #{@http_status}\r\n" +
                          "Content-Type: text/json\r\n" +
                          "Content-Length: #{response_json.size}\r\n" +
                          "Connection: close\r\n"
              socket.print "\r\n"
              socket.print response_json
              socket.print "\r\n"
              MIDB::Interface::Server.info(:success)
            end
          end
          unless found
            MIDB::Interface::Server.info(:not_found)
            response = MIDB::Interface::Server.json_error(404, "Invalid API endpoint.").to_json

            socket.print "HTTP/1.1 404 Not Found\r\n" +
                         "Content-Type: text/json\r\n" +
                         "Content-Length: #{response.size}\r\n" +
                         "Connection: close\r\n"
            socket.print "\r\n"
            socket.print response
          end
        end
      end

      # Method: parse_request
      # Parses an HTTP requests and returns an array [method, uri]
      def parse_request(req)
        [req.split(" ")[0], req.split(" ")[1]]
      end
    end
  end
end