API Update (#876)

This adds some useful functions to the API and redefines the way the params are checked.

API Versioning
Very important to be able to check from the client if the serverside API is still compatible with the client implementation
Refactored Params
Mark params as required / optional
Different types of params (string / integer / list / select / senderkey)
Name and description for params
API Function to get all the functions a specific ip/token combo is allowed to use
API Function to get details about a specific API function
This commit is contained in:
Werner
2016-09-11 14:53:20 +02:00
committed by skull132
parent 18ec817b67
commit 0a2df816c6
3 changed files with 227 additions and 29 deletions

View File

@@ -21,7 +21,7 @@ CREATE TABLE `ss13_admin_log` (
CREATE TABLE `ss13_api_functions` ( CREATE TABLE `ss13_api_functions` (
`id` INT(11) NOT NULL AUTO_INCREMENT, `id` INT(11) NOT NULL AUTO_INCREMENT,
`function` VARCHAR(50) NULL DEFAULT '' COLLATE 'utf8_bin', `function` VARCHAR(50) NOT NULL COLLATE 'utf8_bin',
`description` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8_bin', `description` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8_bin',
PRIMARY KEY (`id`) PRIMARY KEY (`id`)
) )

View File

@@ -1,6 +1,9 @@
// //
// This file contains the API functions for the serverside API // This file contains the API functions for the serverside API
// //
// IMPORTANT:
// When changing api functions always update the version number of the API
// The version number is defined in /datum/topic_command/api_get_version
//Init the API at startup //Init the API at startup
/hook/startup/proc/setup_api() /hook/startup/proc/setup_api()
@@ -12,8 +15,20 @@
//API Boilerplate //API Boilerplate
/datum/topic_command /datum/topic_command
var/name = null var/name = null //Name for the command
var/list/required_params = list() //Required Parameters for the command var/description = null //Description for the command
var/list/params = list() //Required Parameters for the command
//Explanation of the parameter options:
//Required - name -> Name of the parameter - should be the same as the index in the list
//Required - desc -> Description of the parameter
//Required - req -> Is this a required parameter: 1 -> Yes, 0 -> No
//Required - type -> What type is this:
// str->String,
// int->Integer,
// lst->List/array,
// senderkey->unique identifier of the person sending the request
// slct -> Select one of multiple specified options
//Required* - options -> The possible options that can be selected (slct)
var/statuscode = null var/statuscode = null
var/response = null var/response = null
var/data = null var/data = null
@@ -27,20 +42,150 @@
var/list/missing_params = list() var/list/missing_params = list()
var/errorcount = 0 var/errorcount = 0
for(var/param in required_params) for(var/key in params)
if(queryparams[param] == null) var/list/param = params[key]
if(queryparams[key] == null)
if(param["req"] == 0)
log_debug("API: The following parameter is OPTIONAL and missing: [param["name"]] - [param["desc"]]")
else
log_debug("API: The following parameter is REQUIRED but missing: [param["name"]] - [param["desc"]]")
errorcount ++ errorcount ++
missing_params += param missing_params += param["name"]
if(errorcount) if(errorcount)
log_debug("API: Request aborted. Required parameters missing")
statuscode = 400 statuscode = 400
response = "Required params missing" response = "Required params missing"
data = missing_params data = missing_params
return errorcount return errorcount
return 0
//
// API for the API
//
/datum/topic_command/api_get_version
name = "api_get_version"
description = "Gets the version of the API"
/datum/topic_command/api_get_version/run_command(queryparams)
var/list/version = list()
var/versionstring = null
//The Version Number follows SemVer http://semver.org/
version["major"] = 2 //Major Version Number --> Increment when implementing breaking changes
version["minor"] = 0 //Minor Version Number --> Increment when adding features
version["patch"] = 0 //Patchlevel --> Increment when fixing bugs
versionstring = "[version["major"]].[version["minor"]].[version["patch"]]"
statuscode = 200
response = versionstring
data = version
return 1
//Get all the functions a specific token / ip combo is authorized to use
/datum/topic_command/api_get_authed_functions
name = "api_get_authed_functions"
description = "Returns the functions that can be accessed by the requesting ip and token"
/datum/topic_command/api_get_authed_functions/run_command(queryparams)
var/list/functions = list()
//Check if DB Connection is established
if (!establish_db_connection(dbcon))
statuscode = 500
response = "DB Connection Unavailable"
return 1
var/DBQuery/functionsquery = dbcon.NewQuery({"SELECT api_f.function
FROM ss13_api_token_function as api_t_f, ss13_api_tokens as api_t, ss13_api_functions as api_f
WHERE api_t.id = api_t_f.token_id AND api_f.id = api_t_f.function_id
AND (
(token = :token AND ip = :ip)
OR
(token = :token AND ip IS NULL)
OR
(token IS NULL AND ip = :ip)
)"})
functionsquery.Execute(list(":token" = queryparams["auth"], ":ip" = queryparams["addr"]))
while (functionsquery.NextRow())
functions[functionsquery.item[1]] = functionsquery.item[1]
statuscode = 200
response = "Authorized functions retrieved"
data = functions
return 1
//Get details for a specific api function
/datum/topic_command/api_explain_function
name = "api_explain_function"
description = "Explains a specific API function"
params = list(
"function" = list("name"="function","desc"="The name of the API function that should be explained","req"=1,"type"="str")
)
/datum/topic_command/api_explain_function/run_command(queryparams)
var/datum/topic_command/apifunction = topic_commands[queryparams["function"]]
var/list/functiondata = list()
if (isnull(apifunction))
statuscode = 501
response = "Not Implemented - The requested function does not exist"
return 1
//Then query for auth
if (!establish_db_connection(dbcon))
statuscode = 500
response = "DB Connection Unavailable"
return 1
var/DBQuery/permquery = dbcon.NewQuery({"SELECT api_f.function
FROM ss13_api_token_function as api_t_f, ss13_api_tokens as api_t, ss13_api_functions as api_f
WHERE api_t.id = api_t_f.token_id AND api_f.id = api_t_f.function_id
AND api_t.deleted_at IS NULL
AND (
(token = :token AND ip = :ip AND function = :function)
OR
(token = :token AND ip IS NULL AND function = :function)
OR
(token = :token AND ip = :ip AND function = \"ANY\")
OR
(token = :token AND ip IS NULL AND function = \"ANY\")
OR
(token IS NULL AND ip IS NULL AND function = :function)
)"})
//Get the tokens and the associated functions
//Check if the token, the ip and the function matches OR
// the token + function matches and the ip is NULL (Functions that can be used by any ip, but require a token)
// the token + ip matches and the function is NULL (Allow a specific ip with a specific token to use all functions)
// the token + ip is NULL and the function matches (Allow a specific function to be used without auth)
permquery.Execute(list(":token" = queryparams["auth"], ":ip" = queryparams["addr"], ":function" = queryparams["function"]))
if (!permquery.RowCount())
statuscode = 401
response = "Unauthorized - To access this function"
return 1
functiondata["name"] = apifunction.name
functiondata["description"] = apifunction.description
functiondata["params"] = apifunction.params
statuscode = 200
response = "Function data retrieved"
data = functiondata
return 1
//
// API for the other stuff
//
//Char Names //Char Names
/datum/topic_command/get_char_list /datum/topic_command/get_char_list
name = "get_char_list" name = "get_char_list"
description = "Provides a list of all characters ingame"
/datum/topic_command/get_char_list/run_command(queryparams) /datum/topic_command/get_char_list/run_command(queryparams)
if (!ticker) if (!ticker)
statuscode = 500 statuscode = 500
@@ -62,6 +207,7 @@
//Admin Count //Admin Count
/datum/topic_command/get_count_admin /datum/topic_command/get_count_admin
name = "get_count_admin" name = "get_count_admin"
description = "Gets the number of admins connected"
/datum/topic_command/get_count_admin/run_command(queryparams) /datum/topic_command/get_count_admin/run_command(queryparams)
var/n = 0 var/n = 0
for (var/client/client in clients) for (var/client/client in clients)
@@ -74,8 +220,9 @@
return 1 return 1
//CCIA Count //CCIA Count
/datum/topic_command/get_count_ccia /datum/topic_command/get_count_cciaa
name = "get_count_ccia" name = "get_count_cciaa"
description = "Gets the number of ccia connected"
/datum/topic_command/get_count_ccia/run_command(queryparams) /datum/topic_command/get_count_ccia/run_command(queryparams)
var/n = 0 var/n = 0
for (var/client/client in clients) for (var/client/client in clients)
@@ -90,6 +237,7 @@
//Mod Count //Mod Count
/datum/topic_command/get_count_mod /datum/topic_command/get_count_mod
name = "get_count_mod" name = "get_count_mod"
description = "Gets the number of mods connected"
/datum/topic_command/get_count_mod/run_command(queryparams) /datum/topic_command/get_count_mod/run_command(queryparams)
var/n = 0 var/n = 0
for (var/client/client in clients) for (var/client/client in clients)
@@ -104,6 +252,7 @@
//Player Count //Player Count
/datum/topic_command/get_count_player /datum/topic_command/get_count_player
name = "get_count_player" name = "get_count_player"
description = "Gets the number of players connected"
/datum/topic_command/get_count_player/run_command(queryparams) /datum/topic_command/get_count_player/run_command(queryparams)
var/n = 0 var/n = 0
for(var/mob/M in player_list) for(var/mob/M in player_list)
@@ -118,6 +267,7 @@
//Get available Fax Machines //Get available Fax Machines
/datum/topic_command/get_faxmachines /datum/topic_command/get_faxmachines
name = "get_faxmachines" name = "get_faxmachines"
description = "Gets all available fax machines"
/datum/topic_command/get_faxmachines/run_command(queryparams) /datum/topic_command/get_faxmachines/run_command(queryparams)
var/list/faxlocations = list() var/list/faxlocations = list()
@@ -132,7 +282,10 @@
//Get Fax List //Get Fax List
/datum/topic_command/get_faxlist /datum/topic_command/get_faxlist
name = "get_faxlist" name = "get_faxlist"
required_params = list("faxtype") //Type of the faxes to be retrieved (sent / received) description = "Gets the list of faxes sent / received"
params = list(
"faxtype" = list("name"="faxtype","desc"="Type of the faxes that should be retrieved","req"=1,"type"="slct","options"=list("sent","received"))
)
/datum/topic_command/get_faxlist/run_command(queryparams) /datum/topic_command/get_faxlist/run_command(queryparams)
if (!ticker) if (!ticker)
statuscode = 500 statuscode = 500
@@ -167,7 +320,11 @@
//Get Specific Fax //Get Specific Fax
/datum/topic_command/get_fax /datum/topic_command/get_fax
name = "get_fax" name = "get_fax"
required_params = list("faxtype","faxid") description = "Gets a specific fax that has been sent or received"
params = list(
"faxtype" = list("name"="faxtype","desc"="Type of the faxes that should be retrieved","req"=1,"type"="slct","options"=list("sent","received")),
"faxid" = list("name"="faxid","desc"="ID of the fax that should be retrieved","req"=1,"type"="int")
)
/datum/topic_command/get_fax/run_command(queryparams) /datum/topic_command/get_fax/run_command(queryparams)
var/list/faxes = list() var/list/faxes = list()
switch (queryparams["faxtype"]) switch (queryparams["faxtype"])
@@ -237,6 +394,7 @@
//Get Ghosts //Get Ghosts
/datum/topic_command/get_ghosts /datum/topic_command/get_ghosts
name = "get_ghosts" name = "get_ghosts"
description = "Gets the ghosts"
/datum/topic_command/get_ghosts/run_command(queryparams) /datum/topic_command/get_ghosts/run_command(queryparams)
var/list/ghosts[] = list() var/list/ghosts[] = list()
ghosts = get_ghosts(1,1) ghosts = get_ghosts(1,1)
@@ -249,6 +407,7 @@
// Crew Manifest // Crew Manifest
/datum/topic_command/get_manifest /datum/topic_command/get_manifest
name = "get_manifest" name = "get_manifest"
description = "Gets the crew manifest"
/datum/topic_command/get_manifest/run_command(queryparams) /datum/topic_command/get_manifest/run_command(queryparams)
if (!ticker) if (!ticker)
statuscode = 500 statuscode = 500
@@ -294,6 +453,7 @@
//Player Ckeys //Player Ckeys
/datum/topic_command/get_player_list /datum/topic_command/get_player_list
name = "get_player_list" name = "get_player_list"
description = "Gets a list of connected players"
/datum/topic_command/get_player_list/run_command(queryparams) /datum/topic_command/get_player_list/run_command(queryparams)
var/list/players = list() var/list/players = list()
for (var/client/C in clients) for (var/client/C in clients)
@@ -307,7 +467,10 @@
//Get info about a specific player //Get info about a specific player
/datum/topic_command/get_player_info /datum/topic_command/get_player_info
name = "get_player_info" name = "get_player_info"
required_params = list("search") //search --> list with data to search for description = "Gets information about a specific player"
params = list(
"search" = list("name"="search","desc"="List with strings that should be searched for","req"=1,"type"="lst")
)
/datum/topic_command/get_player_info/run_command(queryparams) /datum/topic_command/get_player_info/run_command(queryparams)
var/list/search = queryparams["search"] var/list/search = queryparams["search"]
@@ -385,6 +548,7 @@
//Get Server Status //Get Server Status
/datum/topic_command/get_serverstatus /datum/topic_command/get_serverstatus
name = "get_serverstatus" name = "get_serverstatus"
description = "Gets the serverstatus"
/datum/topic_command/get_serverstatus/run_command(queryparams) /datum/topic_command/get_serverstatus/run_command(queryparams)
var/list/s[] = list() var/list/s[] = list()
s["version"] = game_version s["version"] = game_version
@@ -436,6 +600,7 @@
//Get a Staff List //Get a Staff List
/datum/topic_command/get_stafflist /datum/topic_command/get_stafflist
name = "get_stafflist" name = "get_stafflist"
description = "Gets a list of connected staffmembers"
/datum/topic_command/get_stafflist/run_command(queryparams) /datum/topic_command/get_stafflist/run_command(queryparams)
var/list/staff = list() var/list/staff = list()
for (var/client/C in admins) for (var/client/C in admins)
@@ -449,7 +614,11 @@
//Grant Respawn //Grant Respawn
/datum/topic_command/grant_respawn /datum/topic_command/grant_respawn
name = "grant_respawn" name = "grant_respawn"
required_params = list("senderkey","target") description = "Grants a respawn to a specific target"
params = list(
"senderkey" = list("name"="senderkey","desc"="Unique id of the person that authorized the respawn","req"=1,"type"="senderkey"),
"target" = list("name"="target","desc"="Ckey of the target that should be granted a respawn","req"=1,"type"="str")
)
/datum/topic_command/grant_respawn/run_command(queryparams) /datum/topic_command/grant_respawn/run_command(queryparams)
var/list/ghosts = get_ghosts(1,1) var/list/ghosts = get_ghosts(1,1)
var/target = queryparams["target"] var/target = queryparams["target"]
@@ -504,6 +673,7 @@
//Ping Test //Ping Test
/datum/topic_command/ping /datum/topic_command/ping
name = "ping" name = "ping"
description = "API test command"
/datum/topic_command/ping/run_command(queryparams) /datum/topic_command/ping/run_command(queryparams)
var/x = 1 var/x = 1
for (var/client/C) for (var/client/C)
@@ -516,7 +686,10 @@
//Restart Round //Restart Round
/datum/topic_command/restart_round /datum/topic_command/restart_round
name = "restart_round" name = "restart_round"
required_params = list("senderkey") description = "Restarts the round"
params = list(
"senderkey" = list("name"="senderkey","desc"="Unique id of the person that authorized the restart","req"=1,"type"="senderkey")
)
/datum/topic_command/restart_round/run_command(queryparams) /datum/topic_command/restart_round/run_command(queryparams)
var/senderkey = sanitize(queryparams["senderkey"]) //Identifier of the sender (Ckey / Userid / ...) var/senderkey = sanitize(queryparams["senderkey"]) //Identifier of the sender (Ckey / Userid / ...)
@@ -539,7 +712,13 @@
//Get available Fax Machines //Get available Fax Machines
/datum/topic_command/send_adminmsg /datum/topic_command/send_adminmsg
name = "send_adminmsg" name = "send_adminmsg"
required_params = list("ckey","msg","senderkey") description = "Sends a adminmessage to a player"
params = list(
"ckey" = list("name"="ckey","desc"="The target of the adminmessage","req"=1,"type"="str"),
"msg" = list("name"="msg","desc"="The message that should be sent","req"=1,"type"="str"),
"senderkey" = list("name"="senderkey","desc"="Unique id of the person that sent the adminmessage","req"=1,"type"="senderkey"),
"rank" = list("name"="rank","desc"="The rank that should be displayed - Defaults to admin if none specified","req"=0,"type"="str"),
)
/datum/topic_command/send_adminmsg/run_command(queryparams) /datum/topic_command/send_adminmsg/run_command(queryparams)
/* /*
We got an adminmsg from IRC bot lets split the API We got an adminmsg from IRC bot lets split the API
@@ -589,7 +768,15 @@
//Send a Command Report //Send a Command Report
/datum/topic_command/send_commandreport /datum/topic_command/send_commandreport
name = "send_commandreport" name = "send_commandreport"
required_params = list("senderkey","body") description = "Sends a command report"
params = list(
"senderkey" = list("name"="senderkey","desc"="Unique id of the person that sent the commandreport","req"=1,"type"="senderkey"),
"title" = list("name"="title","desc"="The message title that should be sent, Defaults to NanoTrasen Update if not specified","req"=0,"type"="str"),
"body" = list("name"="body","desc"="The message body that should be sent","req"=1,"type"="str"),
"type" = list("name"="type","desc"="The type of the message that should be sent, Defaults to freeform","req"=0,"type"="slct","options"=list("freeform","ccia")),
"sendername" = list("name"="sendername","desc"="IC Name of the sender for the CCIA Report, Defaults to CCIAAMS, \[Command-StationName\]","req"=0,"type"="string"),
"announce" = list("name"="announce","desc"="If the report should be announce 1 -> Yes, 0 -> No, Defaults to 1","req"=0,"type"="int")
)
/datum/topic_command/send_commandreport/run_command(queryparams) /datum/topic_command/send_commandreport/run_command(queryparams)
var/senderkey = sanitize(queryparams["senderkey"]) //Identifier of the sender (Ckey / Userid / ...) var/senderkey = sanitize(queryparams["senderkey"]) //Identifier of the sender (Ckey / Userid / ...)
var/reporttitle = sanitizeSafe(queryparams["title"]) //Title of the report var/reporttitle = sanitizeSafe(queryparams["title"]) //Title of the report
@@ -641,7 +828,13 @@
//Send Fax //Send Fax
/datum/topic_command/send_fax /datum/topic_command/send_fax
name = "send_fax" name = "send_fax"
required_params = list("target","senderkey","title","body") description = "Sends a fax"
params = list(
"senderkey" = list("name"="senderkey","desc"="Unique id of the person that sent the fax","req"=1,"type"="senderkey"),
"title" = list("name"="title","desc"="The message title that should be sent","req"=1,"type"="str"),
"body" = list("name"="body","desc"="The message body that should be sent","req"=1,"type"="str"),
"target" = list("name"="target","desc"="The target faxmachines the fax should be sent to","req"=1,"type"="lst")
)
/datum/topic_command/send_fax/run_command(queryparams) /datum/topic_command/send_fax/run_command(queryparams)
var/list/responselist = list() var/list/responselist = list()
var/list/sendsuccess = list() var/list/sendsuccess = list()
@@ -649,7 +842,6 @@
var/senderkey = sanitize(queryparams["senderkey"]) //Identifier of the sender (Ckey / Userid / ...) var/senderkey = sanitize(queryparams["senderkey"]) //Identifier of the sender (Ckey / Userid / ...)
var/faxtitle = sanitizeSafe(queryparams["title"]) //Title of the report var/faxtitle = sanitizeSafe(queryparams["title"]) //Title of the report
var/faxbody = sanitize(queryparams["body"]) //Body of the report var/faxbody = sanitize(queryparams["body"]) //Body of the report
var/faxsender = sanitize(queryparams["sendername"]) //Name of the sender
var/faxannounce = queryparams["announce"] //Announce the contents report to the public: 1 / 0 var/faxannounce = queryparams["announce"] //Announce the contents report to the public: 1 / 0
if(!targetlist || targetlist.len < 1) if(!targetlist || targetlist.len < 1)
@@ -679,8 +871,8 @@
else else
command_announcement.Announce("A fax message from Central Command has been sent to the following fax machines: <br>"+list2text(sendsuccess, ", "), "Fax Received", new_sound = 'sound/AI/commandreport.ogg', msg_sanitized = 1); command_announcement.Announce("A fax message from Central Command has been sent to the following fax machines: <br>"+list2text(sendsuccess, ", "), "Fax Received", new_sound = 'sound/AI/commandreport.ogg', msg_sanitized = 1);
log_admin("[faxsender] sent a fax via the API: : [faxbody]") log_admin("[senderkey] sent a fax via the API: : [faxbody]")
message_admins("[faxsender] sent a fax via the API", 1) message_admins("[senderkey] sent a fax via the API", 1)
statuscode = 200 statuscode = 200
response = "Fax sent" response = "Fax sent"

View File

@@ -113,6 +113,7 @@ var/list/world_api_rate_limit = list()
/world/Topic(T, addr, master, key) /world/Topic(T, addr, master, key)
var/list/response[] = list() var/list/response[] = list()
var/list/queryparams[] = json_decode(T) var/list/queryparams[] = json_decode(T)
queryparams["addr"] = addr //Add the IP to the queryparams that are passed to the api functions
var/query = queryparams["query"] var/query = queryparams["query"]
var/auth = queryparams["auth"] var/auth = queryparams["auth"]
log_debug("API: Request Received - from:[addr], master:[master], key:[key]") log_debug("API: Request Received - from:[addr], master:[master], key:[key]")
@@ -418,23 +419,28 @@ var/list/world_api_rate_limit = list()
if (!establish_db_connection(dbcon)) if (!establish_db_connection(dbcon))
return 3 //DB Unavailable return 3 //DB Unavailable
var/DBQuery/authquery = dbcon.NewQuery({"SELECT * var/DBQuery/authquery = dbcon.NewQuery({"SELECT api_f.function
FROM ss13_api_token_function as api_t_f, ss13_api_tokens as api_t, ss13_api_functions as api_f FROM ss13_api_token_function as api_t_f, ss13_api_tokens as api_t, ss13_api_functions as api_f
WHERE api_t.id = api_t_f.token_id AND api_f.id = api_t_f.function_id WHERE api_t.id = api_t_f.token_id AND api_f.id = api_t_f.function_id
AND api_t.deleted_at IS NULL
AND ( AND (
(token = :token AND ip = :ip AND function = :function) (token = :token AND ip = :ip AND function = :function)
OR OR
(token = :token AND ip IS NULL AND function = :function) (token = :token AND ip IS NULL AND function = :function)
OR OR
(token = :token AND ip = :ip AND function IS NULL) (token = :token AND ip = :ip AND function = \"ANY\")
OR OR
(token IS NULL AND ip = :ip AND function IS NULL) (token = :token AND ip IS NULL AND function = \"ANY\")
OR
(token IS NULL AND ip IS NULL AND function = :function)
)"}) )"})
//Get the tokens and the associated functions //Check if the token is not deleted
//Check if the token, the ip and the function matches OR //Check if one of the following is true:
// the token + function matches and the ip is NULL (Functions that can be used by any ip, but require a token) // Full Match - Token IP and Function Matches
// the token + ip matches and the function is NULL (Allow a specific ip with a specific token to use all functions) // Any IP - Token and Function Matches, IP is set to NULL (not required)
// the token + ip is NULL and the function matches (Allow a specific function to be used without auth) // Any Function - Token and IP Matches, Function is set to ANY
// Any Function, Any IP - Token Matches, IP is set to NULL (not required), Function is set to ANY
// Public - Token is set to NULL, IP is set to NULL and function matches
authquery.Execute(list(":token" = auth, ":ip" = addr, ":function" = function)) authquery.Execute(list(":token" = auth, ":ip" = addr, ":function" = function))
log_debug("API: Auth Check - Query Executed - Returned Rows: [authquery.RowCount()]") log_debug("API: Auth Check - Query Executed - Returned Rows: [authquery.RowCount()]")