API Update 2 (#932)

This is another Update to the API that adds / changes:

Unified Naming of API Comands and API Functions. --> They are now called "API Commands" across all files and the DB
Moves API Related procs from world.dm to api.dm
Adds a proc to write the API commands to the db (so there is no need to manually maintain the list of API commands)
The name change needs to be done before the API is live.

No changelog because there is already a entry
This commit is contained in:
Werner
2016-09-17 17:01:02 +02:00
committed by skull132
parent 958d95af06
commit 6bd2247d07
4 changed files with 796 additions and 759 deletions

View File

@@ -19,15 +19,17 @@ CREATE TABLE `ss13_admin_log` (
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `ss13_api_functions` (
CREATE TABLE `ss13_api_commands` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`function` VARCHAR(50) NOT NULL COLLATE 'utf8_bin',
`command` VARCHAR(50) NOT NULL COLLATE 'utf8_bin',
`description` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8_bin',
PRIMARY KEY (`id`)
PRIMARY KEY (`id`),
UNIQUE INDEX `UNIQUE command` (`command`)
)
COLLATE='utf8_bin'
ENGINE=InnoDB;
CREATE TABLE `ss13_api_tokens` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`token` VARCHAR(100) NOT NULL COLLATE 'utf8_bin',
@@ -42,18 +44,19 @@ CREATE TABLE `ss13_api_tokens` (
COLLATE='utf8_bin'
ENGINE=InnoDB;
CREATE TABLE `ss13_api_token_function` (
`function_id` INT(11) NOT NULL,
CREATE TABLE `ss13_api_token_command` (
`command_id` INT(11) NOT NULL,
`token_id` INT(11) NOT NULL,
PRIMARY KEY (`function_id`, `token_id`),
PRIMARY KEY (`command_id`, `token_id`),
INDEX `token_id` (`token_id`),
CONSTRAINT `function_id` FOREIGN KEY (`function_id`) REFERENCES `ss13_api_functions` (`id`) ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT `function_id` FOREIGN KEY (`command_id`) REFERENCES `ss13_api_commands` (`id`) ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT `token_id` FOREIGN KEY (`token_id`) REFERENCES `ss13_api_tokens` (`id`) ON UPDATE CASCADE ON DELETE CASCADE
)
COLLATE='utf8_bin'
ENGINE=InnoDB;
CREATE TABLE `ss13_ban` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`bantime` datetime NOT NULL,

View File

@@ -12,6 +12,7 @@ var/global/list/silicon_mob_list = list() //List of all silicon mobs, includin
var/global/list/living_mob_list = list() //List of all alive mobs, including clientless. Excludes /mob/new_player
var/global/list/dead_mob_list = list() //List of all dead mobs, including clientless. Excludes /mob/new_player
var/global/list/topic_commands = list() //List of all API commands available
var/global/list/topic_commands_names = list() //List of all API commands available
var/global/list/cable_list = list() //Index for all cables, so that powernets don't have to look through the entire world all the time
var/global/list/chemical_reactions_list //list of all /datum/chemical_reaction datums. Used during chemical reactions

View File

@@ -1,21 +1,90 @@
//
// This file contains the API functions for the serverside API
// This file contains the API commands for the serverside API
//
// IMPORTANT:
// When changing api functions always update the version number of the API
// When changing api commands 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
/hook/startup/proc/setup_api()
for (var/path in typesof(/datum/topic_command) - /datum/topic_command)
var/datum/topic_command/A = new path()
if(A != null)
topic_commands[A.name] = A
topic_commands_names.Add(A.name)
listclearnulls(topic_commands)
listclearnulls(topic_commands_names)
return 1
/world/proc/api_do_auth_check(var/addr, var/auth, var/datum/topic_command/command)
//Check if command is on nothrottle list
if(command.no_throttle == 1)
log_debug("API: Throttling bypassed - Command [command.name] set to no_throttle")
else
if(world_api_rate_limit[addr] != null && config.api_rate_limit_whitelist[addr] == null) //Check if the ip is in the rate limiting list and not in the whitelist
if(abs(world_api_rate_limit[addr] - world.time) < config.api_rate_limit) //Check the last request time of the ip
world_api_rate_limit[addr] = world.time // Set the time of the last request
return 2 //Throttled
world_api_rate_limit[addr] = world.time // Set the time of the last request
//Check if the command is on the auth whitelist
if(command.no_auth == 1)
log_debug("API: Auth bypassed - Command [command.name] set to no_auth")
return 0 // Authed (bypassed)
var/DBQuery/authquery = dbcon.NewQuery({"SELECT api_f.command
FROM ss13_api_token_command as api_t_f, ss13_api_tokens as api_t, ss13_api_commands as api_f
WHERE api_t.id = api_t_f.token_id AND api_f.id = api_t_f.command_id
AND api_t.deleted_at IS NULL
AND (
(token = :token AND ip = :ip AND command = :command)
OR
(token = :token AND ip IS NULL AND command = :command)
OR
(token = :token AND ip = :ip AND command = \"_ANY\")
OR
(token = :token AND ip IS NULL AND command = \"_ANY\")
OR
(token IS NULL AND ip IS NULL AND command = :command)
)"})
//Check if the token is not deleted
//Check if one of the following is true:
// Full Match - Token IP and Command Matches
// Any IP - Token and Command Matches, IP is set to NULL (not required)
// Any Command - Token and IP Matches, Command is set to _ANY
// Any Command, Any IP - Token Matches, IP is set to NULL (not required), Command is set to _ANY
// Public - Token is set to NULL, IP is set to NULL and command matches
authquery.Execute(list(":token" = auth, ":ip" = addr, ":command" = command.name))
log_debug("API: Auth Check - Query Executed - Returned Rows: [authquery.RowCount()]")
if (authquery.RowCount())
return 0 // Authed
return 1 // Bad Key
proc/api_update_command_database()
log_debug("API: DB Command Update Called")
//Check if DB Connection is established
if (!establish_db_connection(dbcon))
return 0 //Error
var/DBQuery/commandinsertquery = dbcon.NewQuery({"INSERT INTO ss13_api_commands (command,description)
VALUES (:command_name,:command_description)
ON DUPLICATE KEY UPDATE description = :command_description;"})
for(var/com in topic_commands)
var/datum/topic_command/command = topic_commands[com]
commandinsertquery.Execute(list(":command_name" = command.name, ":command_description" = command.description))
log_debug("API: DB Command Update Executed")
return 1 //OK
//API Boilerplate
/datum/topic_command
var/name = null //Name for the command
var/no_auth = 0 //If the user does NOT need to be authed to use the command
var/no_throttle = 0 //If this command should NOT be limited by the throtteling
var/description = null //Description for the command
var/list/params = list() //Required Parameters for the command
//Explanation of the parameter options:
@@ -65,6 +134,8 @@
/datum/topic_command/api_get_version
name = "api_get_version"
description = "Gets the version of the API"
no_auth = 1
no_throttle = 1
/datum/topic_command/api_get_version/run_command(queryparams)
var/list/version = list()
var/versionstring = null
@@ -81,12 +152,12 @@
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()
//Get all the commands a specific token / ip combo is authorized to use
/datum/topic_command/api_get_authed_commands
name = "api_get_authed_commands"
description = "Returns the commands that can be accessed by the requesting ip and token"
/datum/topic_command/api_get_authed_commands/run_command(queryparams)
var/list/commands = list()
//Check if DB Connection is established
@@ -95,42 +166,49 @@
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
var/DBQuery/commandsquery = dbcon.NewQuery({"SELECT api_f.command
FROM ss13_api_token_command as api_t_f, ss13_api_tokens as api_t, ss13_api_commands as api_f
WHERE api_t.id = api_t_f.token_id AND api_f.id = api_t_f.command_id
AND (
(token = :token AND ip = :ip)
OR
(token = :token AND ip IS NULL)
OR
(token IS NULL AND ip = :ip)
)"})
)
ORDER BY command DESC"})
functionsquery.Execute(list(":token" = queryparams["auth"], ":ip" = queryparams["addr"]))
while (functionsquery.NextRow())
functions[functionsquery.item[1]] = functionsquery.item[1]
commandsquery.Execute(list(":token" = queryparams["auth"], ":ip" = queryparams["addr"]))
while (commandsquery.NextRow())
commands[commandsquery.item[1]] = commandsquery.item[1]
if(commandsquery.item[1] == "_ANY")
statuscode = 200
response = "Authorized functions retrieved"
data = functions
response = "Authorized commands retrieved - ALL"
data = topic_commands_names
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()
statuscode = 200
response = "Authorized commands retrieved"
data = commands
return 1
if (isnull(apifunction))
//Get details for a specific api command
/datum/topic_command/api_explain_command
name = "api_explain_command"
description = "Explains a specific API command"
no_throttle = 1
params = list(
"command" = list("name"="command","desc"="The name of the API command that should be explained","req"=1,"type"="str")
)
/datum/topic_command/api_explain_command/run_command(queryparams)
var/datum/topic_command/apicommand = topic_commands[queryparams["command"]]
var/list/commanddata = list()
if (isnull(apicommand))
statuscode = 501
response = "Not Implemented - The requested function does not exist"
response = "Not Implemented - The requested command does not exist"
return 1
//Then query for auth
@@ -139,44 +217,53 @@
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
var/DBQuery/permquery = dbcon.NewQuery({"SELECT api_f.command
FROM ss13_api_token_command as api_t_f, ss13_api_tokens as api_t, ss13_api_commands as api_f
WHERE api_t.id = api_t_f.token_id AND api_f.id = api_t_f.command_id
AND api_t.deleted_at IS NULL
AND (
(token = :token AND ip = :ip AND function = :function)
(token = :token AND ip = :ip AND command = :command)
OR
(token = :token AND ip IS NULL AND function = :function)
(token = :token AND ip IS NULL AND command = :command)
OR
(token = :token AND ip = :ip AND function = \"ANY\")
(token = :token AND ip = :ip AND command = \"_ANY\")
OR
(token = :token AND ip IS NULL AND function = \"ANY\")
(token = :token AND ip IS NULL AND command = \"_ANY\")
OR
(token IS NULL AND ip IS NULL AND function = :function)
(token IS NULL AND ip IS NULL AND command = :command)
)"})
//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)
//Get the tokens and the associated commands
//Check if the token, the ip and the command matches OR
// the token + command matches and the ip is NULL (commands that can be used by any ip, but require a token)
// the token + ip matches and the command is NULL (Allow a specific ip with a specific token to use all commands)
// the token + ip is NULL and the command matches (Allow a specific command to be used without auth)
permquery.Execute(list(":token" = queryparams["auth"], ":ip" = queryparams["addr"], ":function" = queryparams["function"]))
permquery.Execute(list(":token" = queryparams["auth"], ":ip" = queryparams["addr"], ":command" = queryparams["command"]))
if (!permquery.RowCount())
statuscode = 401
response = "Unauthorized - To access this function"
response = "Unauthorized - To access this command"
return 1
functiondata["name"] = apifunction.name
functiondata["description"] = apifunction.description
functiondata["params"] = apifunction.params
commanddata["name"] = apicommand.name
commanddata["description"] = apicommand.description
commanddata["params"] = apicommand.params
statuscode = 200
response = "Function data retrieved"
data = functiondata
response = "Command data retrieved"
data = commanddata
return 1
/datum/topic_command/update_command_database
name = "update_command_database"
description = "Updates the available topic commands in the database"
/datum/topic_command/update_command_database/run_command(queryparams)
api_update_command_database()
statuscode = 200
response = "Database Updated"
return 1
//
// API for the other stuff
@@ -187,11 +274,6 @@
name = "get_char_list"
description = "Provides a list of all characters ingame"
/datum/topic_command/get_char_list/run_command(queryparams)
if (!ticker)
statuscode = 500
response = "Game not started yet!"
return 1
var/list/chars = list()
var/list/mobs = sortmobs()
@@ -287,12 +369,6 @@
"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)
if (!ticker)
statuscode = 500
response = "Round hasn't started yet! No faxes to display!"
data = null
return 1
var/list/faxes = list()
switch (queryparams["faxtype"])
if ("received")
@@ -409,11 +485,6 @@
name = "get_manifest"
description = "Gets the crew manifest"
/datum/topic_command/get_manifest/run_command(queryparams)
if (!ticker)
statuscode = 500
response = "Game not started yet!"
return 1
var/list/positions = list()
var/list/set_names = list(
"heads" = command_positions,
@@ -719,6 +790,7 @@
"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)
/*
We got an adminmsg from IRC bot lets split the API
@@ -780,9 +852,9 @@
/datum/topic_command/send_commandreport/run_command(queryparams)
var/senderkey = sanitize(queryparams["senderkey"]) //Identifier of the sender (Ckey / Userid / ...)
var/reporttitle = sanitizeSafe(queryparams["title"]) //Title of the report
var/reportbody = sanitize(queryparams["body"]) //Body of the report
var/reportbody = sanitizeSafe(queryparams["body"]) //Body of the report
var/reporttype = queryparams["type"] //Type of the report: freeform / ccia / admin
var/reportsender = sanitize(queryparams["sendername"]) //Name of the sender
var/reportsender = sanitizeSafe(queryparams["sendername"]) //Name of the sender
var/reportannounce = queryparams["announce"] //Announce the contents report to the public: 1 / 0
if(!reporttitle)
@@ -811,7 +883,7 @@
reportbody += "<br/><br/>- CCIAAMS, [commstation_name()]"
if(reportannounce == 1)
command_announcement.Announce(reportbody, reporttitle, new_sound = 'sound/AI/commandreport.ogg', msg_sanitized = 1);
command_announcement.Announce(reportbody, reporttitle, new_sound = 'sound/AI/commandreport.ogg', do_newscast = 1, msg_sanitized = 1);
if(reportannounce == 0)
world << "\red New NanoTrasen Update available at all communication consoles."
world << sound('sound/AI/commandreport.ogg')

View File

@@ -119,13 +119,27 @@ var/list/world_api_rate_limit = list()
log_debug("API: Request Received - from:[addr], master:[master], key:[key]")
diary << "TOPIC: \"[T]\", from:[addr], master:[master], key:[key], auth:[auth] [log_end]"
if (!ticker) //If the game is not started most API Requests would not work because of the throtteling
response["statuscode"] = 500
response["response"] = "Game not started yet!"
return json_encode(response)
if (isnull(query))
log_debug("API - Bad Request - No query specified")
response["statuscode"] = 400
response["response"] = "Bad Request - No query specified"
return json_encode(response)
var/unauthed = do_auth_check(addr,auth,query)
var/datum/topic_command/command = topic_commands[query]
//Check if that command exists
if (isnull(command))
log_debug("API: Unknown command called: [query]")
response["statuscode"] = 501
response["response"] = "Not Implemented"
return json_encode(response)
var/unauthed = api_do_auth_check(addr,auth,command)
if (unauthed)
if (unauthed == 3)
log_debug("API: Request denied - Auth Service Unavailable")
@@ -143,16 +157,7 @@ var/list/world_api_rate_limit = list()
response["response"] = "Bad Auth"
return json_encode(response)
log_debug("API: Auth valid")
var/datum/topic_command/command = topic_commands[query]
if (isnull(command))
log_debug("API: Unknown command called: [query]")
response["statuscode"] = 501
response["response"] = "Not Implemented"
return json_encode(response)
if(command.check_params_missing(queryparams))
log_debug("API: Mising Params - Status: [command.statuscode] - Response: [command.response]")
@@ -162,7 +167,7 @@ var/list/world_api_rate_limit = list()
return json_encode(response)
else
command.run_command(queryparams)
log_debug("API: Function called: [query] - Status: [command.statuscode] - Response: [command.response]")
log_debug("API: Command called: [query] - Status: [command.statuscode] - Response: [command.response]")
response["statuscode"] = command.statuscode
response["response"] = command.response
response["data"] = command.data
@@ -454,47 +459,3 @@ var/inerror = 0
return 1
#undef FAILED_DB_CONNECTION_CUTOFF
/world/proc/do_auth_check(var/addr, var/auth, var/function)
//Check if rate limited
if(world_api_rate_limit[addr] != null && config.api_rate_limit_whitelist[addr] == null) //Check if the ip is in the rate limiting list and not in the whitelist
if(abs(world_api_rate_limit[addr] - world.time) < config.api_rate_limit) //Check the last request time of the ip
world_api_rate_limit[addr] = world.time // Set the time of the last request
return 2 //Throttled
world_api_rate_limit[addr] = world.time // Set the time of the last request
//Then query for auth
if (!establish_db_connection(dbcon))
return 3 //DB Unavailable
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
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)
)"})
//Check if the token is not deleted
//Check if one of the following is true:
// Full Match - Token IP and Function Matches
// Any IP - Token and Function Matches, IP is set to NULL (not required)
// 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))
log_debug("API: Auth Check - Query Executed - Returned Rows: [authquery.RowCount()]")
if (authquery.RowCount())
return 0 // Authed
else
return 1 // Bad Key