class GamekeyService

This class defines the REST API for the gamekey service.

Attributes

memory[R]

Hash of the in memory database

port[R]

Port

storage[R]

Path to storage file

Public Class Methods

new(storage: Defaults::STORAGE, port: Defaults::PORT) click to toggle source

Constructor to create a Gamekey service

# File lib/gamekey.rb, line 29
def initialize(storage: Defaults::STORAGE, port: Defaults::PORT)
  @storage = storage
  @port = port

  File.open(storage, "w") { |file| file.write(Defaults::DB.to_json) } unless File.exist?(storage)
  content = File.read(storage)

  # print(content)
  @memory = JSON.parse(content)
end

Public Instance Methods

api() click to toggle source

Defines the CORS enabled REST API of the Gamekey service. Following resources are managed:

  • User

  • Game

  • Gamestate

# File lib/gamekey.rb, line 68
def api()

  memory  = @memory
  storage = @storage
  port    = @port
  service = self

  Sinatra.new do

    register Sinatra::CrossOrigin

    set :bind, "0.0.0.0"
    set :port, port
    enable :cross_origin
    set :allow_origin, :any
    set :allow_methods, [:get, :post, :options, :delete, :put]

    #
    # Standard 404 message
    #
    not_found do
      "not found"
    end

    options "*" do
      response.headers["Allow"] = "HEAD,GET,PUT,POST,DELETE,OPTIONS"
      response.headers["Access-Control-Allow-Headers"] = "charset, pwd, secret, name, mail, newpwd"
      200
    end

    #
    # API Endpoints for User resources
    #

    #
    # Lists all registered users.
    #
    # @return 200 OK, Response body includes JSON list of all registered users, list might be empty)
    # Due to the fact that this request can be send unauthenticated, user data never!!! include data
    # about games that are played by a user.
    #
    get "/users" do
      JSON.pretty_generate(memory['users'])
    end

    #
    # Creates a user.
    #
    # @param pwd Password for the user (used for authentication). Required. Parameter is part of request body.
    # @param name Name of the user to provided new name. Required. Parameter is part of request body.
    # @param mail Mail of the user. Optional. Parameter is part of request body.
    #
    # @return 200 OK, on successfull creation (response body includes JSON representation of updated user)
    # @return 400, on invalid mail (response body includes error message)
    # @return 409, on already existing new name (response body includes error message)
    #
    post "/user" do

      name = params['name']
      pwd  = params['pwd']
      mail = params['mail']
      id   = SecureRandom.uuid

      if (name == nil || name.empty?)
        status 400
        return "Bad Request: '#{name}' is not a valid name"
      end

      if (pwd == nil || pwd.empty?)
        status 400
        return "Bad Request: password must be provided and not be empty"
      end

      unless mail =~ Defaults::VALID_EMAIL_REGEX || mail == nil
        status 400
        return "Bad Request: '#{mail}' is not a valid email."
      end

      if memory['users'].map { |entity| entity['name'] }.include? name
        status 409
        return "User with name '#{params['name']}' exists already."
      end

      user = {
          "type"      => 'user',
          "name"      => params['name'],
          "id"        => id,
          "created"   => "#{ Time.now.utc.iso8601(6) }",
          "mail"      => mail,
          "signature" => Auth::signature(id, pwd)
      }

      memory['users'] << user
      File.open(storage, "w") { |file| file.write(JSON.pretty_generate(memory)) }
      JSON.pretty_generate(user)
    end

    #
    # Retrieves user data.
    #
    # @param :id Unique identifier (or name, if called by byname option )of the user (the id is never changed!). Required. Parameter is part of the REST-URI!
    # @param pwd Existing password of the user (used for authentication). Required. Parameter is part of request body.
    # @param byname {=true, =false} Indicates that look up should be done by name (and not by identifier, which is the default). Optional. Parameter is part of request body.

    # @return 200 OK, response body includes JSON representation of user
    # @return 400, Bad Request, if byname parameter is set but not set to 'true' or 'false'
    # @return 401, if request is not provided with correct password (response body includes error message)
    # @return 404, if user with id is not present (response body includes error message)
    #
    get "/user/:id" do
      pwd = params['pwd']
      id  = params['id']
      byname = params['byname']

      if !byname.nil? && byname != 'true' && byname != 'false'
        status 400
        return "Bad Request: byname parameter must be 'true' or 'false' (if set), was '#{byname}'."
      end

      user = service.get_user_by_id(id)   if byname == 'false' || byname == nil
      user = service.get_user_by_name(URI.decode(id)) if byname == 'true'

      if user == nil
        status 404
        return "not found"
      end

      user = user.clone

      unless Auth::authentic?(user, pwd)
        status 401
        return "unauthorized, please provide correct credentials"
      end

      user['games'] = memory['gamestates'].select { |state| state['userid'] == user['id'] }
                                          .map { |state| state['gameid'] }
                                          .uniq

      JSON.pretty_generate(user)
    end

    #
    # Updates a user.
    #
    # @param :id Unique identifier of the user (the id is never changed!). Required. Parameter is part of the REST-URI!
    # @param pwd Existing password of the user (used for authentication). Required. Parameter is part of request body.
    # @param new_name Changes name of the user to provided new name. Optional. Parameter is part of request body.
    # @param new_mail Changes mail of the user to provided new mail. Optional. Parameter is part of request body.
    # @param new_pwd Changes password of the user to a new password. Optional. Parameter is part of request body.
    #
    # @return 200 OK, on successfull update (response body includes JSON representation of updated user)
    # @return 400, on invalid mail (response body includes error message)
    # @return 401, on non matching access credentials (response body includes error message)
    # @return 409, on already existing new name (response body includes error message)
    #
    put "/user/:id" do
      id       = params['id']
      pwd      = params['pwd']
      new_name = params['name']
      new_mail = params['mail']
      new_pwd  = params['newpwd']

      if new_mail
        unless new_mail =~ Defaults::VALID_EMAIL_REGEX
          status 400
          return "Bad Request: '#{new_mail}' is not a valid email."
        end
      end

      if memory['users'].map { |entity| entity['name'] }.include? new_name
        status 409
        return "User with name '#{new_name}' exists already."
      end

      begin
        user = service.get_user_by_id(id)

        unless Auth::authentic?(user, pwd)
          status 401
          return "unauthorized, please provide correct credentials"
        end

        user['name'] = new_name if new_name != nil
        user['mail'] = new_mail if new_mail != nil
        user['signature'] = Auth::signature(id, new_pwd) if new_pwd != nil
        user['update'] = "#{ Time.now.utc.iso8601(6) }"

        File.open(storage, "w") { |file| file.write(JSON.pretty_generate(memory)) }
        return JSON.pretty_generate(user)
      rescue Exception => ex
        status 401
        return "#{ex}\nunauthorized, please provide correct credentials"
      end
    end

    #
    # Deletes a user and all of its associated game states.
    #
    # @param :id Unique identifier of the user (used for authentication). Required. Parameter is part of the REST-URI!
    # @param pwd Existing password of the user (used for authentication). Required. Parameter is part of request body.
    #
    # @return 200 OK, on successfull delete (response body includes confirmation message)
    # @return 401, on non matching access credentials (response body includes error message)
    #
    delete "/user/:id" do

      id  = params['id']
      pwd = params['pwd']

      user = service.get_user_by_id(id)

      if user == nil
        # This is an idempotent operation.
        return "User '#{id}' deleted successfully."
      end

      unless Auth::authentic?(user, pwd)
        status 401
        return "unauthorized, please provide correct credentials"
      end

      memory['users'].delete_if { |user| user['id'] == id }
      memory['gamestates'].delete_if { |state| state['userid'] == id }
      File.open(storage, "w") { |file| file.write(JSON.pretty_generate(memory)) }

      "User '#{id}' deleted successfully."
    end

    #
    # API Endpoints for Game resources
    #

    #
    # Lists all registered games.
    #
    # @return 200 OK, Response body includes JSON list of all registered games, list might be empty)
    # Due to the fact that this request can be send unauthenticated, game data never!!! include data
    # about users that are playing a game.
    #
    get "/games" do
      games = memory['games']
      JSON.pretty_generate(games)
    end

    #
    # Creates a game.
    #
    # @param secret Secret of the game (used for authentication). Required. Parameter is part of request body.
    # @param name Name of the game. Required. Parameter is part of request body.
    # @param url URL of the game. Optional. Parameter is part of request body.
    #
    # @return 200 OK, on successfull creation (response body includes JSON representation of game)
    # @return 400, on invalid url (response body includes error message)
    # @return 400, on invalid name (response body includes error message)
    # @return 400, on missing secret (response body includes error message)
    # @return 409, if a game with provided name already exists (response body includes error message)
    #
    post "/game" do
      name   = params['name']
      secret = params['secret']
      url    = params['url']

      uri = URI.parse(url) rescue nil

      if (name == nil || name.empty?)
        status 400
        return "Bad Request: '#{name}' is not a valid name"
      end

      if (secret == nil || secret.empty?)
        status 400
        return "Bad Request: 'secret must be provided"
      end

      if (uri != nil && !url.empty?)
        if !uri.absolute?
          status 400
          return "Bad Request: '#{url}' is not a valid absolute url"
        end

        if !url =~ Defaults::VALID_URL_REGEX
          status 400
          return "Bad Request: '#{url}' is not a valid absolute url"
        end
      end

      if memory['games'].map { |entity| entity['name'] }.include? name
        status 409
        return "Game with name '#{params['name']}' exists already."
      end

      id = SecureRandom.uuid

      game = {
          "type"      => 'game',
          "name"      => params['name'],
          "id"        => id,
          "url"       => "#{uri}",
          "signature" => Auth::signature(id, secret),
          "created"   => "#{ Time.now.utc.iso8601(6) }"
      }

      memory['games'] << game
      File.open(storage, "w") { |file| file.write(JSON.pretty_generate(memory)) }
      JSON.pretty_generate(game)
    end

    #
    # Retrieves game data.
    #
    # @param :id Unique identifier of the game (the id is never changed!). Required. Parameter is part of the REST-URI!
    # @param secret Secret of the game (used for authentication). Required. Parameter is part of request body.
    #
    # @return 200 OK, response body includes JSON representation of user
    # @return 401, if request is not provided with correct secret (response body includes error message)
    # @return 404, if game with id is not present (response body includes error message)
    #
    get "/game/:id" do

      secret = params['secret']
      id = params['id']
      game = service.get_game_by_id(id)

      if game == nil
        status 404
        return "not found"
      end

      game = game.clone

      unless Auth::authentic?(game, secret)
        status 401
        return "unauthorized, please provide correct credentials"
      end

      game['users'] = memory['gamestates'].select { |state| state['gameid'] == id }
                                          .map { |state| state['userid'] }
                                          .uniq

      JSON.pretty_generate(game)
    end

    #
    # Updates a game.
    #
    # @param :id Unique identifier of the game (the id is never changed!). Required. Parameter is part of the REST-URI!
    # @param pwd Existing secret of the game (used for authentication). Required. Parameter is part of request body.
    # @param new_name Changes name of the game to provided new name. Optional. Parameter is part of request body.
    # @param new_url Changes url of the game to provided new url. Optional. Parameter is part of request body.
    # @param new_secret Changes secret of the game to a new secret. Optional. Parameter is part of request body.
    #
    # @return 200 OK, on successfull update (response body includes JSON representation of updated game)
    # @return 400, on invalid url (response body includes error message)
    # @return 401, on non matching access credentials (response body includes error message)
    # @return 409, on already existing new name (response body includes error message)
    #
    put "/game/:id" do
      id         = params['id']
      secret     = params['secret']
      new_name   = params['name']
      new_url    = params['url']
      new_secret = params['newsecret']

      uri = URI(new_url) rescue nil

      if uri != nil && (!new_url =~ Defaults::VALID_URL_REGEX || !uri.absolute?)
        status 400
        return "Bad Request: '#{new_url}' is not a valid url."
      end

      if memory['games'].map { |entity| entity['name'] }.include? new_name
        status 409
        return "Game with name '#{new_name}' exists already."
      end

      begin
        game = service.get_game_by_id(id)

        unless Auth::authentic?(game, secret)
          status 401
          return "unauthorized, please provide correct credentials"
        end

        game['name'] = new_name if new_name != nil
        game['url'] = new_url if new_url != nil
        game['signature'] = Auth::signature(id, new_secret) if new_secret != nil
        game['update'] = "#{ Time.now.utc.iso8601(6) }"

        File.open(storage, "w") { |file| file.write(JSON.pretty_generate(memory)) }
        return JSON.pretty_generate(game)
      rescue Exception => ex
        status 401
        return "#{ex}\nunauthorized, please provide correct credentials"
      end

    end

    #
    # Deletes a game and all of its associated game states.
    #
    # @param :id Unique identifier of the game (used for authentication). Required. Parameter is part of the REST-URI!
    # @param secret Existing secret of the game (used for authentication). Required. Parameter is part of request body.
    #
    # @return 200 OK, on successfull delete (response body includes confirmation message)
    # @return 401, on non matching access credentials (response body includes error message)
    #
    delete "/game/:id" do
      id     = params['id']
      secret = params['secret']

      game = service.get_game_by_id(id)

      if game == nil
        # This is an idempotent operation.
        return "Game '#{id}' deleted successfully."
      end

      unless Auth::authentic?(game, secret)
        status 401
        return "unauthorized, please provide correct credentials"
      end

      memory['games'].delete_if { |game| game['id'] == id }
      memory['gamestates'].delete_if { |state| state['gameid'] == id }
      File.open(storage, "w") { |file| file.write(JSON.pretty_generate(memory)) }

      "Game '#{id}' deleted successfully."
    end

    #
    # API Endpoint for Gamestate resources
    #

    #
    # Retrieves all gamestates stored for a game and a user.
    # Gamestates are returned sorted by decreasing creation timestamps.
    #
    # @param :gameid Unique identifier of the game (used for authentication). Required. Parameter is part of the REST-URI!
    # @param :userid Unique identifier of the user (used for authentication). Required. Parameter is part of the REST-URI!
    # @param secret Secret of the game (used for authentication). Required. Parameter is part of request body.
    #
    # @return 200 OK,  (response body includes confirmation message)
    # @return 401, on non matching access credentials (response body includes error message)
    # @return 404, not found (in case of gameid or userid are not existing) (response body includes error message)
    #
    get "/gamestate/:gameid/:userid" do

      gameid = params['gameid']
      userid = params['userid']
      secret = params['secret']

      game   = service.get_game_by_id(gameid)
      user   = service.get_user_by_id(userid)

      if game == nil || user == nil
        status 404
        "not found"
      end

      unless Auth::authentic?(game, secret)
        status 401
        return "unauthorized, please provide correct game credentials"
      end

      states = memory['gamestates'].select do |state|
        state['gameid'] == gameid && state['userid'] == userid
      end

      return JSON.pretty_generate(states.map { |state|
        r = state.clone
        r['gamename'] = service.get_game_by_id(r['gameid'])['name']
        r['username'] = service.get_user_by_id(r['userid'])['name']
        r
      }.sort { |b, a| Time.parse(a['created']) <=> Time.parse(b['created']) })
    end

    #
    # Retrieves all gamestates stored for a game.
    # Gamestates are returned sorted by decreasing creation timestamps.
    #
    # @param :gameid Unique identifier of the game (used for authentication). Required. Parameter is part of the REST-URI!
    # @param secret Secret of the game (used for authentication). Required. Parameter is part of request body.
    #
    # @return 200 OK,  (response body includes confirmation message)
    # @return 401, on non matching access credentials (response body includes error message)
    # @return 404, not found (in case of gameid or userid are not existing) (response body includes error message)
    #
    get "/gamestate/:gameid" do

      gameid = params['gameid']
      secret = params['secret']

      game   = service.get_game_by_id(gameid)

      if game == nil
        status 404
        "not found"
      end

      unless Auth::authentic?(game, secret)
        status 401
        return "unauthorized, please provide correct game credentials"
      end

      states = memory['gamestates'].select do |state|
        state['gameid'] == gameid
      end

      return JSON.pretty_generate(states.map { |state|
        r = state.clone
        r['gamename'] = service.get_game_by_id(r['gameid'])['name']
        r['username'] = service.get_user_by_id(r['userid'])['name']
        r
      }.sort { |b, a| Time.parse(a['created']) <=> Time.parse(b['created']) })
    end

    #
    # Stores a gamestate for a game and a user.
    #
    # @param :gameid Unique identifier of the game (used for authentication). Required. Parameter is part of the REST-URI!
    # @param :userid Unique identifier of the user (used for authentication). Required. Parameter is part of the REST-URI!
    # @param secret Secret of the game (used for authentication). Required. Parameter is part of request body.
    # @param state JSON encoded gamestate to store. Required.
    #
    # @return 200 OK (response body includes confirmation message)
    # @return 400, Bad request, in case of gamestate was empty or not encoded as valid JSON (response body includes error message)
    # @return 401, on non matching access credentials of game (response body includes error message)
    # @return 404, not found (in case of gameid or userid are not existing) (response body includes error message)
    #
    post "/gamestate/:gameid/:userid" do

      gameid = params['gameid']
      userid = params['userid']
      secret = params['secret']
      state  = params['state']

      game   = service.get_game_by_id(gameid)
      user   = service.get_user_by_id(userid)

      unless game != nil && user != nil
        status 404
        return "game id or user id not found"
      end

      unless Auth::authentic?(game, secret)
        status 401
        return "unauthorized, please provide correct game credentials"
      end

      begin
        state = JSON.parse(state)

        if state.empty?
          status 400
          return "Bad request: state must not be empty, was #{state}"
        end

        memory['gamestates'] << {
            "type"    => 'gamestate',
            "gameid"  => gameid,
            "userid"  => userid,
            "created" => "#{ Time.now.utc.iso8601(6) }",
            "state"   => state
        }

      rescue
        status 400
        return "Bad request: state must be provided as valid JSON, was #{state}"
      end

      File.open(storage, "w") { |file| file.write(JSON.pretty_generate(memory)) }

      "Added state."

    end

  end

end
get_game_by_id(id) click to toggle source

Gets game hash by id from memory. return nil, if id is not present

# File lib/gamekey.rb, line 57
def get_game_by_id(id)
  @memory['games'].select { |game| game['id'] == id }.first
end
get_user_by_id(id) click to toggle source

Gets user hash by id from memory. return nil, if id is not present

# File lib/gamekey.rb, line 43
def get_user_by_id(id)
  @memory['users'].select { |user| user['id'] == id }.first
end
get_user_by_name(name) click to toggle source

Gets user hash by name from memory. return nil, if name is not present

# File lib/gamekey.rb, line 50
def get_user_by_name(name)
  @memory['users'].select { |user| user['name'] == name }.first
end