From 41608d7f3e1214a7c1be8c06fd653b0c72046388 Mon Sep 17 00:00:00 2001 From: Nasir Iqbal Date: Sat, 25 Nov 2017 10:57:36 +0500 Subject: [PATCH 01/12] bug fixed for cors --- source/Jacwright/RestServer/RestServer.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/Jacwright/RestServer/RestServer.php b/source/Jacwright/RestServer/RestServer.php index 9313e24..2084cbf 100755 --- a/source/Jacwright/RestServer/RestServer.php +++ b/source/Jacwright/RestServer/RestServer.php @@ -564,14 +564,14 @@ private function corsHeaders() { $allowedOrigin = (array)$this->allowedOrigin; // if no origin header is present then requested origin can be anything (i.e *) $currentOrigin = !empty($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : '*'; - if (in_array($currentOrigin, $allowedOrigin)) { + if (in_array($currentOrigin, $allowedOrigin) || in_array('*', $allowedOrigin)) { $allowedOrigin = array($currentOrigin); // array ; if there is a match then only one is enough } foreach($allowedOrigin as $allowed_origin) { // to support multiple origins header("Access-Control-Allow-Origin: $allowed_origin"); } header('Access-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS'); - header('Access-Control-Allow-Credential: true'); + header('Access-Control-Allow-Credentials: true'); header('Access-Control-Allow-Headers: X-Requested-With, content-type, access-control-allow-origin, access-control-allow-methods, access-control-allow-headers, Authorization'); } From e691c5e98d2f72f7860572491596bf304e8fa57c Mon Sep 17 00:00:00 2001 From: Nasir Iqbal Date: Sun, 26 Nov 2017 23:21:58 +0500 Subject: [PATCH 02/12] Direct file upload / stream upload support added --- example/TestController.php | 22 +++++++++++ source/Jacwright/RestServer/RestFormat.php | 4 ++ source/Jacwright/RestServer/RestServer.php | 43 +++++++++++++++++++++- 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/example/TestController.php b/example/TestController.php index f5d3b45..eefa02a 100644 --- a/example/TestController.php +++ b/example/TestController.php @@ -73,6 +73,28 @@ public function listUsers($query) return $users; // serializes object into JSON } + /** + * Upload a file + * + * @url PUT /files/$filename + */ + public function upload($filename, $data, $mime) + { + $storage_dir = sys_get_temp_dir(); + $allowedTypes = array('pdf' => 'application/pdf', 'html' => 'plain/html', 'wav' => 'audio/wav'); + if (in_array($mime, $allowedTypes)) { + if (!empty($data)) { + $file_path = $storage_dir . DIRECTORY_SEPARATOR . $filename; + file_put_contents($file_path, $data); + return $filename; + } else { + throw new RestException(411, "Empty file"); + } + } else { + throw new RestException(415, "Unsupported File Type"); + } + } + /** * Get Charts * diff --git a/source/Jacwright/RestServer/RestFormat.php b/source/Jacwright/RestServer/RestFormat.php index 860e325..d7b11f8 100644 --- a/source/Jacwright/RestServer/RestFormat.php +++ b/source/Jacwright/RestServer/RestFormat.php @@ -33,6 +33,8 @@ class RestFormat { const HTML = 'text/html'; const JSON = 'application/json'; const XML = 'application/xml'; + const FORM = 'application/x-www-form-urlencoded'; + const FILE = 'application/octet-stream'; /** @var array */ static public $formats = array( @@ -41,5 +43,7 @@ class RestFormat { 'html' => RestFormat::HTML, 'json' => RestFormat::JSON, 'xml' => RestFormat::XML, + 'form' => RestFormat::FORM, + 'file' => RestFormat::FILE ); } diff --git a/source/Jacwright/RestServer/RestServer.php b/source/Jacwright/RestServer/RestServer.php index 2084cbf..941843d 100755 --- a/source/Jacwright/RestServer/RestServer.php +++ b/source/Jacwright/RestServer/RestServer.php @@ -57,6 +57,12 @@ class RestServer { public $useCors = false; public $allowedOrigin = '*'; + /** + * Default format / mime type of request when there is none i.e no Content-Type present + */ + public $mimeDefault; + + protected $mime = null; // special parameter for mime type of posted data protected $data = null; // special parameter for post data protected $query = null; // special parameter for query string protected $map = array(); @@ -84,6 +90,10 @@ public function __construct($mode = 'debug') { $this->root = $dir; + // To retain the original behavior of RestServer + $this->mimeDefault = RestFormat::JSON; // from input + $this->format = RestFormat::JSON; // for output + // For backwards compatability, register HTTPAuthServer $this->setAuthHandler(new \Jacwright\RestServer\Auth\HTTPAuthServer); } @@ -124,6 +134,7 @@ public function handle() { } if ($this->method == 'PUT' || $this->method == 'POST' || $this->method == 'PATCH') { + $this->mime = $this->getMime(); $this->data = $this->getData(); } @@ -291,6 +302,10 @@ protected function findUrl() { if (!strstr($url, '$')) { if ($url == $this->url) { $params = array(); + if (isset($args['mime'])) { + $params += array_fill(0, $args['mime'] + 1, null); + $params[$args['mime']] = $this->mime; + } if (isset($args['data'])) { $params += array_fill(0, $args['data'] + 1, null); $params[$args['data']] = $this->data; @@ -310,6 +325,9 @@ protected function findUrl() { $params = array(); $paramMap = array(); + if (isset($args['mime'])) { + $params[$args['mime']] = $this->mime; + } if (isset($args['data'])) { $params[$args['data']] = $this->data; } @@ -466,9 +484,32 @@ public function getFormat() { return $format; } + public function getMime() { + $mime = $this->mimeDefault; + if (!empty($_SERVER["CONTENT_TYPE"])) { + $mime = strtolower(trim($_SERVER["CONTENT_TYPE"])); + } + return $mime; + } + public function getData() { $data = file_get_contents('php://input'); - $data = json_decode($data, $this->jsonAssoc); + + switch($this->mime) { + case RestFormat::FORM: + parse_str($data, $data); + break; + case RestFormat::JSON: + $data = json_decode($data, $this->jsonAssoc); + break; + case RestFormat::XML: + case RestFormat::HTML: + case RestFormat::TXT: + case RestFormat::FILE: + default: // And for all other formats / mime types + // No action needed + break; + } return $data; } From 3c70b5db35a3677220fa884eb84af03c0eb6ce87 Mon Sep 17 00:00:00 2001 From: Nasir Iqbal Date: Mon, 27 Nov 2017 12:31:48 +0500 Subject: [PATCH 03/12] File download support added --- example/TestController.php | 16 +++++++++++++++ source/Jacwright/RestServer/RestServer.php | 23 +++++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/example/TestController.php b/example/TestController.php index eefa02a..0e37e2c 100644 --- a/example/TestController.php +++ b/example/TestController.php @@ -95,6 +95,22 @@ public function upload($filename, $data, $mime) } } + /** + * Download a file + * + * @url GET /files/$filename + */ + public function download($filename) + { + $storage_dir = sys_get_temp_dir(); + $file_path = $storage_dir . DIRECTORY_SEPARATOR . $filename; + if (file_exists($file_path)) { + return SplFileInfo($file_path); + } else { + throw new RestException(404, "File not found"); + } + } + /** * Get Charts * diff --git a/source/Jacwright/RestServer/RestServer.php b/source/Jacwright/RestServer/RestServer.php index 941843d..e3a1c0d 100755 --- a/source/Jacwright/RestServer/RestServer.php +++ b/source/Jacwright/RestServer/RestServer.php @@ -163,7 +163,11 @@ public function handle() { $result = call_user_func_array(array($obj, $method), $params); if ($result !== null) { - $this->sendData($result); + if ($result instanceof SplFileInfo) { + $this->sendFile($result); + } else { + $this->sendData($result); + } } } } catch (RestException $e) { @@ -514,6 +518,23 @@ public function getData() { return $data; } + public function sendFile($file) { + $fInfo = new finfo(FILEINFO_MIME); + $fObject = $file->openFile(); + + header('Content-Description: File Transfer'); + header('Content-Type: ' . $fInfo->file($fObject->getRealPath())); + header('Content-Disposition: attachment; filename=' . $fObject->getFilename()); + header('Content-Transfer-Encoding: binary'); + header('Connection: Keep-Alive'); + header('Expires: 0'); + header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); + header('Pragma: public'); + header('Content-Length: ' . $fObject->getSize()); + + $fObject->rewind(); + echo $fObject->fread($fObject->getSize()); + } public function sendData($data) { header("Cache-Control: no-cache, must-revalidate"); From a772a5c9d66e63a39f5fd1214f32c0fa8c4fcaba Mon Sep 17 00:00:00 2001 From: Nasir Iqbal Date: Mon, 27 Nov 2017 16:42:31 +0500 Subject: [PATCH 04/12] bug fixes after testing --- source/Jacwright/RestServer/RestServer.php | 25 ++++++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/source/Jacwright/RestServer/RestServer.php b/source/Jacwright/RestServer/RestServer.php index e3a1c0d..f08e859 100755 --- a/source/Jacwright/RestServer/RestServer.php +++ b/source/Jacwright/RestServer/RestServer.php @@ -35,6 +35,8 @@ use ReflectionObject; use ReflectionMethod; use DOMDocument; +use SplFileInfo; +use finfo; /** * Description of RestServer @@ -508,7 +510,7 @@ public function getData() { break; case RestFormat::XML: case RestFormat::HTML: - case RestFormat::TXT: + case RestFormat::PLAIN: case RestFormat::FILE: default: // And for all other formats / mime types // No action needed @@ -519,21 +521,30 @@ public function getData() { } public function sendFile($file) { + $filename = $file->getFilename(); + $filepath = $file->getRealPath(); + $size = $file->getSize(); + $fInfo = new finfo(FILEINFO_MIME); - $fObject = $file->openFile(); + $content_type = $fInfo->file($filepath); + + $data = file_get_contents($filepath); + $filename_quoted = sprintf('"%s"', addcslashes($filename, '"\\')); header('Content-Description: File Transfer'); - header('Content-Type: ' . $fInfo->file($fObject->getRealPath())); - header('Content-Disposition: attachment; filename=' . $fObject->getFilename()); + header('Content-Type: ' . $content_type); + header('Content-Disposition: attachment; filename=' . $filename_quoted); header('Content-Transfer-Encoding: binary'); header('Connection: Keep-Alive'); header('Expires: 0'); header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); header('Pragma: public'); - header('Content-Length: ' . $fObject->getSize()); + header('Content-Length: ' . $size); + if ($this->useCors) { + $this->corsHeaders(); + } - $fObject->rewind(); - echo $fObject->fread($fObject->getSize()); + echo $data; } public function sendData($data) { From d4261c1e6472e192657a19313cd454da7f18fc49 Mon Sep 17 00:00:00 2001 From: Nasir Iqbal Date: Tue, 19 Dec 2017 12:04:44 +0500 Subject: [PATCH 05/12] Bearer token and Basic authentication support added for HTTPAuthServer, isAuthenticated added along with isAuthorized for independent authentication and authorization funtionality --- .../RestServer/Auth/HTTPAuthServer.php | 94 ++++++++++++++++++- source/Jacwright/RestServer/AuthServer.php | 27 +++++- source/Jacwright/RestServer/RestServer.php | 30 ++++-- 3 files changed, 136 insertions(+), 15 deletions(-) diff --git a/source/Jacwright/RestServer/Auth/HTTPAuthServer.php b/source/Jacwright/RestServer/Auth/HTTPAuthServer.php index 72bfc7c..f8d8cd0 100644 --- a/source/Jacwright/RestServer/Auth/HTTPAuthServer.php +++ b/source/Jacwright/RestServer/Auth/HTTPAuthServer.php @@ -8,16 +8,100 @@ public function __construct($realm = 'Rest Server') { $this->realm = $realm; } - public function isAuthorized($classObj) { + public function isAuthenticated($classObj) { + $auth_headers = $this->getAuthHeaders(); + + // Try to use bearer token as default + $auth_method = 'Bearer'; + $credentials = $this->getBearer($auth_headers); + + // TODO: add digest method + + // In case bearer token is not present try with Basic autentication + if (empty($credentials)) { + $auth_method = 'Basic'; + $credentials = $this->getBasic($auth_headers); + } + + if (method_exists($classObj, 'authenticate')) { + return $classObj->authenticate($credentials, $auth_method); + } + + return true; // original behavior + } + + public function unauthenticated($path) { + header("WWW-Authenticate: Basic realm=\"$this->realm\""); + throw new \Jacwright\RestServer\RestException(401, "Invalid credentials, access is denied to $path."); + } + + public function isAuthorized($classObj, $method) { if (method_exists($classObj, 'authorize')) { - return $classObj->authorize(); + return $classObj->authorize($method); } return true; } - public function unauthorized($classObj) { - header("WWW-Authenticate: Basic realm=\"$this->realm\""); - throw new \Jacwright\RestServer\RestException(401, "You are not authorized to access this resource."); + public function unauthorized($path) { + throw new \Jacwright\RestServer\RestException(403, "You are not authorized to access $path."); + } + + /** + * Get username and password from header + */ + protected function getBasic($headers) { + // mod_php + if (isset($_SERVER['PHP_AUTH_USER'])) { + return array($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']); + } else { // most other servers + if (!empty($headers)) { + list ($username, $password) = explode(':',base64_decode(substr($headers, 6))); + return array('username' => $username, 'password' => $password); + } + } + return array('username' => null, 'password' => null); + } + + /** + * Get access token from header + */ + protected function getBearer($headers) { + if (!empty($headers)) { + if (preg_match('/Bearer\s(\S+)/', $headers, $matches)) { + return $matches[1]; + } + } + return null; } + + /** + * Get username and password from header via Digest method + */ + protected function getDigest() { + if (false) { // TODO // currently not in function + return array('username' => null, 'password' => null); + } + return null; + } + + /** + * Get authorization header + */ + protected function getAuthHeaders() { + $headers = null; + if (isset($_SERVER['Authorization'])) { + $headers = trim($_SERVER["Authorization"]); + } else if (isset($_SERVER['HTTP_AUTHORIZATION'])) { //Nginx or fast CGI + $headers = trim($_SERVER["HTTP_AUTHORIZATION"]); + } else if (function_exists('apache_request_headers')) { + $requestHeaders = apache_request_headers(); + $requestHeaders = array_combine(array_map('ucwords', array_keys($requestHeaders)), array_values($requestHeaders)); + if (isset($requestHeaders['Authorization'])) { + $headers = trim($requestHeaders['Authorization']); + } + } + return $headers; + } + } diff --git a/source/Jacwright/RestServer/AuthServer.php b/source/Jacwright/RestServer/AuthServer.php index a34aa16..515ee09 100644 --- a/source/Jacwright/RestServer/AuthServer.php +++ b/source/Jacwright/RestServer/AuthServer.php @@ -2,15 +2,36 @@ namespace Jacwright\RestServer; interface AuthServer { + /** + * Indicates whether the requesting client is a recognized and authenticated party. + * + * @param object $classObj An instance of the controller for the path. + * + * @return bool True if authenticated, false if not. + */ + public function isAuthenticated($classObj); + + /** + * Handles the case where the client is not recognized party. + * This method must either return data or throw a RestException. + * + * @param string $path The requested path. + * + * @return mixed The response to send to the client + * + * @throws RestException + */ + public function unauthenticated($path); + /** * Indicates whether the client is authorized to access the resource. * - * @param string $path The requested path. * @param object $classObj An instance of the controller for the path. + * @param string $method The requested method. * * @return bool True if authorized, false if not. */ - public function isAuthorized($classObj); + public function isAuthorized($classObj, $method); /** * Handles the case where the client is not authorized. @@ -22,5 +43,5 @@ public function isAuthorized($classObj); * * @throws RestException */ - public function unauthorized($classObj); + public function unauthorized($path); } diff --git a/source/Jacwright/RestServer/RestServer.php b/source/Jacwright/RestServer/RestServer.php index 5b210cf..4a6da25 100755 --- a/source/Jacwright/RestServer/RestServer.php +++ b/source/Jacwright/RestServer/RestServer.php @@ -158,8 +158,11 @@ public function handle() { try { $this->initClass($obj); - if (!$noAuth && !$this->isAuthorized($obj)) { - $data = $this->unauthorized($obj); + if (!$noAuth && !$this->isAuthenticated($obj)) { + $data = $this->unauthenticated($this->url); + $this->sendData($data); + } else if (!$noAuth && !$this->isAuthorized($obj, $method)) { + $data = $this->unauthorized($this->url); $this->sendData($data); } else { $result = call_user_func_array(array($obj, $method), $params); @@ -255,17 +258,30 @@ protected function initClass($obj) { } } - protected function unauthorized($obj) { + public function unauthenticated($path) { + header("WWW-Authenticate: Basic realm=\"$this->realm\""); + throw new \Jacwright\RestServer\RestException(401, "Invalid credentials, access is denied to $path."); + } + + public function isAuthenticated($obj) { + if ($this->authHandler !== null) { + return $this->authHandler->isAuthenticated($obj); + } + + return true; + } + + protected function unauthorized($path) { if ($this->authHandler !== null) { - return $this->authHandler->unauthorized($obj); + return $this->authHandler->unauthorized($path); } - throw new RestException(401, "You are not authorized to access this resource."); + throw new RestException(403, "You are not authorized to access this resource."); } - protected function isAuthorized($obj) { + protected function isAuthorized($obj, $method) { if ($this->authHandler !== null) { - return $this->authHandler->isAuthorized($obj); + return $this->authHandler->isAuthorized($obj, $method); } return true; From 2b998c1d7a31d3f248474bf265cd0cc6c8212311 Mon Sep 17 00:00:00 2001 From: Nasir Iqbal Date: Tue, 19 Dec 2017 14:58:07 +0500 Subject: [PATCH 06/12] Example and documentation added for file upload and authentication --- README.md | 13 ++- example/TestAuthController.php | 149 +++++++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+), 4 deletions(-) create mode 100644 example/TestAuthController.php diff --git a/README.md b/README.md index b74eb9a..f612c28 100644 --- a/README.md +++ b/README.md @@ -156,13 +156,15 @@ DirectoryIndex index.php ``` -### Authentication +### Authentication and Authorization -Authentication is unique for each application. But tying your authentication mechanisms into RestServer is easy. By simply adding a method named `authorize` to your `Controller` all requests will call that method first. If `authorize()` returns false, the server will issue a 401 Unauthorized response. If `authorize()` returns true, the request continues on to call the correct controller action. All actions will run the authorization first unless you add `@noAuth` in the action's docs (I usually put it above the `@url` mappings). +Authentication is unique for each application. But tying your authentication mechanisms into RestServer is easy. By simply adding `authenticate` and `authorize` methods to your `Controller` all requests will call these methods first. If `authenticate()` or `authorize()` returns false, the server will issue a **401 Invalid credentials** or **403 Unauthorized** response respectively. If both `authenticate()` and `authorize()` returns true, the request continues on to call the correct controller action. All actions will run the authorization first unless you add `@noAuth` in the action's docs (I usually put it above the `@url` mappings). -Inside your authentication method you can use PHP's [`getallheaders`](http://php.net/manual/en/function.getallheaders.php) function or `$_COOKIE` depending on how you want to authorize your users. This is where you would load the user object from your database, and set it to `$this->user = getUserFromDatabase()` so that your action will have access to it later when it gets called. +You can select authentication and authorization functions as per your requirements you can implement only `autenticate` if you want to confirm client identity. Or you can implement both, then `authorize` can help to confirm if current client is allowed to access a certain method. -RestServer is meant to be a simple mechanism to map your application into a REST API. The rest of the details are up to you. +For more detail you can check example file `TestAuthControll.php`. Currently default authentication handler support **Basic** and **Bearer** authentication headers. and pass `[username, password]` or `bearer token` respectively to `authenticate()` method in your controller. In case you want to authenticate clients using some other method like cookies, You can do that inside `authenticate` method. You may replace default authentication handler by pass your own implementation of `AuthServer` interface to RestServer instance. + +RestServer is meant to be a simple mechanism to map your application into a REST API and pass requested data or headers. The rest of the details are up to you. ### Cross-origin resource sharing @@ -181,6 +183,9 @@ For security reasons, browsers restrict cross-origin HTTP or REST requests initi $server->allowedOrigin = '*'; ``` +### Working with files +Using Multipart with REST APIs is a bad idea and neither it is supported by `RestServer`. RestServer uses direct file upload approach like S3 services. you can upload one file per request without any additional form data. same is true for file download. for details please check `upload` and `download` methods in example. + ### Throwing and Handling Errors You may provide errors to your API users easily by throwing an excetion with the class `RestException`. Example: diff --git a/example/TestAuthController.php b/example/TestAuthController.php new file mode 100644 index 0000000..41a5bcb --- /dev/null +++ b/example/TestAuthController.php @@ -0,0 +1,149 @@ + array('email' => 'admin@domain.tld', 'password' => 'adminPass', 'role' => 'admin'), + 'user@domain.tld' => array('email' => 'user@domain.tld', 'password' => 'userPass', 'role' => 'user') + ); + + /** + * Security + */ + $private_key = __DIR__ . DIRECTORY_SEPARATOR . 'testkey'; + $public_key = __DIR__ . DIRECTORY_SEPARATOR . 'testkey.pub'; + $hash_type = 'RS256'; + + /** + * Logged in user + */ + $loggedUser = null; + + /** + * Check client credentials and return true if found valid, false otherwise + */ + public function authenticate($credentials, $auth_type) + { + switch ($auth_type) { + case 'Bearer': + $public_key = file_get_contents($this->public_key); + $token = JWT::decode($credentials, $public_key, array($this->hash_type)); + if ($token && !empty($token->username) && $this->listUser[$token->username]) { + $this->loggedUser = $this->listUser[$token->username]; + return true; + } + break; + + case 'Basic': + default: + $email = $credentials['username']; + if (isset($this->listUser[$email]) && $this->listUser[$email]['password'] == $credentials['password']) { + $this->loggedUser = $this->listUser[$email]; + return true; + } + break; + } + + return false; + } + + /** + * Check if current user is allowed to access a certain method + */ + public function authorize($method) + { + if ('admin' == $this->loggedUser['role']) { + return true; // admin can access everthing + + } else if ('user' == $this->loggedUser['role']) { + // user can access selected methods only + if (in_array($method, array('download'))) { + return true; + } + } + + return false; + } + + /** + * To get JWT token client can post his username and password to this method + * + * @url POST /login + * @noAuth + */ + public function login($username, $password) + { + // only if we have valid user + if (isset($this->listUser[$username]) && $this->listUser[$username] == $password) { + $token = array( + "iss" => 'My Website', + "iat" => time(), + "nbf" => time(), + "exp" => time() + (60 * 60 * 24 * 30 * 12 * 1), // valid for one year + "username" => $email + ); + + // return jwt token + $private_key = file_get_contents($this->private_key); + return JWT::encode($token, $private_key, $this->hash_type); + } + + throw new RestException(401, "Invalid username or password"); + } + + /** + * Upload a file + * + * @url PUT /files/$filename + */ + public function upload($filename, $data, $mime) + { + $storage_dir = sys_get_temp_dir(); + $allowedTypes = array('pdf' => 'application/pdf', 'html' => 'plain/html', 'wav' => 'audio/wav'); + if (in_array($mime, $allowedTypes)) { + if (!empty($data)) { + $file_path = $storage_dir . DIRECTORY_SEPARATOR . $filename; + file_put_contents($file_path, $data); + return $filename; + } else { + throw new RestException(411, "Empty file"); + } + } else { + throw new RestException(415, "Unsupported File Type"); + } + } + + /** + * Download a file + * + * @url GET /files/$filename + */ + public function download($filename) + { + $storage_dir = sys_get_temp_dir(); + $file_path = $storage_dir . DIRECTORY_SEPARATOR . $filename; + if (file_exists($file_path)) { + return SplFileInfo($file_path); + } else { + throw new RestException(404, "File not found"); + } + } + +} From 6dd8dbdeb3dcf11c89593cb0736dd099faf9d55b Mon Sep 17 00:00:00 2001 From: Nasir Iqbal Date: Tue, 19 Dec 2017 15:22:38 +0500 Subject: [PATCH 07/12] Example and documentation added for file upload and authentication-2 --- README.md | 21 ++++++++++++++++++--- example/TestAuthController.php | 5 ++++- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f612c28..f592578 100644 --- a/README.md +++ b/README.md @@ -160,9 +160,16 @@ DirectoryIndex index.php Authentication is unique for each application. But tying your authentication mechanisms into RestServer is easy. By simply adding `authenticate` and `authorize` methods to your `Controller` all requests will call these methods first. If `authenticate()` or `authorize()` returns false, the server will issue a **401 Invalid credentials** or **403 Unauthorized** response respectively. If both `authenticate()` and `authorize()` returns true, the request continues on to call the correct controller action. All actions will run the authorization first unless you add `@noAuth` in the action's docs (I usually put it above the `@url` mappings). -You can select authentication and authorization functions as per your requirements you can implement only `autenticate` if you want to confirm client identity. Or you can implement both, then `authorize` can help to confirm if current client is allowed to access a certain method. +You can select authentication and authorization methods as per your requirements you can implement only `autenticate` if you want to confirm client identity. Or you can implement both, then `authorize` can help to confirm if current client is allowed to access a certain action. For more detail you can check example file `TestAuthControll.php`. -For more detail you can check example file `TestAuthControll.php`. Currently default authentication handler support **Basic** and **Bearer** authentication headers. and pass `[username, password]` or `bearer token` respectively to `authenticate()` method in your controller. In case you want to authenticate clients using some other method like cookies, You can do that inside `authenticate` method. You may replace default authentication handler by pass your own implementation of `AuthServer` interface to RestServer instance. +Currently default authentication handler support **Basic** and **Bearer** headers based authentication. and pass `[username, password]` or `bearer token` respectively to `authenticate()` method in your controller. In case you want to authenticate clients using some other method like cookies, You can do that inside `authenticate` method. You may replace default authentication handler by passing your own implementation of `AuthServer` interface to RestServer instance. like + +```php + /** + * include following lines after $server = new RestServer($mode); + */ + $server->authHandler = new myAuthServer(); +``` RestServer is meant to be a simple mechanism to map your application into a REST API and pass requested data or headers. The rest of the details are up to you. @@ -184,7 +191,15 @@ For security reasons, browsers restrict cross-origin HTTP or REST requests initi ``` ### Working with files -Using Multipart with REST APIs is a bad idea and neither it is supported by `RestServer`. RestServer uses direct file upload approach like S3 services. you can upload one file per request without any additional form data. same is true for file download. for details please check `upload` and `download` methods in example. +Using Multipart with REST APIs is a bad idea and neither it is supported by `RestServer`. RestServer uses direct file upload approach like S3 services. you can upload one file per request without any additional form data. + +* **Upload:** +In file uploads action you may use two special parameters in method definition. `$data` and `$mime` first parameter will hold file content and the `$mime` parameter can provide details about file content type. + +* **Download:** +RestServer will start a file download in case a action return `SplFileInfo` object. + +For more details please check `upload` and `download` methods in example. ### Throwing and Handling Errors diff --git a/example/TestAuthController.php b/example/TestAuthController.php index 41a5bcb..1fe5997 100644 --- a/example/TestAuthController.php +++ b/example/TestAuthController.php @@ -88,8 +88,11 @@ public function authorize($method) * @url POST /login * @noAuth */ - public function login($username, $password) + public function login($data = array()) { + $username = isset($data['username']) ? $data['username'] : null; + $password = isset($data['password']) ? $data['password'] : null; + // only if we have valid user if (isset($this->listUser[$username]) && $this->listUser[$username] == $password) { $token = array( From 01f2aeeea646f151190c22f69c1521f31a43b6eb Mon Sep 17 00:00:00 2001 From: Nasir Iqbal Date: Tue, 19 Dec 2017 15:34:00 +0500 Subject: [PATCH 08/12] Example and documentation added for file upload and authentication-3 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f592578..4c5ed80 100644 --- a/README.md +++ b/README.md @@ -160,7 +160,7 @@ DirectoryIndex index.php Authentication is unique for each application. But tying your authentication mechanisms into RestServer is easy. By simply adding `authenticate` and `authorize` methods to your `Controller` all requests will call these methods first. If `authenticate()` or `authorize()` returns false, the server will issue a **401 Invalid credentials** or **403 Unauthorized** response respectively. If both `authenticate()` and `authorize()` returns true, the request continues on to call the correct controller action. All actions will run the authorization first unless you add `@noAuth` in the action's docs (I usually put it above the `@url` mappings). -You can select authentication and authorization methods as per your requirements you can implement only `autenticate` if you want to confirm client identity. Or you can implement both, then `authorize` can help to confirm if current client is allowed to access a certain action. For more detail you can check example file `TestAuthControll.php`. +You can select authentication and authorization methods as per your requirements you can implement only `autenticate` if you want to confirm client identity. Or you can implement both, then `authorize` can help to confirm if current client is allowed to access a certain action. For more details about authentication. and how to use `JWT` token as bearer header please check example file `TestAuthControll.php`. Currently default authentication handler support **Basic** and **Bearer** headers based authentication. and pass `[username, password]` or `bearer token` respectively to `authenticate()` method in your controller. In case you want to authenticate clients using some other method like cookies, You can do that inside `authenticate` method. You may replace default authentication handler by passing your own implementation of `AuthServer` interface to RestServer instance. like From 2b99afbd9702795fea78eeb0ce7797df6bd8b8bf Mon Sep 17 00:00:00 2001 From: Nasir Iqbal Date: Sat, 23 Dec 2017 18:24:13 +0500 Subject: [PATCH 09/12] minor fixes --- .../RestServer/Auth/HTTPAuthServer.php | 63 ++++++++++--------- source/Jacwright/RestServer/RestServer.php | 9 ++- 2 files changed, 40 insertions(+), 32 deletions(-) diff --git a/source/Jacwright/RestServer/Auth/HTTPAuthServer.php b/source/Jacwright/RestServer/Auth/HTTPAuthServer.php index f8d8cd0..8573de4 100644 --- a/source/Jacwright/RestServer/Auth/HTTPAuthServer.php +++ b/source/Jacwright/RestServer/Auth/HTTPAuthServer.php @@ -9,23 +9,23 @@ public function __construct($realm = 'Rest Server') { } public function isAuthenticated($classObj) { - $auth_headers = $this->getAuthHeaders(); + $auth_headers = $this->getAuthHeaders(); - // Try to use bearer token as default - $auth_method = 'Bearer'; - $credentials = $this->getBearer($auth_headers); + // Try to use bearer token as default + $auth_method = 'Bearer'; + $credentials = $this->getBearer($auth_headers); - // TODO: add digest method + // TODO: add digest method - // In case bearer token is not present try with Basic autentication - if (empty($credentials)) { - $auth_method = 'Basic'; - $credentials = $this->getBasic($auth_headers); - } + // In case bearer token is not present try with Basic autentication + if (empty($credentials)) { + $auth_method = 'Basic'; + $credentials = $this->getBasic($auth_headers); + } - if (method_exists($classObj, 'authenticate')) { - return $classObj->authenticate($credentials, $auth_method); - } + if (method_exists($classObj, 'authenticate')) { + return $classObj->authenticate($credentials, $auth_method); + } return true; // original behavior } @@ -51,17 +51,20 @@ public function unauthorized($path) { * Get username and password from header */ protected function getBasic($headers) { - // mod_php - if (isset($_SERVER['PHP_AUTH_USER'])) { - return array($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']); - } else { // most other servers - if (!empty($headers)) { - list ($username, $password) = explode(':',base64_decode(substr($headers, 6))); - return array('username' => $username, 'password' => $password); - } - } - return array('username' => null, 'password' => null); - } + // mod_php + if (isset($_SERVER['PHP_AUTH_USER'])) { + return array( + 'username' => $this->server_get('PHP_AUTH_USER'), + 'password' => $this->server_get('PHP_AUTH_PW') + ); + } else { // most other servers + if (!empty($headers)) { + list ($username, $password) = explode(':',base64_decode(substr($headers, 6))); + return array('username' => $username, 'password' => $password); + } + } + return array('username' => null, 'password' => null); + } /** * Get access token from header @@ -79,11 +82,11 @@ protected function getBearer($headers) { * Get username and password from header via Digest method */ protected function getDigest() { - if (false) { // TODO // currently not in function - return array('username' => null, 'password' => null); - } - return null; - } + if (false) { // TODO // currently not functional + return array('username' => null, 'password' => null); + } + return null; + } /** * Get authorization header @@ -102,6 +105,6 @@ protected function getAuthHeaders() { } } return $headers; - } + } } diff --git a/source/Jacwright/RestServer/RestServer.php b/source/Jacwright/RestServer/RestServer.php index 4a6da25..68bc358 100755 --- a/source/Jacwright/RestServer/RestServer.php +++ b/source/Jacwright/RestServer/RestServer.php @@ -141,7 +141,8 @@ public function handle() { } //preflight requests response - if ($this->method == 'OPTIONS' && getallheaders()->Access-Control-Request-Headers) { + $headers = (object)getallheaders(); + if ($this->method == 'OPTIONS' && $headers->Access-Control-Request-Headers) { $this->sendData($this->options()); } @@ -259,7 +260,11 @@ protected function initClass($obj) { } public function unauthenticated($path) { - header("WWW-Authenticate: Basic realm=\"$this->realm\""); + if ($this->authHandler !== null) { + return $this->authHandler->unauthenticated($path); + } + + header("WWW-Authenticate: Basic realm=\"API Server\""); throw new \Jacwright\RestServer\RestException(401, "Invalid credentials, access is denied to $path."); } From 91b241668cad01ac5986201d359a0c3a1678e01a Mon Sep 17 00:00:00 2001 From: Nasir Iqbal Date: Mon, 15 Jan 2018 11:04:39 +0500 Subject: [PATCH 10/12] issue fixed in sample code --- example/TestAuthController.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/example/TestAuthController.php b/example/TestAuthController.php index 1fe5997..7f1ca82 100644 --- a/example/TestAuthController.php +++ b/example/TestAuthController.php @@ -19,7 +19,7 @@ class TestController /** * Mocking up user table */ - $listUser = array( + private $listUser = array( 'admin@domain.tld' => array('email' => 'admin@domain.tld', 'password' => 'adminPass', 'role' => 'admin'), 'user@domain.tld' => array('email' => 'user@domain.tld', 'password' => 'userPass', 'role' => 'user') ); @@ -27,14 +27,14 @@ class TestController /** * Security */ - $private_key = __DIR__ . DIRECTORY_SEPARATOR . 'testkey'; - $public_key = __DIR__ . DIRECTORY_SEPARATOR . 'testkey.pub'; - $hash_type = 'RS256'; + public $private_key = __DIR__ . DIRECTORY_SEPARATOR . 'testkey'; + public $public_key = __DIR__ . DIRECTORY_SEPARATOR . 'testkey.pub'; + public $hash_type = 'RS256'; /** * Logged in user */ - $loggedUser = null; + private $loggedUser = null; /** * Check client credentials and return true if found valid, false otherwise @@ -100,7 +100,7 @@ public function login($data = array()) "iat" => time(), "nbf" => time(), "exp" => time() + (60 * 60 * 24 * 30 * 12 * 1), // valid for one year - "username" => $email + "username" => $this->listUser[$username]['email']; ); // return jwt token From d2c93f57892469f85ebb92eb7b52c60bf0749b3e Mon Sep 17 00:00:00 2001 From: Nasir Iqbal Date: Mon, 22 Oct 2018 15:29:33 +0500 Subject: [PATCH 11/12] cors issue fixed with filename i.e content-disposition header --- source/Jacwright/RestServer/RestServer.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/source/Jacwright/RestServer/RestServer.php b/source/Jacwright/RestServer/RestServer.php index 68bc358..97a457a 100755 --- a/source/Jacwright/RestServer/RestServer.php +++ b/source/Jacwright/RestServer/RestServer.php @@ -668,6 +668,8 @@ private function corsHeaders() { header('Access-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS'); header('Access-Control-Allow-Credentials: true'); header('Access-Control-Allow-Headers: X-Requested-With, content-type, access-control-allow-origin, access-control-allow-methods, access-control-allow-headers, Authorization'); + header('Access-Control-Expose-Headers: Content-Type, Content-Length, Content-Disposition'); + } private $codes = array( From 0a09d5b1537b205d40a42312561367889eb761a4 Mon Sep 17 00:00:00 2001 From: Nasir Iqbal Date: Mon, 18 May 2020 17:02:54 +0500 Subject: [PATCH 12/12] base64 upload support added, see reader.readAsDataURL in javascript --- source/Jacwright/RestServer/RestFormat.php | 2 ++ source/Jacwright/RestServer/RestServer.php | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/source/Jacwright/RestServer/RestFormat.php b/source/Jacwright/RestServer/RestFormat.php index d7b11f8..3e8246e 100644 --- a/source/Jacwright/RestServer/RestFormat.php +++ b/source/Jacwright/RestServer/RestFormat.php @@ -34,6 +34,7 @@ class RestFormat { const JSON = 'application/json'; const XML = 'application/xml'; const FORM = 'application/x-www-form-urlencoded'; + const BASE64= 'base64'; const FILE = 'application/octet-stream'; /** @var array */ @@ -44,6 +45,7 @@ class RestFormat { 'json' => RestFormat::JSON, 'xml' => RestFormat::XML, 'form' => RestFormat::FORM, + 'base64'=> RestFormat::BASE64, 'file' => RestFormat::FILE ); } diff --git a/source/Jacwright/RestServer/RestServer.php b/source/Jacwright/RestServer/RestServer.php index 97a457a..79d926e 100755 --- a/source/Jacwright/RestServer/RestServer.php +++ b/source/Jacwright/RestServer/RestServer.php @@ -529,6 +529,10 @@ public function getData() { case RestFormat::JSON: $data = json_decode($data, $this->jsonAssoc); break; + case RestFormat::BASE64: + $this->mime = mime_content_type($data); + $data = file_get_contents($data); + break; case RestFormat::XML: case RestFormat::HTML: case RestFormat::PLAIN: