diff --git a/tequila.php b/tequila.php index 66bdbdf..d159844 100644 --- a/tequila.php +++ b/tequila.php @@ -1,295 +1,299 @@ // Copyright (C) 2021 Liip SA // Copyright (C) 2021 Doran Kayoumi // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; version 2. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public Licensáe // along with this program. If not, see . require __DIR__ . '/vendor/autoload.php'; require_once "exception.php"; class TequilaClient { // maybe move to configuration file? const TEQUILA_BIN = "/cgi-bin/tequila"; const COOKIE_NAME = "TequilaPHP"; const COOKIE_LIFE = 0; const BODY_GLUE = "+"; const LANGUAGE_FRENCH = 0; const LANGUAGE_ENGLISH = 1; const LANGUAGE_GERMAN = 2; const SERVER_ENDPOINTS = ["createrequest", "fetchattributes", "logout"]; private $serverURL; private $timeout; private $logoutURL; private $language; private $applicationURL; private $applicationName; private $attributes = []; private $key; private $stack; /** * @brief Class constructor * * @param $server - Tequila server URL * @param $timeout - Session timeout * @param $applicationName - Name of the application using the client * @param $applicationURL - (optional) URL the application using the client * @param $language - (Default: English) Language of the application */ function __construct(string $server, int $timeout, string $applicationName, string $applicationURL = "", int $language = TequilaClient::LANGUAGE_ENGLISH, callable $handler = null) { $this->serverURL = $server . TequilaClient::TEQUILA_BIN; $this->timeout = $timeout; $this->language = $language; $this->applicationURL = $applicationURL; $this->applicationName = $applicationName; $this->stack = GuzzleHttp\HandlerStack::create($handler); // if no application URL was specified, we try to generate it if (empty($this->applicationURL)) { $protocol = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on' ? "https://" : "http://"; $this->applicationURL = $protocol . $_SERVER['SERVER_NAME'] . ":" . $_SERVER['SERVER_PORT'] . $_SERVER['PHP_SELF']; if (!empty($_SERVER['PATH_INFO'])) $this->applicationURL .= $_SERVER['PATH_INFO']; if (!empty($_SERVER['QUERY_STRING'])) $this->applicationURL .= "?" . $_SERVER['QUERY_STRING']; } } /** * @brief User authentication to server * * @param $wantedAttributes - (optional) List of attributes about the user that the server will return * @param $filters - (optional) Filters that will be applied to the user attributes * @param $authorised - (optional) Tequila server restrictions to lift */ public function authenticate( array $wantedAttributes = [], string $filters = "", string $authorised = "", string $allowedRequestHosts = "") { // session_start(); if ($this->preExistingSession()) return; // Start buffering, as we want to use header() in that function. ob_start(); // check if a session creation was already started if (!empty($_COOKIE[TequilaClient::COOKIE_NAME])) { $this->key = $_COOKIE[TequilaClient::COOKIE_NAME]; $attributes = $this->fetchAttributes($_COOKIE[TequilaClient::COOKIE_NAME], $allowedRequestHosts); if (!empty($attributes)) { $this->createSession($attributes); // If there was output, spit it. ob_end_flush(); return; } } $this->key = $this->createRequest($wantedAttributes, $filters, $authorised); setcookie(TequilaClient::COOKIE_NAME, $this->key, array( 'expires' => TequilaClient::COOKIE_LIFE, 'httponly' => true, "secure" => true, )); header("Location: {$this->serverURL}/requestauth?requestkey={$this->key}"); // If there was output, spit it. ob_end_flush(); exit; } /** * @brief Logout from Tequila server * * @param $redirectUrl - (optional) URL to redirect to after logout */ public function logout(string $redirectUrl = "") { session_destroy(); // Delete cookie by setting expiration time in the past with root path setcookie(TequilaClient::COOKIE_NAME, "", time() - 3600); // Start buffering, as we want to use header() in that function. ob_start(); $this->contactServer("logout"); $redirectUrl = empty($redirectUrl) ? $this->applicationURL : urlencode($redirectUrl); header("Location: {$this->serverURL}/logout?urlaccess={$redirectUrl}"); // If there was output, spit it. ob_end_flush(); } /** * @brief Sends an authentication request * * @param $wantedAttributes - (optional) List of attributes about the user that the server will return * @param $filters - (optional) Filters that will be applied to the user attributes * @param $authorised - (optional) Tequila server restrictions to lift * * @return Key returned by the Tequila server */ private function createRequest(array $wantedAttributes = [], string $filters = "", string $authorised = "") : string { $body = []; $body["urlaccess"] = $this->applicationURL; $body["dontappendkey"] = "1"; $body["language"] = $this->language; $body["service"] = $this->applicationName; $body["request"] = implode(TequilaClient::BODY_GLUE, $wantedAttributes); $body["allows"] = $filters; $body["require"] = $authorised; $res = $this->contactServer('createrequest', $body); preg_match('/^(?P\w+)=(?P\w+)\b$/', $res, $matches); if ($matches["key"] == "key") { return $matches["value"]; } throw new TequilaException("No requestkey obtained from createRequest"); } /** * @brief Retrieve the attributes of an authenticated user * (i.e. the ones requested when establishing an authentication) * * @param $key - the request key * * @return Array containing all the user attributes */ private function fetchAttributes(string $key, string $allowedRequestHosts = "") : array { $body = []; $body["key"] = $key; if (!empty($allowedRequestHosts)) $body["allowedrequesthosts"] = preg_split("/\\r\\n|\\r|\\n/", $allowedRequestHosts); $res = $this->contactServer('fetchattributes', $body); $result = []; $attributes = explode("\n", $res); foreach ($attributes as $attribute) { $attribute = trim($attribute); if (!$attribute) continue; list($key, $val) = explode("=", $attribute, 2); $result[$key] = $val; } return $result; } /** * @brief Sends a POST request to one of the servers endpoints * * @param $endpoint - the server endpoint to contact * @param $fields - (optional) the fields to add to the requests body * * @throws TequilaException if the server returns a code other than 200, that no connection could be established * or that we're trying to acces an unknow endpoint * * @return Body of the server response */ private function contactServer($endpoint, $fields = []) : string { // check if it's a valid endpoint if (!in_array($endpoint, TequilaClient::SERVER_ENDPOINTS)) { throw new TequilaException("Unknown endpoint {$endpoint}"); } try { $client = new GuzzleHttp\Client([ 'base_uri' => $this->serverURL . "/", "handler" => $this->stack ]); /** * Note: First things first, sorry for the horrible code that follows. * So, the Tequila server doesn't understand/use normal POST requests and * only works cleartext body. */ $reqBody = []; if (is_array($fields) && count($fields)) { foreach ($fields as $key => $val) { $reqBody[] = "{$key}={$val}"; } } $response = $client->request("POST", $endpoint, [ "body" => implode("\n", $reqBody) . "\n", ]); return $response->getBody(); } catch (GuzzleHttp\Exception\RequestException $e) { + if (!$e->hasResponse()) { + throw new TequilaException("No response from server : {$e->getMessage()}", 1, $e); + } + $response = $e->getResponse(); throw new TequilaException("Unexpected return from server : [{$response->getStatusCode()}] {$response->getReasonPhrase()}", 1, $e); } catch (GuzzleHttp\Exception\ConnectException $e) { throw new TequilaException("Connection to {$this->serverURL} server failed", 0, $e); } } /** * @brief Check if a session was previously established * * @return True if a session was previously established * @return False if no session was previously established */ private function preExistingSession() : bool { if (empty($_SESSION)) return false; // check if the session hasn't expired if ((time() - $_SESSION["creation"]) > $this->timeout) return false; $this->key = $_SESSION ['key']; return true; } /** * @brief Establish a new session * * @param $attributes - the user attributes returned by the server */ private function createSession(array $attributes) { $_SESSION["creation"] = time(); foreach ($attributes as $key => $val) { echo("{$key} {$val}"); $this->attributes[$key] = $val; $_SESSION[$key] = $val; } } public function getKey() : string { return $this->key; } public function getAttributes() : array { return $this->attributes; } }