commit b9e3840ebf825b890796ae49113dc088a69c3a65 Author: a-sansara Date: Tue Mar 14 23:12:58 2017 +0100 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d1502b0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +vendor/ +composer.lock diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..79ad341 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016-2017 meta-tech.academy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e4bb3e1 --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ + +# MetaTech PwsClient + +a php webservice client managing [ PwsAuth ](https://github.com/meta-tech/pws-auth) protocol + +### Requirements + +PHP >= 5.4 + +### Install + +The package can be installed using [ Composer ](https://getcomposer.org/). (not yet) +``` +composer require meta-tech/pws-client +``` + +Or add the package to your `composer.json`. + +``` +"require": { + "meta-tech/pws-client" : "1.0" +} +``` + +### Usage +When instantiating, PwsClient automatically checks if it has been authenticated. Otherwise, or if the session has expired, +the client will perform the authentication. Then, you can initiate get or post call + +```php +check(); +// $client->call(); + +// get example +$response = $client->get('/ws/person/222'); +if ($response->done) { + // do stuff + +} +// post example +$client->post('/ws/person/222/update', [ 'firstname' => 'toto']); +if ($response->done) { + // do stuff +} + +$client->logout(); + +``` + +### Config + +```yaml +# pwsclient config + +# 0 : disable, 1 : verboose, 2 : most verboose +debug : 1 +protocol : https:// +hostname : pwsserver.docker +# file storing the server 's session id - must be out of DocumentRoot and read/writable by server +store : wsess +login : test +password : test +key : test +# 0 : display cli, 1 : display html +html_output : 0 +# http authentication +http : + user : + password : +# server uris for authentication +uri : + auth : /ws/auth + logout : /ws/logout + check : /ws/isauth + +``` + +### Server Response + +PwsClient intend to receiv any JsonResponse, the structure of the response is free. +However, meta-tech always return this simple Json Structure : +{ done : boolean, msg : 'string contextual msg', data : whatever } + + +### License + +The project is released under the MIT license, see the LICENSE file. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..47d4360 --- /dev/null +++ b/composer.json @@ -0,0 +1,29 @@ +{ + "name" : "meta-tech/pws-client", + "type" : "library", + "homepage" : "https://github.com/meta-tech/pws-client", + "description" : "PwsClient is a php web service client managing PwsAuth protocol", + "license" : "MIT", + "authors" : [ + { + "name" : "a-Sansara", + "homepage" : "https://github.com/a-sansara/" + } + ], + "keywords" : ["Client", "PwsAuth", "WebService", "Http"], + "autoload" : { + "psr-4" : { + "" : "src/" + } + }, + "require" : { + "symfony/yaml" : "^3.2", + "meta-tech/pws-auth": "@dev" + }, + "repositories" : [ + { + "type": "git", + "url": "https://github.com/meta-tech/pws-auth.git" + } + ] +} diff --git a/config/pwsauth.yml.dist b/config/pwsauth.yml.dist new file mode 100644 index 0000000..a33e099 --- /dev/null +++ b/config/pwsauth.yml.dist @@ -0,0 +1,24 @@ +# pwsauth config + +type : PwsAuth2 + +header : + auth : Pws-Authorization + ident : Pws-Ident + +salt : + common : jK5#p9Mh5.Zv} + # used for generating user specific salt + user.index : 10 + user.length : 12 + +hash : + sep : / + algo : sha256 + # effective token length size. out of bound data is simply noise + length : 52 + # session index (or obfuscate length) + session.index : 58 + # ending noise data length) + noise.length : 12 + diff --git a/config/pwsclient.yml.dist b/config/pwsclient.yml.dist new file mode 100644 index 0000000..8a44b22 --- /dev/null +++ b/config/pwsclient.yml.dist @@ -0,0 +1,22 @@ +# pwsclient config + +# 0 : disable, 1 : verboose, 2 : most verboose +debug : 1 +protocol : https:// +hostname : pwsserver.docker +# file storing the server 's session id - must be out of DocumentRoot and read/writable by server +store : wsess +login : test +password : test +key : test +# 0 : display cli, 1 : display html +html_output : 0 +# http authentication +http : + user : + password : +# server uris for authentication +uri : + auth : /ws/auth + logout : /ws/logout + check : /ws/isauth diff --git a/src/MetaTech/Output/Formatter.php b/src/MetaTech/Output/Formatter.php new file mode 100644 index 0000000..74ba611 --- /dev/null +++ b/src/MetaTech/Output/Formatter.php @@ -0,0 +1,136 @@ +type = $type; + $this->embedStyle = false; + } + + /*! + * desc + * + * @method _initCall + * @private + * @param str $uri the web service uri + * @return $curl instance + */ + private function format($value, $color=null) + { + if ($this->type == self::TYPE_HTML) { + $tag = $color === false ? '' : (is_null($color) ? '' : ''); + $content = $tag . $value; + } + else { + $tag = $color === false ? '' : (is_null($color) ? "\033[0m" : "\033[1;3".$color."m" ); + $content = $tag . $value; + } + return $content; + } + + /*! + * @method getHtmlClass + * @private + * @param int $idColor + * @return str + */ + public function embedStyleIfNeeded() + { + $style = ''; + if (!$this->embedStyle) { + $this->embedStyle = true; + $style .= ''; + } + return $style; + } + + /*! + * @method writeTags + * @private + * @param [] $tags + */ + public function writeTags($tags) + { + $content = ''; + foreach($tags as $tag) { + if (!empty($tag)) { + if (is_array($tag)) { + $content .= $this->format($tag[0], count($tag) > 1 ? $tag[1] : null); + } + elseif (is_string($tag)) { + $content .= $this->format($tag); + } + } + } + $this->write($content); + } + + /*! + * @method write + * @private + * @param str $value + * @param bool $newline + * @return str + */ + private function write($value, $newline=true) + { + $content = $value . ($newline ? self::LF : ''); + if ($this->type == self::TYPE_HTML) { + $content = $this->embedStyleIfNeeded() . '
'.$content.'
'; + } + echo $content; + } + +} diff --git a/src/MetaTech/Ws/Client.php b/src/MetaTech/Ws/Client.php new file mode 100644 index 0000000..6e2087b --- /dev/null +++ b/src/MetaTech/Ws/Client.php @@ -0,0 +1,335 @@ +config['html_output'] ? Formatter::TYPE_HTML : Formatter::TYPE_CLI; + $this->formatter = new Formatter($typeFormatter); + $this->config = $config; + $this->authenticator = $authenticator; + + if ($this->config['debug'] && $this->config['debug'] == self::MOST_VERBOOSE) { + $config['password'] = '--hidden--'; + $config['key'] = substr($config['key'], 0, 3) . '...--hidden--'; + if (!empty($config['http']['password'])) { + $config['http']['password'] = '--hidden--'; + } + $this->formatter->writeTags([ + [date('H:i:s') , '3'], + [' [' . __class__ . ']' , '1'], + [' debug mode verboose, view config :', '2'], + Formatter::LF, + [var_export($config, true), null] + ]); + } + if (!file_exists($config['store'])) { + $this->_persist('#'); + } + $resp = $this->check(); + if (is_null($resp) || !$resp->done) { + $this->auth(); + } + } + + /*! + * call the authenticate web service + * + * @method auth + * @public + * @return object + */ + public function auth() + { + $header = $this->_buildHeader(); + $data = Tool::compact($this->config, ['login', 'password']); + $data = $this->_send($this->config['uri']['auth'], $data, "POST", $header); + $resp = $data['response']; + if ($resp!=null && $resp->done) { + $this->_persist($resp->data->sid); + } + unset($resp->data->sid); + return $resp; + } + + /*! + * call the logout web service + * + * @method logout + * @public + * @return + */ + public function logout() + { + return $this->get($this->config['uri']['logout']); + } + + /*! + * call the check auth web service + * + * @method check + * @public + * @return + */ + public function check() + { + return $this->get($this->config['uri']['check']); + } + + /*! + * call a web service denotes by $uri + * + * @method get + * @public + * @param str $uri the web service uri + * @param [] $data the post data + * @return object + */ + public function get($uri) + { + $header = $this->_buildHeader($this->_getToken()); + $resp = $this->_send($uri, null, 'GET', $header); + return $resp['response']; + } + + /*! + * call a web service denotes by $uri with $data post + * + * @method post + * @public + * @param str $uri the web service uri + * @param [] $data the post data + * @return object + */ + public function post($uri, $data=array(), $trace=false) + { + $header = $this->_buildHeader($this->_getToken()); + $resp = $this->_send($uri, $data, 'POST', $header); + return $resp['response']; + } + + /*! + * build header for authentication. if token is missing or null, the header is + * specific to a login action, overwise the header is specific to a loading session + * + * @method _buildHeader + * @private + * @param str $login + * @param str $key + * @param str $sessid + * @retval [str] + */ + private function _buildHeader($sessid=null) + { + $header = $this->authenticator->generateHeader($this->config['login'], $this->config['key'], $sessid); + return $header; + } + + /*! + * desc + * + * @method _persist + * @private + * @param str $token the session token + */ + private function _persist($token) + { + file_put_contents($this->config['store'], $token); + } + + /*! + * desc + * + * @method _getToken + * @private + * @return str + */ + private function _getToken() + { + return file_get_contents($this->config['store']); + } + + /*! + * desc + * + * @method _initCall + * @private + * @param str $uri the web service uri + * @return $curl instance + */ + private function _initCall($uri, $method) + { + $curl = curl_init(); + $url = $this->config['protocol'] . $this->config['hostname'] . $uri; + if ($this->config['debug'] == self::MOST_VERBOOSE) { + $this->formatter->writeTags([ + [date('H:i:s') , '3'], + [' [' . __class__ . ']', '1'], + [" => $method $url" , '2'], + '' + ]); + } + curl_setopt($curl, CURLOPT_URL , $url); + curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curl, CURLOPT_HEADER , true); + curl_setopt($curl, CURLOPT_COOKIESESSION , false); + curl_setopt($curl, CURLOPT_USERAGENT , $this->config['key']); + if (preg_match('/^https:/i', $this->config['protocol'])) { + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0); + } + if (isset($this->config['http']) && isset($this->config['http']['user']) && isset($this->config['http']['password'])) { + curl_setopt($curl, CURLOPT_HTTPAUTH, CURLAUTH_ANY); + curl_setopt($curl, CURLOPT_USERPWD , $this->config['http']['user'] . ':'. $this->config['http']['password']); + } + return $curl; + } + + /*! + * @method _send + * @private + * @param str $uri the web service uri to call + * @param []|null $data the post params + * @param str $method the webservice method + * @param bool $header the header to send + * @retval [assoc] + */ + private function _send($uri, $data=null, $method="GET", $header=array()) + { + try { + $stime = microtime(true); + $date = Tool::dateFromTime($stime); + $curl = $this->_initCall($uri, $method); + curl_setopt($curl, CURLOPT_CUSTOMREQUEST , $method); + if ($method == "POST") { + curl_setopt($curl, CURLOPT_POST, true); + if ($data!=null && !empty($data)) { + $fields = http_build_query($data); + $header[] = 'Content-Length: ' . strlen($fields); + curl_setopt($curl, CURLOPT_POSTFIELDS, $fields); + } + } + if (count($header) > 0) curl_setopt($curl, CURLOPT_HTTPHEADER, $header); + // curl_setopt($curl, CURLOPT_HEADERFUNCTION, array($this, "HandleHeaderLine")); + $rs = curl_exec($curl); + $exectime = number_format(((microtime(true)-$stime)),5); + $status = curl_getinfo($curl, CURLINFO_HTTP_CODE); + $size = curl_getinfo($curl, CURLINFO_HEADER_SIZE); + $respheader = substr($rs, 0, $size); + $body = substr($rs, $size); + $response = json_decode($body); + $url = curl_getinfo($curl, CURLINFO_EFFECTIVE_URL); + if ($status == 0) { + throw new \Exception(curl_error($curl)); + } + curl_close($curl); + } + catch(\Exception $e) { + if (isset($curl)) { + $status = curl_getinfo($curl, CURLINFO_HTTP_CODE); + $url = curl_getinfo($curl, CURLINFO_EFFECTIVE_URL); + $error = curl_error($curl); + $response = [ 'done' => false, 'msg' => ' CURL ERROR : '.$error.' - status : '.$status.' - url : '.$url ]; + curl_close($curl); + } + } + if ($this->config['debug']) { + $respcontent = null; + if (is_object($response)) { + $respcontent = clone $response; + if (isset($respcontent->data) && $this->config['html_output']) { + if (is_string($respcontent->data)) { + $respcontent->data = htmlentities($respcontent->data); + } + elseif (isset($respcontent->data->html) && is_string($respcontent->data->html)) { + $respcontent->data = clone $response->data; + $respcontent->data->html = htmlentities($respcontent->data->html); + } + } + } + $tags = [ + [date('H:i:s') , 3], + [' ['. __class__ .']' , 1], + [($this->config['debug'] == self::MOST_VERBOOSE ? " <=" : "")." $method $url" , 2], + [" $exectime s " , 4], + ['', null], + ]; + } + switch ($this->config['debug']) { + + case self::MOST_VERBOOSE : + if (isset($data['password'])) { + $data['password'] = '--hidden--'; + } + $traces = var_export([ + 'HEADER' => $this->authenticator->readHeader($header), + 'PARAMS' => $data, + 'METHOD' => $method, + 'RESPONSE' => compact('date', 'uri', 'status') + ['curl' => $rs, 'response' => $respcontent] + ], true) . Formatter::LF; + array_unshift($tags, $traces); + $this->formatter->writeTags($tags); + break; + + case self::VERBOOSE : + array_unshift($tags, Formatter::LF); + $tags[] = var_export(compact('status')+['response' => $respcontent], true); + $this->formatter->writeTags($tags); + break; + } + return compact('date', 'uri', 'response', 'status', 'exectime'); + } + +}