From c93241fe91c6446d423a44e7a6ed5f5426032222 Mon Sep 17 00:00:00 2001 From: Sven Slootweg Date: Wed, 23 Jan 2013 06:44:21 +0100 Subject: [PATCH] First bits of an API --- docs/api/client.html | 221 ++++++++++++++++++++++ docs/api/client.zpy | 230 +++++++++++++++++++++++ frontend/authenticators/api/client.php | 57 ++++++ frontend/classes/apikey.php | 86 +++++++++ frontend/includes/include.constants.php | 4 + frontend/includes/include.misc.php | 63 +++++++ frontend/modules/api/client/vps/list.php | 66 +++++++ frontend/modules/error/api/access.php | 2 + frontend/modules/test.php | 11 +- frontend/rewrite.php | 62 +++--- 10 files changed, 780 insertions(+), 22 deletions(-) create mode 100644 docs/api/client.html create mode 100644 docs/api/client.zpy create mode 100644 frontend/authenticators/api/client.php create mode 100644 frontend/classes/apikey.php create mode 100644 frontend/modules/api/client/vps/list.php create mode 100644 frontend/modules/error/api/access.php diff --git a/docs/api/client.html b/docs/api/client.html new file mode 100644 index 0000000..9784018 --- /dev/null +++ b/docs/api/client.html @@ -0,0 +1,221 @@ + + + + + + +

CVM Client API Documentation

Table of contents

Overview

The CVM Client API is a more or less RESTful API. That means it uses the standard HTTP 'verbs' like GET, POST, DELETE, etc. to execute certain commands. Authentication takes place per request (there is no concept of 'sessions') through the use of custom HTTP headers. Each API token pair is linked to a particular user, and can only be used for that user. A user can have multiple token pairs. Token pairs can be revoked at any time via the panel.

Authentication

The CVM Client API expects two custom HTTP headers as authentication.
API-Public-Token
This is the public part of your API token pair. It's used to identify who you are.
API-Private-Token
This is the private part of your API token pair. It's used to verify your access.
If no valid token pair is passed on with your request, the server will return a 401 Not Authorized status code.
If your token pair does not have access to the client API, the server will return a 403 Forbidden status code.

Response format

The API responses will always be in JSON format. If errors occurred, an errors key will be present containing an array of errors. If there is a response, a response key will be present containing the response.
Example: Valid API call
Code:
/api/client/vps/list
Output:
{
+    "response": {
+	"vpses": [{
+	    "id": "1",
+	    "virtualization_type": "1",
+	    "hostname": "test-vz.cryto.net",
+	    "guaranteed_ram": "128",
+	    "burstable_ram": "256",
+	    "disk_space": "5000",
+	    "cpu_count": "1",
+	    "traffic_in_limit": "500000000000",
+	    "traffic_out_limit": "500000000000",
+	    "traffic_in_used": "912727849",
+	    "traffic_out_used": "16923948"
+	}, {
+	    "id": "2",
+	    "virtualization_type": "1",
+	    "hostname": "test2.cryto.net",
+	    "guaranteed_ram": "512",
+	    "burstable_ram": "768",
+	    "disk_space": "40000",
+	    "cpu_count": "240",
+	    "traffic_in_limit": "500000000000",
+	    "traffic_out_limit": "500000000000",
+	    "traffic_in_used": "0",
+	    "traffic_out_used": "0"
+	}]
+    }
+}
Example: API call with invalid token pair
Code:
/api/client/vps/list
Output:
{
+    "errors": ["No valid API token pair was specified."]
+}

API Calls

GET /api/client/vps/list
This call will return a list of VPSes associated with the currently authenticated user. It takes no arguments.

Keys in the response objects

id
The numeric ID of this VPS. You will need this in further API calls.
node
The host node that this VPS exists on. You will need this in further node-related API calls.
virtualization_type
The virtualization platform used for this VPS. Right now the only supported value is 1 (OpenVZ).
hostname
The configured hostname of the VPS.
guaranteed_ram
The configured amount of guaranteed RAM, in megabytes.
burstable_ram
The configured amount of burstable RAM, in megabytes. This key may not be present if vSwap is used.
disk_space
The configured amount of disk space, in megabytes.
cpu_count
The amount of configured CPUs (or rather, CPU units) that this VPS has access to.
traffic_limit
The total traffic limit, in bytes. This may not be present, depending on the method of traffic measuring. See the explanation below.
traffic_used
The total amount of traffic used, in bytes. This may not be present, depending on the method of traffic measuring. See the explanation below.
traffic_in_limit
The total incoming traffic limit, in bytes. This may not be present, depending on the method of traffic measuring. See the explanation below.
traffic_in_used
The total amount of incoming traffic used, in bytes. This may not be present, depending on the method of traffic measuring. See the explanation below.
traffic_out_limit
The total outgoing traffic limit, in bytes. This may not be present, depending on the method of traffic measuring. See the explanation below.
traffic_out_used
The total amount of outgoing traffic used, in bytes. This may not be present, depending on the method of traffic measuring. See the explanation below.
Important: If traffic accounting for the VPS is combined (incoming + outgoing), a traffic_limit and traffic_used key will be present. If traffic accounting for the VPS is split, the keys traffic_in_limit, traffic_in_used, traffic_out_limit, and traffic_out_used will be present.
Example: Valid call to /api/client/vps/list
Output:
{
+    "response": {
+	"vpses": [{
+	    "id": "1",
+	    "node": "1",
+	    "virtualization_type": "1",
+	    "hostname": "test-vz.cryto.net",
+	    "guaranteed_ram": "128",
+	    "burstable_ram": "256",
+	    "disk_space": "5000",
+	    "cpu_count": "1",
+	    "traffic_in_limit": "500000000000",
+	    "traffic_out_limit": "500000000000",
+	    "traffic_in_used": "912727849",
+	    "traffic_out_used": "16923948"
+	}, {
+	    "id": "2",
+	    "node": "1",
+	    "virtualization_type": "1",
+	    "hostname": "test2.cryto.net",
+	    "guaranteed_ram": "512",
+	    "burstable_ram": "768",
+	    "disk_space": "40000",
+	    "cpu_count": "2",
+	    "traffic_in_limit": "500000000000",
+	    "traffic_out_limit": "500000000000",
+	    "traffic_in_used": "0",
+	    "traffic_out_used": "0"
+	}]
+    }
+}
+ + diff --git a/docs/api/client.zpy b/docs/api/client.zpy new file mode 100644 index 0000000..d04cd9c --- /dev/null +++ b/docs/api/client.zpy @@ -0,0 +1,230 @@ +# CVM Client API Documentation + +{TOC} + +## Overview + +The CVM Client API is a more or less RESTful API. That means it uses the standard HTTP 'verbs' like GET, POST, DELETE, etc. to execute certain commands. +Authentication takes place per request (there is no concept of 'sessions') through the use of custom HTTP headers. Each API token pair is linked to a particular +user, and can only be used for that user. A user can have multiple token pairs. Token pairs can be revoked at any time via the panel. + +## Authentication + +The CVM Client API expects two custom HTTP headers as authentication. + +API-Public-Token:: + This is the public part of your API token pair. It's used to identify who you are. + +API-Private-Token:: + This is the private part of your API token pair. It's used to verify your access. + +If no valid token pair is passed on with your request, the server will return a `401 Not Authorized` status code. + +If your token pair does not have access to the client API, the server will return a `403 Forbidden` status code. + +## Response format + +The API responses will always be in JSON format. If errors occurred, an `errors` key will be present containing an array of errors. If there is a response, a `response` key will +be present containing the response. + +@ Valid API call + + $ /api/client/vps/list + + > { + "response": { + "vpses": [{ + "id": "1", + "virtualization_type": "1", + "hostname": "test-vz.cryto.net", + "guaranteed_ram": "128", + "burstable_ram": "256", + "disk_space": "5000", + "cpu_count": "1", + "traffic_in_limit": "500000000000", + "traffic_out_limit": "500000000000", + "traffic_in_used": "912727849", + "traffic_out_used": "16923948" + }, { + "id": "2", + "virtualization_type": "1", + "hostname": "test2.cryto.net", + "guaranteed_ram": "512", + "burstable_ram": "768", + "disk_space": "40000", + "cpu_count": "240", + "traffic_in_limit": "500000000000", + "traffic_out_limit": "500000000000", + "traffic_in_used": "0", + "traffic_out_used": "0" + }] + } + } + +@ API call with invalid token pair + + $ /api/client/vps/list + + > { + "errors": ["No valid API token pair was specified."] + } + +## API Calls + +^ GET /api/client/vps/list + + This call will return a list of VPSes associated with the currently authenticated user. It takes no arguments. + + ### Keys in the response objects + + id:: + The numeric ID of this VPS. You will need this in further API calls. + + node:: + The host node that this VPS exists on. You will need this in further node-related API calls. + + virtualization_type:: + The virtualization platform used for this VPS. Right now the only supported value is `1` (OpenVZ). + + hostname:: + The configured hostname of the VPS. + + guaranteed_ram:: + The configured amount of guaranteed RAM, in __megabytes__. + + burstable_ram:: + The configured amount of burstable RAM, in __megabytes__. **This key may not be present if vSwap is used.** + + disk_space:: + The configured amount of disk space, in __megabytes__. + + cpu_count:: + The amount of configured CPUs (or rather, CPU units) that this VPS has access to. + + traffic_limit:: + The total traffic limit, in __bytes__. **This may not be present, depending on the method of traffic measuring. See the explanation below.** + + traffic_used:: + The total amount of traffic used, in __bytes__. **This may not be present, depending on the method of traffic measuring. See the explanation below.** + + traffic_in_limit:: + The total incoming traffic limit, in __bytes__. **This may not be present, depending on the method of traffic measuring. See the explanation below.** + + traffic_in_used:: + The total amount of incoming traffic used, in __bytes__. **This may not be present, depending on the method of traffic measuring. See the explanation below.** + + traffic_out_limit:: + The total outgoing traffic limit, in __bytes__. **This may not be present, depending on the method of traffic measuring. See the explanation below.** + + traffic_out_used:: + The total amount of outgoing traffic used, in __bytes__. **This may not be present, depending on the method of traffic measuring. See the explanation below.** + + ! If traffic accounting for the VPS is combined (incoming + outgoing), a `traffic_limit` and `traffic_used` key will be present. If traffic accounting for the VPS is split, the keys `traffic_in_limit`, `traffic_in_used`, `traffic_out_limit`, and `traffic_out_used` will be present. + + @ Valid call to /api/client/vps/list + + > { + "response": { + "vpses": [{ + "id": "1", + "node": "1", + "virtualization_type": "1", + "hostname": "test-vz.cryto.net", + "guaranteed_ram": "128", + "burstable_ram": "256", + "disk_space": "5000", + "cpu_count": "1", + "traffic_in_limit": "500000000000", + "traffic_out_limit": "500000000000", + "traffic_in_used": "912727849", + "traffic_out_used": "16923948" + }, { + "id": "2", + "node": "1", + "virtualization_type": "1", + "hostname": "test2.cryto.net", + "guaranteed_ram": "512", + "burstable_ram": "768", + "disk_space": "40000", + "cpu_count": "2", + "traffic_in_limit": "500000000000", + "traffic_out_limit": "500000000000", + "traffic_in_used": "0", + "traffic_out_used": "0" + }] + } + } + +^ GET /api/client/vps/**id**/status + + This returns the current status and metrics for the specified VPS. The response will be a single object. + + ### Arguments + + id:: + The ID of the VPS you wish to retrieve the status for. + + ### Keys in the response object + + traffic_used:: + The total amount of traffic used, in __bytes__. **This may not be present, depending on the method of traffic measuring.** + + traffic_in_used:: + The total amount of incoming traffic used, in __bytes__. **This may not be present, depending on the method of traffic measuring.** + + traffic_out_used:: + The total amount of outgoing traffic used, in __bytes__. **This may not be present, depending on the method of traffic measuring.** + + ram_used:: + The amount of RAM that is currently in use for the VPS, in __bytes__. + + disk_used:: + The amount of disk space currently used by the VPS, in __bytes__. + + status:: + The current status of the VPS. + + running:: + The VPS is active and booted. + + stopped:: + The VPS is active and shut down. + + suspended:: + The VPS is suspended. + + terminated:: + The VPS is terminated. + + unknown:: + The status of the VPS is unknown. This can happen when, for example, the host node can't be reached. + +^ POST /api/client/vps/**id**/start + + Starts the specified VPS. Returns either a `200` status code with an empty response if successful, a `500` status code with an error message if the VPS fails to start, or + a `503` status code with an error message if the host node is unreachable. + + ### Arguments + + id:: + The ID of the VPS you wish to start. + +^ POST /api/client/vps/**id**/stop + + Shuts down the specified VPS. Returns either a `200` status code with an empty response if successful, a `500` status code with an error message if the VPS fails to shut down, + or a `503` status code with an error message if the host node is unreachable. + + ### Arguments + + id:: + The ID of the VPS you wish to stop. + +^ POST /api/client/vps/**id**/restart + + Restarts the specified VPS. Returns either a `200` status code with an empty response if successful, a `500` status code with an error message if the VPS fails to restart, or + a `503` status code with an error message if the host node is unreachable. + + ### Arguments + + id:: + The ID of the VPS you wish to restart. diff --git a/frontend/authenticators/api/client.php b/frontend/authenticators/api/client.php new file mode 100644 index 0000000..be0cfda --- /dev/null +++ b/frontend/authenticators/api/client.php @@ -0,0 +1,57 @@ +CachedQuery("SELECT * FROM api_keys WHERE `PublicToken` = :Token", array(":Token" => $public_token))) +{ + $sApiKey = new ApiKey($result); + + if($sApiKey->VerifyToken($private_token)) + { + if($sApiKey->sKeyType >= API_CLIENT) + { + $sRouterAuthenticated = true; + } + else + { + $sResponseCode = 403; + $sResponse = array( + "errors" => array( + "The specified API token pair does not have access to this API." + ) + ); + } + } + else + { + $sResponseCode = 401; + $sResponse = array( + "errors" => array( + "No valid API token pair was specified." + ) + ); + } +} +else +{ + $sResponseCode = 401; + $sResponse = array( + "errors" => array( + "No valid API token pair was specified." + ) + ); +} diff --git a/frontend/classes/apikey.php b/frontend/classes/apikey.php new file mode 100644 index 0000000..9fe0860 --- /dev/null +++ b/frontend/classes/apikey.php @@ -0,0 +1,86 @@ + array( + 'PublicToken' => "PublicToken", + 'PrivateToken' => "PrivateToken", + 'Salt' => "Salt" + ), + 'numeric' => array( + 'UserId' => "UserId", + 'KeyType' => "KeyType" + ), + 'user' => array( + 'User' => "UserId" + ) + ); + + public function GenerateSalt() + { + $this->uSalt = random_string(10); + } + + public function GenerateHash() + { + if(!empty($this->uSalt)) + { + if(!empty($this->uToken)) + { + $this->uPrivateToken = $this->CreateHash($this->uToken); + } + else + { + throw new MissingDataException("ApiKey object is missing a token."); + } + } + else + { + throw new MissingDataException("ApiKey object is missing a salt."); + } + } + + public function CreateHash($input) + { + global $settings; + $hash = crypt($input, "$5\$rounds=50000\${$this->uSalt}{$settings['salt']}$"); + $parts = explode("$", $hash); + return $parts[4]; + } + + public function VerifyToken($token) + { + if($this->CreateHash($token) == $this->sPrivateToken) + { + return true; + } + else + { + return false; + } + } + + public function SetPrivateToken($token) + { + $this->uToken = $token; + $this->GenerateHash(); + } +} diff --git a/frontend/includes/include.constants.php b/frontend/includes/include.constants.php index afbe9c0..0583086 100644 --- a/frontend/includes/include.constants.php +++ b/frontend/includes/include.constants.php @@ -23,5 +23,9 @@ define("CVM_STATUS_STOPPED", 5 ); define("CVM_STATUS_SUSPENDED", 6 ); define("CVM_STATUS_TERMINATED", 7 ); +define("API_CLIENT", 1 ); +define("API_BILLING", 2 ); +define("API_ADMIN", 3 ); + define("REGEX_HOSTNAME", "/(([a-zA-Z0-9-]+\.)+)([a-zA-Z0-9-]+)/"); ?> diff --git a/frontend/includes/include.misc.php b/frontend/includes/include.misc.php index 1e920f8..72dbd3b 100644 --- a/frontend/includes/include.misc.php +++ b/frontend/includes/include.misc.php @@ -163,3 +163,66 @@ function format_size($input, $multiplier = 1024, $group = false, $decimal_places return $number . $unit; } } + +function status_code($code) +{ + $codes = array( + 100 => "Continue", + 101 => "Switching Protocols", + 200 => "OK", + 201 => "Created", + 202 => "Accepted", + 203 => "Non-Authoritative Information", + 204 => "No Content", + 205 => "Reset Content", + 206 => "Partial Content", + 300 => "Multiple Choices", + 301 => "Moved Permanently", + 302 => "Moved Temporarily", + 303 => "See Other", + 304 => "Not Modified", + 305 => "Use Proxy", + 400 => "Bad Request", + 401 => "Unauthorized", + 402 => "Payment Required", + 403 => "Forbidden", + 404 => "Not Found", + 405 => "Method Not Allowed", + 406 => "Not Acceptable", + 407 => "Proxy Authentication Required", + 408 => "Request Time-out", + 409 => "Conflict", + 410 => "Gone", + 411 => "Length Required", + 412 => "Precondition Failed", + 413 => "Request Entity Too Large", + 414 => "Request-URI Too Large", + 415 => "Unsupported Media Type", + 418 => "I'm a teapot", + 500 => "Internal Server Error", + 501 => "Not Implemented", + 502 => "Bad Gateway", + 503 => "Service Unavailable", + 504 => "Gateway Time-out", + 505 => "HTTP Version not supported", + ); + + if(array_key_exists($code, $codes)) + { + $text = $codes[$code]; + } + else + { + throw new Exception("The specified HTTP status code does not exist."); + } + + if(strpos(php_sapi_name(), "cgi") !== false) + { + header("Status: {$code} {$text}"); + } + else + { + $protocol = (isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0'); + header("{$protocol} {$code} {$text}"); + } +} diff --git a/frontend/modules/api/client/vps/list.php b/frontend/modules/api/client/vps/list.php new file mode 100644 index 0000000..b1d7f2d --- /dev/null +++ b/frontend/modules/api/client/vps/list.php @@ -0,0 +1,66 @@ +CachedQuery("SELECT * FROM containers WHERE `UserId` = :UserId", array(':UserId' => $sApiKey->sUser->sId))) +{ + $sVpses = array(); + + foreach($result->data as $row) + { + $sVps = new Vps($row); + + $sVpsData = array( + 'id' => $sVps->sId, + 'virtualization_type' => $sVps->sVirtualizationType, + 'hostname' => $sVps->sHostname, + 'guaranteed_ram' => $sVps->sGuaranteedRam, + 'burstable_ram' => $sVps->sBurstableRam, + 'disk_space' => $sVps->sDiskSpace, + 'cpu_count' => $sVps->sCpuCount, + 'node' => $sVps->sNodeId + ); + + if($sVps->sTotalTrafficLimit == 0) + { + /* Split traffic accounting */ + $sVpsData['traffic_in_limit'] = $sVps->sIncomingTrafficLimit; + $sVpsData['traffic_out_limit'] = $sVps->sOutgoingTrafficLimit; + $sVpsData['traffic_in_used'] = $sVps->sIncomingTrafficUsed; + $sVpsData['traffic_out_used'] = $sVps->sOutgoingTrafficUsed; + } + else + { + /* Combined traffic accounting */ + $sVpsData['traffic_limit'] = $sVps->sTotalTrafficLimit; + $sVpsData['traffic_used'] = $sVps->sIncomingTrafficUsed + $sVps->sOutgoingTrafficUsed; + } + + $sVpses[] = $sVpsData; + } + + $sResponse = array( + 'response' => array( + 'vpses' => $sVpses + ) + ); +} +else +{ + $sResponse = array( + 'response' => array( + 'vpses' => array() + ) + ); +} diff --git a/frontend/modules/error/api/access.php b/frontend/modules/error/api/access.php new file mode 100644 index 0000000..9c4c3af --- /dev/null +++ b/frontend/modules/error/api/access.php @@ -0,0 +1,2 @@ +GenerateSalt(); +$sKey->uPublicToken = random_string(32); +$new_token = random_string(32); +echo($new_token); +$sKey->SetPrivateToken($new_token); +$sKey->uUserId = 1; +$sKey->InsertIntoDatabase(); diff --git a/frontend/rewrite.php b/frontend/rewrite.php index b0097c7..7201b4d 100644 --- a/frontend/rewrite.php +++ b/frontend/rewrite.php @@ -39,6 +39,8 @@ else $sMainContents = ""; $sMainClass = ""; $sPageTitle = ""; +$sResponse = array(); +$sResponseCode = 200; /* Initialize some variables to ensure that they are available throughout the application. * Due to the way PHP variable scoping works (and the way CPHP works around this), variables @@ -205,6 +207,13 @@ try '_menu' => "admin", '_prefilled_node' => true ), + /* API - Client - List VPSes */ + '^/api/client/list' => array( + 'target' => "modules/api/client/vps/list.php", + 'authenticator' => "authenticators/api/client.php", + 'auth_error' => "modules/error/api/access.php", + '_raw' => true + ), '^/test/?$' => "modules/test.php" ) ); @@ -228,20 +237,23 @@ try )); } - if($router->uVariables['menu'] == "vps" && $router->uVariables['display_menu'] === true) - { - $sMainContents .= Templater::AdvancedParse("{$sTheme}/client/vps/main", $locale->strings, array( - 'error' => $sError, - 'contents' => $sPageContents, - 'id' => $sVps->sId - )); - } - elseif($router->uVariables['menu'] == "admin" && $router->uVariables['display_menu'] === true) + if(empty($router->uVariables['raw'])) { - $sMainContents .= Templater::AdvancedParse("{$sTheme}/admin/main", $locale->strings, array( - 'error' => $sError, - 'contents' => $sPageContents - )); + if($router->uVariables['menu'] == "vps" && $router->uVariables['display_menu'] === true) + { + $sMainContents .= Templater::AdvancedParse("{$sTheme}/client/vps/main", $locale->strings, array( + 'error' => $sError, + 'contents' => $sPageContents, + 'id' => $sVps->sId + )); + } + elseif($router->uVariables['menu'] == "admin" && $router->uVariables['display_menu'] === true) + { + $sMainContents .= Templater::AdvancedParse("{$sTheme}/admin/main", $locale->strings, array( + 'error' => $sError, + 'contents' => $sPageContents + )); + } } } catch (UnauthorizedException $e) @@ -254,12 +266,20 @@ catch (UnauthorizedException $e) )); } -$sTemplateParameters = array_merge($sTemplateParameters, array( - 'logged-in' => $sLoggedIn, - 'title' => $sPageTitle, - 'main' => $sMainContents, - 'menu-visible' => (isset($router->uVariables['menu']) && $router->sAuthenticated === true), - 'generation' => round(microtime(true) - $timing_start, 6) -)); +if(empty($router->uVariables['raw'])) +{ + $sTemplateParameters = array_merge($sTemplateParameters, array( + 'logged-in' => $sLoggedIn, + 'title' => $sPageTitle, + 'main' => $sMainContents, + 'menu-visible' => (isset($router->uVariables['menu']) && $router->sAuthenticated === true), + 'generation' => round(microtime(true) - $timing_start, 6) + )); -echo(Templater::AdvancedParse("{$sTheme}/shared/main", $locale->strings, $sTemplateParameters)); + echo(Templater::AdvancedParse("{$sTheme}/shared/main", $locale->strings, $sTemplateParameters)); +} +else +{ + status_code($sResponseCode); + echo(json_encode($sResponse)); +}