<?php
require_once("vmlib/logUtil.inc");

/**
 * \file AmazonAWS.php
 * \brief Main class to use AWS services (keyGen, CrulRequest, ...)
 *
 * \author Frederic Carretero <frederic.carretero@veremes.com>
 */

Class AmazonAWS {
    const HASHING_ALGORITHM = 'sha256';
    public $sAwsRegion;
    public $aLastCurlRequestInfo;
    public $sHost;
    public $sLogFilePath;   // Chemin optionnel vers le fichier de log pour Amazon S3.
    private $sAwsAccessKeyId;
    private $sAwsSecretAccessKey;
    private $sHashingAlgorithmString;
    
    /**
     * construct
     * @param {string} $sAwsAccessKeyId Access key ID.
     * @param {string} $sAwsSecretAccessKey Secret access key.
     * @param {string} $sAwsRegion Region.
     */
    function __construct ($sAwsAccessKeyId, $sAwsSecretAccessKey, $sAwsRegion) {
        $this->sAwsAccessKeyId = $sAwsAccessKeyId;
        $this->sAwsSecretAccessKey = $sAwsSecretAccessKey;
        $this->sAwsRegion = $sAwsRegion;
        $this->sHashingAlgorithmString = 'AWS4-HMAC-' . strtoupper(self::HASHING_ALGORITHM);
    }
    
    /**
     * Get value of inaccessible property.
     * @param {string} $sPropertyName Name of the property.
     * @return Paramter's value
     */
    function __get($sPropertyName) {
        if (isset($this->$sPropertyName))
            return $this->$sPropertyName;
    }
    
    /**
     * generate an header for AWS request
     * @param {string} $sMainRequestString Requête principale: 'GET https://s3.amazonaws.com/azerty/?Action=ListUsers&Version=2010-05-08 HTTP/1.1'.
     * @param {string} $sHost hostname du serveur de destination.
     * @param {string} $sService Service à contacter (Constante dans les fichiers enfants).
     * @param {Array} $aHeaders Liste de headers à ajouter.
     * @param {string} $sRequestPayload Chaine de référence pour le calcul de la signature (dans le cas du POST passer les paramètres de la requête, S3 'UNSIGNED-PAYLOAD', sinon laisser chaine vide).
     * @param {boolean} $bAddAmzContentHeader Ajoute le header x-amz-content à la liste des headers signés.
     * @param {string|object} $sTimestamp objet timestamp à utiliser (le mot clef 'Now' permet à la fonction d'intancier un objet sur le timestamp courant). A n'utiliser que pour des tests
     * @param {boolean} $bFromTestCase Active les débuggage de la signature (log).
     * @return array request header for AWS or false if error
     */
    function generateAwsSignedHeaders ($sMainRequestString, $sHost, $sService, $aHeaders = array(), $sRequestPayload = 'UNSIGNED-PAYLOAD', $bAddAmzContentHeader = false, $sTimestamp = "Now", $bFromTestCase = false){

        $aMainRequest = explode(" ", $sMainRequestString);

        if (count($aMainRequest) < 2){
            writeToErrorLog('sMainRequestString has to be as this pattern : GET https://s3.amazonaws.com/azerty/?Action=ListUsers&Version=2010-05-08 HTTP/1.1 and yours is ' . $sMainRequestString);
            return false;
        }

        if (count($aMainRequest) > 3){
            // on sauvegarde avant de trafiquer la requete
            $aTempMainRequest = $aMainRequest;
            $aMainRequest = array();
            $aMainRequest[] = $aTempMainRequest[0];
            // éliminer
            $sTempUrl = "";

            for($i = 1; $i <= (count($aTempMainRequest) - 2); $i++){
                $sTempUrl .= $aTempMainRequest[$i];

                if($i != (count($aTempMainRequest) - 2)){
                    $sTempUrl .= " ";
                }
            }

            $aMainRequest[] = $sTempUrl;

            $aMainRequest[] = end($aTempMainRequest);
        }

        if ($sTimestamp == "Now"){
            $sTimestamp = time();
        }

        // Entêtes obligatoires.
        $sXAmzDate = gmdate('Ymd\THis\Z', $sTimestamp);
        $sDate = gmdate('Ymd', $sTimestamp);

        $sHashedPayload = $this->createHashedPayload($sRequestPayload);;

        array_push($aHeaders, 'x-amz-date: ' . $sXAmzDate);
        array_push($aHeaders, 'host: ' . $sHost);

        if($bAddAmzContentHeader){
            array_push($aHeaders, 'x-amz-content-' . self::HASHING_ALGORITHM . ': ' . $sHashedPayload);
        }

        $sSignedHeaders = $this->prepareQuerySignedHeaders($aHeaders);

        // 1. Création d'une demande canonique pour Signature V4.
        $sCanonicalRequest = $this->mainRequestArrayToCanonicalString($aMainRequest, $sHost, $aHeaders, $sHashedPayload, $sSignedHeaders);

        if($bFromTestCase){
            writeToDebugLog("creq : \n" . $sCanonicalRequest);
        }

        // 2. Création d'une chaîne à signer pour Signature V4.
        $sStringToSign = $this->createStringToSignv2($sDate, $sXAmzDate, $sCanonicalRequest, $sService);

        if($bFromTestCase){
            writeToDebugLog("sts : \n" . $sStringToSign);
        }

        // 3. Calcul de la signature pour AWS Signature V4.
        $sSigningKey = $this->createSigningKey($sService, $sDate);
        $sSignature = $this->calculateSignature($sStringToSign, $sSigningKey);

        // 4. Création de l'entête "Authorization" pour la requête.
        $sCredentialScope = $this->createCredentialScope($sDate, $sService);
        $sAuthorizationHeader = $this->generateAuthorizationHeader($this->sHashingAlgorithmString, $sCredentialScope, $sSignedHeaders, $sSignature);

        if($bFromTestCase){
            writeToDebugLog("authz : \n" . $sAuthorizationHeader);
        }

        array_push($aHeaders, 'Authorization: ' . $sAuthorizationHeader);

        return $aHeaders;
    }

    /**
     * generate an canonical request for AWS request
     * @param {array} $aMainRequest Main request String with this pattern: 'GET https://s3.amazonaws.com/azerty/?Action=ListUsers&Version=2010-05-08 HTTP/1.1' exploded on spaces.
     * @param {string} $sHost HostName to parse url
     * @param {array} $aHeaders sorted array of all the headers of this request
     * @param {string} $sHashedPayload hash of the payload request 
     * @return array request header for AWS or false if error
     */
    function mainRequestArrayToCanonicalString($aMainRequest, $sHost, $aHeaders, $sHashedPayload, $sSignedHeaders){
        $sHTTPRequestMethod = $aMainRequest[0]; 

        $aFullUrl = explode("?", $aMainRequest[1]);
        
        $sCanonicalQueryString  = "";
        if(count($aFullUrl) > 1){
            $sCanonicalQueryString  = $this->prepareQueryString($aFullUrl[1]);
        }

        $sCanonicalUri = $this->prepareQueryUri($aFullUrl[0], $sHost);
        $sCanonicalHeaders = $this->prepareQueryHeaders($aHeaders);
        $sCanonicalSignedHeaders = $sSignedHeaders;

        $sReturn = $sHTTPRequestMethod . chr(10) .
                $sCanonicalUri . chr(10) .
                $sCanonicalQueryString . chr(10) .
                $sCanonicalHeaders . chr(10) .
                $sCanonicalSignedHeaders . chr(10) . 
                $sHashedPayload;

        return  $sReturn;
    }

    /**
     * Curl request
     * @param {string} $sUrl Url of the Curl request.
     * @param {string} $sType Type of the Curl request (GET, POST).
     * @param {array} $aData Data of the Curl request.
     * @param {array} $aHeaders Headers of the Curl request.
     * @return Request result
     */
    // 
    function curlRequest($sUrl, $sType, $aData = array(), $aHeaders = array(), $bMultipartFormData = false) {
        //
        $this->aLastCurlRequestInfo = '';
        //
        $ch = curl_init();
        $sType = strtoupper($sType);
        // Force la méthode de requête utilisée (GET, POST...).
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $sType);
        // Url à utiliser.
        if (($sType == 'GET' || $sType == 'DELETE') && !empty($aData))
            $sUrl .= '?' . http_build_query($aData);
        curl_setopt($ch, CURLOPT_URL, $sUrl);
        // Retour sous forme de texte. 
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        // Requête POST.
        if ($sType == 'POST') {
            curl_setopt($ch, CURLOPT_POST, true);
            // Chaîne de requête en encodage URL.
            if (is_array($aData) && !$bMultipartFormData)
                $aData = http_build_query($aData);
            // Données de la requête.
            curl_setopt($ch, CURLOPT_POSTFIELDS, $aData);
            //
            curl_setopt($ch, CURLOPT_SAFE_UPLOAD, true);
            // Entête pour la requête en POST.
            //$aHeaders[] = 'Content-Type: application/x-www-form-urlencoded';
        }
        // Entête pour la requête
        //$aHeaders[] = 'Accept: application/json';
        //if (!empty($this->sToken))
            //$aHeaders[] = 'Authorization: Bearer ' . $this->sToken; // Token obligatoire pour éxécuter la requête.
        // Durée max. de la requête.
        //curl_setopt($ch, CURLOPT_TIMEOUT, 120);
        // Exécute la session CURL.
        // Log.
        /*
        $handle = fopen("curl_log.txt", "a");
        fwrite($handle, PHP_EOL . '-----------------------' . PHP_EOL);
        curl_setopt($ch, CURLOPT_VERBOSE, true);
        curl_setopt($ch, CURLOPT_STDERR , $handle);
        */
        // ajout des en-tête
        curl_setopt($ch, CURLOPT_HTTPHEADER, $aHeaders);
        curl_setopt($ch, CURLINFO_HEADER_OUT, true);
        //curl_setopt($ch, CURLOPT_ENCODING, "");
        //curl_setopt($ch, CURLOPT_MAXREDIRS, 10);
        curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
        // Curl error: SSL certificate problem: unable to get local issuer certificate
        // Curl error n°60
        curl_setopt ($ch, CURLOPT_SSL_VERIFYPEER, false);      
        //
        $output = curl_exec($ch);
        // Erreur de la requête CURL.
        if(curl_errno($ch)) {
            writeToErrorLog('Curl error: ' . curl_error($ch));
            writeToErrorLog('Curl error n°' . curl_errno($ch));
        }
        // Informations de la requête.
        $aCurlInfo = curl_getinfo($ch);
        $this->aLastCurlRequestInfo = $aCurlInfo;
        //file_put_contents('C:/svn/gtf_cloud/gtf.engines/log/response.log', print_r($aCurlInfo, true));
        //writeToErrorLog(print_r(curl_getinfo($ch), true));
        
        // Ferme la session CURL.
        curl_close($ch);
        //
        //fclose($handle);
        //
        return $output;
    }

    /**
     * Creating a credential scope.
     * @param {string} $sDate Date (YYYYMMDD).
     * @param {string} $sApiComponentService Api component service.
     * @return Credential scope.
     */
    function createCredentialScope($sDate, $sApiComponentService) {
        return $sDate . "/" . $this->sAwsRegion . '/' . $sApiComponentService . '/aws4_request';
    }
    
    /**
     * Creating a string to sign.
     * @param {string} $sDate Date (YYYYMMDD).
     * @param {string} $sTimestamp Unix timestamp.
     * @param {string} $sCanonicalRequest Canonical request.
     * @param {string} $sApiComponentService Api component service.
     * @return String to sign.
     */
    function createStringToSign($sDate, $sTimestamp, $sCanonicalRequest, $sApiComponentService) {
        $sRequestDateTime = $sDate . 'T' . gmdate('His', $sTimestamp) . 'Z';
        $sCredentialScope = $this->createCredentialScope($sDate, $sApiComponentService);
        $sHashedCanonicalRequest = hash(self::HASHING_ALGORITHM, $sCanonicalRequest, false);
        $sStringToSign = $this->sHashingAlgorithmString . chr(10) .
                        $sRequestDateTime . chr(10) .
                        $sCredentialScope . chr(10) .
                        $sHashedCanonicalRequest;
        return $sStringToSign;
    }

    /**
     * Creating a string to sign.
     * @param {string} $sDate Date (YYYYMMDD).
     * @param {string} $sTimestamp Unix timestamp.
     * @param {string} $sCanonicalRequest Canonical request.
     * @param {string} $sApiComponentService Api component service.
     * @return String to sign.
     */
    function createStringToSignv2($sDate, $sXAmzDate, $sCanonicalRequest, $sApiComponentService) {
        $sCredentialScope = $this->createCredentialScope($sDate, $sApiComponentService);
        $sHashedCanonicalRequest = hash(self::HASHING_ALGORITHM, $sCanonicalRequest, false);
        $sStringToSign = $this->sHashingAlgorithmString . chr(10) .
                        $sXAmzDate . chr(10) .
                        $sCredentialScope . chr(10) .
                        $sHashedCanonicalRequest;
        return $sStringToSign;
    }

    /**
     * Creating a hashed payload.
     * @param {string} $sPayload CurrentPayload.
     * @return String hashed Payload.
     */
    function createHashedPayload($sPayload){
        if($sPayload == "UNSIGNED-PAYLOAD"){
            return $sPayload;
        }else{
            return hash(self::HASHING_ALGORITHM, $sPayload, false);
        }
    }

    /**
     * Prepare a query string for signature calculation .
     * @param {string} $sQueryString queryString to prepare.
     * @return QueryString prepared
     */
    function prepareQueryString ($sQueryString){
        $aUri = explode("?", $sQueryString);
        $sParameters = $aUri[count($aUri) - 1];
        $aParameters = explode("&", $sParameters);

        $sReturn = ""; 

        //order array before treatement F < a Case sensitive
        sort($aParameters);

        for ($i = 0; $i < count($aParameters); $i++){
            $aParameter = explode("=",$aParameters[$i]);
            if(count($aParameter) === 2){
                $sReturn .= rawurlencode($aParameter[0]);
                $sReturn .= "=";
                $sReturn .= rawurlencode($aParameter[1]);
                if ($i < count($aParameters) - 1){
                    $sReturn .= "&";
                }
            }
        }
        return $sReturn;
    }

    /**
     * Prepare a Uri for signature calculation .
     * @param {string} $sUriWithoutQuery queryURI to prepare. (https://s3.amazon.aws.com/toto/tata)
     * @param {string} $sHost Hostname to extract the path after domain name
     * @return QueryString prepared
     */
    function prepareQueryUri ($sUriWithoutQuery, $sHost){

        $sUriWithoutQuery = preg_replace('/^https?:\/\/' . $sHost . '/i', '', $sUriWithoutQuery);

        $aUrl = explode("/", $sUriWithoutQuery);

        $aUrl = array_values(array_filter($aUrl, function($value) {return ($value !== '');}));

        $i = 0;
        $aUri = array();

        while ($i < count($aUrl)){
            if ($aUrl[$i] !== '.'){
                if($aUrl[$i] !== '..'){
                    array_push($aUri, $aUrl[$i]);
                } else {
                    array_splice($aUri, -1);
                }
            }
            $i++;
        }

        $sReturn = "/" . str_replace('%2F', '/', rawurlencode(implode("/", $aUri)));

        if (substr($sUriWithoutQuery, -1) == '/' && count($aUrl) > 1){
            $sReturn .= "/";
        } 

        return $sReturn;
    }

    /**
     * Prepare headers for signature calculation .
     * @param {array} $aHeaders array of the headers for the request (array of string)
     * @return QueryString prepared
     */
    function prepareQueryHeaders ($aHeaders){
        $aMergedHeader = array();

        $sReturn = "";

        // dédoublonage
        for ($i = 0; $i < count($aHeaders); $i++){
            $aHeader = explode(":", $aHeaders[$i]);
            if (count($aHeader) === 2){
                $sReplacer = ' ';
                if (strpos(trim($aHeader[1]), "\n") > -1) {
                    $sReplacer = ',';
                }
                $aMergedHeader[strtolower($aHeader[0])][] = preg_replace('/\s{1,}/m', $sReplacer, trim($aHeader[1]));
            }
        }

        ksort($aMergedHeader, SORT_STRING | SORT_FLAG_CASE);

        // calcul des header pour la requete canonique
        foreach ($aMergedHeader as $sKey => $aValue) {
            $sReturn .= $sKey;
            $sReturn .= ":";
            $sReturn .= implode(',', $aValue);
            $sReturn .= chr(10);
        }

        return $sReturn;
    }

    /**
     * Prepare signed headers for signature calculation .
     * @param {array} $aHeaders array of the headers for the request (array of string)
     * @return QueryString prepared
     */
    function prepareQuerySignedHeaders ($aHeaders){
        $aMergedHeader = array();
        $sReturn = "";

        // dédoublonage
        for ($i = 0; $i < count($aHeaders); $i++){
            $aHeader = explode(":", $aHeaders[$i]);
            if (count($aHeader) === 2){
                $aMergedHeader[strtolower($aHeader[0])][] = trim($aHeader[1]);
            }
        }

        ksort($aMergedHeader, SORT_STRING | SORT_FLAG_CASE);

        // aggrégat des en-t^tes signées
        foreach ($aMergedHeader as $sKey => $aValue) {
            $sReturn .= $sKey;
            $sReturn .= ";";
        }

        // on supprime le dernier ;
        $sReturn = substr($sReturn, 0, -1);

        return $sReturn;
    }
    
    /**
     * Creating a signing Key.
     * @param {string} $sApiComponentService Component service for API execution.
     * @param {string} $sDate Date (YYYYMMDD).
     * @return Signing key.
     */
    function createSigningKey($sApiComponentService, $sDate) {
        return hash_hmac(self::HASHING_ALGORITHM, 'aws4_request', hash_hmac(self::HASHING_ALGORITHM, $sApiComponentService, hash_hmac(self::HASHING_ALGORITHM, $this->sAwsRegion, hash_hmac(self::HASHING_ALGORITHM, $sDate, 'AWS4' . $this->sAwsSecretAccessKey, true), true), true), true);
    }
    
    /**
     * Calculating a Signature (Version 4 algorithm).
     * @param {string} $sStringToSign String to sign.
     * @param {string} $sSigningKey Signing key.
     * @return Signature.
     */
    function calculateSignature($sStringToSign, $sSigningKey) {
        return hash_hmac(self::HASHING_ALGORITHM, $sStringToSign, $sSigningKey);
    }
    
    /**
     * Generating an authorization header.
     * @param {string} $sAlgorithm String of the hashing algorithm.
     * @param {string} $sCredentialScope Credential scope.
     * @param {string} $sSignedHeaders Request header names.
     * @param {string} $sSignature Calculated signature.
     * @return Headers for request.
     */
    function generateAuthorizationHeader($sAlgorithm, $sCredentialScope, $sSignedHeaders, $sSignature) {
        return $sAlgorithm . ' Credential=' . $this->sAwsAccessKeyId . '/' . $sCredentialScope . ', SignedHeaders=' . $sSignedHeaders . ', Signature=' . $sSignature;
    }
    
    /**
     * Write a message to the error log file.
     * @param {string} $sMessage Message to write to the log file.
     */
    function writeToErrorLog($sMessage) {
        $aDebugBacktrace = debug_backtrace();
        $sLogMessage = '|ERROR|' . $aDebugBacktrace[1]['class'] . '::' . $aDebugBacktrace[1]['function'] . '| ' . $sMessage;
        if (empty($this->sLogFilePath))
            writeToErrorLog($sLogMessage);
        else
            writeToLog($sLogMessage, $this->sLogFilePath);
    }
}
?>