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