From 830b5ccfb411b9f38366b34d3f3e3a666ba2b65e Mon Sep 17 00:00:00 2001 From: Lewis Cowles Date: Sun, 11 Dec 2016 21:03:14 +0000 Subject: [PATCH 1/4] Added Basic Rate-Limit --- controllers/TestController.php | 41 +++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/controllers/TestController.php b/controllers/TestController.php index 758a03a..c06bb69 100644 --- a/controllers/TestController.php +++ b/controllers/TestController.php @@ -6,6 +6,9 @@ use WP_REST_Response; class TestController extends WP_REST_Controller { + const RATE_FIELD = 'cd2_rest_rate'; + const RATE_TIME = 60; // One Hour + const RATE_LIMIT = 5; public function __construct() { $this->namespace = 'test/v1'; @@ -27,11 +30,47 @@ public function register_routes() { } public function index(WP_REST_Request $request) { + $calls = $this->user_rate_check(); + if(!($calls > 0)) { + return new WP_REST_Response("API Limit reached {$calls} left", 429); + } $data = ['now'=> date('Y-m-d H:i:s')]; return new WP_REST_Response( $data, 200); } public function index_permissions_check(WP_REST_Request $request) { - return true; + // Really basic example for permissions checking + // (https://codex.wordpress.org/Roles_and_Capabilities#read) + return $this->user_permission_check('read'); + } + + public function user_permission_check($cap='admin') { + return current_user_can($cap); + } + + public function user_rate_check() { + if($this->user_permission_check('read')) { + $user_id = get_current_user_id(); + $transient_name = self::RATE_FIELD."_{$user_id}"; + $left = get_transient($transient_name); + if(!is_array($left)) { + set_transient($transient_name, [ + 'limit'=>self::RATE_LIMIT, + 'time'=>time() + ], self::RATE_TIME); + $left = [ + 'limit'=>self::RATE_LIMIT, + 'time'=>time() + ]; + } + $ret = intval($left['limit']); + if($ret > 0) { + $left['limit']--; + } + $time = max(0, (self::RATE_TIME-(time()-$left['time']))); + set_transient($transient_name, $left, $time); + return $ret; + } + return false; } } From 0c44d25abfaa05519abab2b8b0ae7fbbffadece2 Mon Sep 17 00:00:00 2001 From: Lewis Cowles Date: Mon, 12 Dec 2016 03:51:11 +0000 Subject: [PATCH 2/4] AutoLoader + DI * Created `CreditDataInterface` * Created `CreditHandlerInterface` * Created `SystemUserInterface` * Created `TTLStorageInterface` * Created `RestEndpointCreditData` implementing `CreditDataInterface` * Created `RestEndpointTimedRateLimit` implementing `CreditHandlerInterface` * Created `WPUser` implementing `SystemUserInterface` * Created `WPTransientStorage` implementing `TTLStorageInterface` * Added Rudimentary auto-loader to (entrypoint) --- index.php | 22 +++++++- vendor/lewiscowles/CreditDataInterface.php | 10 ++++ vendor/lewiscowles/CreditHandlerInterface.php | 12 ++++ .../HTTP/REST/RestEndpointCreditData.php | 31 +++++++++++ .../HTTP/REST/RestEndpointTimedRateLimit.php | 55 +++++++++++++++++++ .../lewiscowles/auth/SystemUserInterface.php | 11 ++++ .../storage/TTLStorageInterface.php | 8 +++ vendor/lewiscowles/wordpress/auth/WPUser.php | 40 ++++++++++++++ .../wordpress/storage/WPTransientStorage.php | 26 +++++++++ 9 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 vendor/lewiscowles/CreditDataInterface.php create mode 100644 vendor/lewiscowles/CreditHandlerInterface.php create mode 100644 vendor/lewiscowles/HTTP/REST/RestEndpointCreditData.php create mode 100644 vendor/lewiscowles/HTTP/REST/RestEndpointTimedRateLimit.php create mode 100644 vendor/lewiscowles/auth/SystemUserInterface.php create mode 100644 vendor/lewiscowles/storage/TTLStorageInterface.php create mode 100644 vendor/lewiscowles/wordpress/auth/WPUser.php create mode 100644 vendor/lewiscowles/wordpress/storage/WPTransientStorage.php diff --git a/index.php b/index.php index db2119a..676c31f 100644 --- a/index.php +++ b/index.php @@ -8,13 +8,31 @@ Author URI: https://www.codesign2.co.uk */ -add_action('rest_api_init', function(){ +namespace lewiscowles; + +\add_action('rest_api_init', function() { + error_reporting(E_ALL); + ini_set('display_errors', true); foreach([ 'Test' ] as $endpoint) { require_once( __DIR__."/controllers/{$endpoint}Controller.php"); - $controller_class = "{$endpoint}Controller"; + $controller_class = __NAMESPACE__."\\{$endpoint}Controller"; $controller = new $controller_class(); $controller->register_routes(); } }); + +\add_action('wp_ajax_apinonce', function() { + die(\wp_create_nonce( 'wp_rest' )); +}); + +\spl_autoload_register( function($classname) { + $file = sprintf( + '%s/vendor/%s.php', + __DIR__, str_replace('\\', DIRECTORY_SEPARATOR, $classname) + ); + if(file_exists($file)) { + require $file; + } +}); diff --git a/vendor/lewiscowles/CreditDataInterface.php b/vendor/lewiscowles/CreditDataInterface.php new file mode 100644 index 0000000..391ceb5 --- /dev/null +++ b/vendor/lewiscowles/CreditDataInterface.php @@ -0,0 +1,10 @@ +Created = $time; + $this->Balance = $balance; + } + + public function getCreated() : int { + return $this->Created; + } + + public function getBalance() : int { + return $this->Balance; + } + + public function deductBalance(int $value) { + $this->Balance -= abs($value); + } + + public function topUpBalance(int $value) { + $this->Balance += abs($value); + } +} diff --git a/vendor/lewiscowles/HTTP/REST/RestEndpointTimedRateLimit.php b/vendor/lewiscowles/HTTP/REST/RestEndpointTimedRateLimit.php new file mode 100644 index 0000000..4cac5d6 --- /dev/null +++ b/vendor/lewiscowles/HTTP/REST/RestEndpointTimedRateLimit.php @@ -0,0 +1,55 @@ +HashId = $hash_id; + $this->Time = $time; + $this->Credits = $credits; + $this->Storage = $ttsi; + $this->Ttl = $ttl; + } + + public function getCreditData() : CreditDataInterface { + return $this->Storage->get($this->HashId, new RestEndpointCreditData( + $this->Time, $this->Credits + )); + } + + public function checkCreditData(CreditDataInterface $data) : bool { + return ($data->getBalance() > 0); + } + + public function setCreditData(CreditDataInterface $data) { + $this->Storage->set($this->HashId, $data, $this->getRefreshTtl($data)); + } + + public function formatCreditReload(CreditDataInterface $data) : string { + $refresh = $this->getRefreshTtl($data); + $out = date('s', $refresh).' seconds'; + if($refresh > 60) { + $out = date('i', $refresh).' minutes, ' . $out; + } + if($refresh > 3600) { + $out = intval($refresh / 3600).' hours, '.$out; + } + return $out; + } + + protected function getRefreshTtl(CreditDataInterface $data) { + return ($this->Ttl - ($this->Time - $data->getCreated())); + } +} diff --git a/vendor/lewiscowles/auth/SystemUserInterface.php b/vendor/lewiscowles/auth/SystemUserInterface.php new file mode 100644 index 0000000..d5792d7 --- /dev/null +++ b/vendor/lewiscowles/auth/SystemUserInterface.php @@ -0,0 +1,11 @@ +Pk = \get_current_user_id(); + } + + public function hasCapability(string $capability) : bool { + return \current_user_can($capability); + } + + public function isLoggedIn() : bool { + return \is_user_logged_in(); + } + + public function getPK() : string { + return $this->Pk; + } + + public function login(string $username, string $password) { + \wp_authenticate($username, $password); + } + + public function logout() { + \wp_logout(); + } +} diff --git a/vendor/lewiscowles/wordpress/storage/WPTransientStorage.php b/vendor/lewiscowles/wordpress/storage/WPTransientStorage.php new file mode 100644 index 0000000..969a63d --- /dev/null +++ b/vendor/lewiscowles/wordpress/storage/WPTransientStorage.php @@ -0,0 +1,26 @@ + Date: Mon, 12 Dec 2016 03:53:43 +0000 Subject: [PATCH 3/4] Re-factored Controller * Pulled in dependencies to replace hard-coded PoC functionality * Added `namespace` and `use` functionality * Manual Testing (should be automated, but we needed DI to be able to have those nice-things) --- controllers/TestController.php | 90 ++++++++++++++++++---------------- 1 file changed, 48 insertions(+), 42 deletions(-) diff --git a/controllers/TestController.php b/controllers/TestController.php index c06bb69..1fd1ab5 100644 --- a/controllers/TestController.php +++ b/controllers/TestController.php @@ -1,26 +1,48 @@ namespace = 'test/v1'; + $this->auth = new WPUser(); + $this->limiter = new RestEndpointTimedRateLimit( + self::RATE_FIELD."_".$this->auth->getPk(), + time(), + self::RATE_LIMIT, + new WPTransientStorage(), + self::RATE_TIME + ); } public function register_routes() { - register_rest_route( + \register_rest_route( "{$this->namespace}", "/date", [ [ - 'methods' => WP_REST_Server::READABLE, + 'methods' => \WP_REST_Server::READABLE, 'callback' => [$this, 'index'], 'permission_callback' => [$this, 'index_permissions_check'], 'args' => [], @@ -29,48 +51,32 @@ public function register_routes() { ); } - public function index(WP_REST_Request $request) { - $calls = $this->user_rate_check(); - if(!($calls > 0)) { - return new WP_REST_Response("API Limit reached {$calls} left", 429); + public function index(\WP_REST_Request $request) { + $creditData = $this->limiter->getCreditData(); + $refresh = $this->limiter->formatCreditReload($creditData); + if(!$this->limiter->checkCreditData($creditData)) { + $left = $creditData->getBalance(); + $limit = self::RATE_LIMIT; + return new \WP_REST_Response( + "API Limit reached {$left}/{$limit} calls refresh in {$refresh}", + 429 + ); } - $data = ['now'=> date('Y-m-d H:i:s')]; - return new WP_REST_Response( $data, 200); + + $data = [ + 'now'=> date('Y-m-d H:i:s'), + 'time_to_refresh' => $refresh + ]; + + $creditData->deductBalance(1); + $this->limiter->setCreditData($creditData); + + return new \WP_REST_Response( $data, 200); } public function index_permissions_check(WP_REST_Request $request) { // Really basic example for permissions checking // (https://codex.wordpress.org/Roles_and_Capabilities#read) - return $this->user_permission_check('read'); - } - - public function user_permission_check($cap='admin') { - return current_user_can($cap); - } - - public function user_rate_check() { - if($this->user_permission_check('read')) { - $user_id = get_current_user_id(); - $transient_name = self::RATE_FIELD."_{$user_id}"; - $left = get_transient($transient_name); - if(!is_array($left)) { - set_transient($transient_name, [ - 'limit'=>self::RATE_LIMIT, - 'time'=>time() - ], self::RATE_TIME); - $left = [ - 'limit'=>self::RATE_LIMIT, - 'time'=>time() - ]; - } - $ret = intval($left['limit']); - if($ret > 0) { - $left['limit']--; - } - $time = max(0, (self::RATE_TIME-(time()-$left['time']))); - set_transient($transient_name, $left, $time); - return $ret; - } - return false; + return $this->auth->hasCapability('read'); } } From 324b6519a0eb54026a573d5f0bc4931c13ddd41d Mon Sep 17 00:00:00 2001 From: Lewis Cowles Date: Mon, 12 Dec 2016 15:23:17 +0000 Subject: [PATCH 4/4] Updated Swagger Definition Updated Swagger definition to cover current and planned works * 421 and 403 error codes * Decision made to have 421 mirror 403 error codes --- swagger/description.yaml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/swagger/description.yaml b/swagger/description.yaml index 47b8a1e..e7a2017 100644 --- a/swagger/description.yaml +++ b/swagger/description.yaml @@ -19,6 +19,14 @@ paths: description: "Successful response" schema: $ref: "#/definitions/inline_response_200" + 403: + description: "Unauthorised request rejection" + schema: + $ref: "#/definitions/inline_response_err" + 421: + description: "Too many requests rejection" + schema: + $ref: "#/definitions/inline_response_err" definitions: inline_response_200: properties: @@ -26,3 +34,18 @@ definitions: type: "string" format: "datetime" description: "Curent server datetime value" + inline_response_err: + properties: + code: + type: string + description: "Error Type Code" + message: + type: string + description: "Error Message" + data: + type: object + properties: + status: + type: integer + description: "HTTP Error Code" +