<?php
/*
 * This file is part of the pws-auth package.
 *
 * (c) meta-tech.academy
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */
namespace MetaTech\PwsAuth;

use MetaTech\Util\Tool;
use MetaTech\PwsAuth\Token;
use MetaTech\PwsAuth\AuthenticateException;

/*!
 * a simple class to authenticate access throught webservices using Pluie\Auth\Token
 * and PwsAuth Protocol
 * 
 * @package     MetaTech\PwsAuth
 * @class       Authenticator
 * @author      a-Sansara
 * @date        2016-05-02 13:08:01 CET
 * 
 */
class Authenticator
{
    /*! @constant DATE_FORMAT */
    const DATE_FORMAT  = 'smHdiy';
    /*! @constant DATE_LENGTH */
    const DATE_LENGTH  = 12;
    /*! @constant DATE_LENGTH */
    const DEFAULT_ALGO = 'sha256';

    /*! @protected @var [assoc] $config */
    protected $config;

    /*!
     * @constructor
     * @public
     */
    public function __construct($config)
    {
        $this->config = $config;
    }

    /*!
     * check if specified Token is a valid token
     * 
     * @method      isValid
     * @public
     * @param       Pluie\Auth\Token     $token
     * @return      bool
     */
    public function isValid(Token $token = null)
    {
        return !is_null($token) && $token->getType() == $this->config['type'] && $this->checkObfuscatePart($token);
    }

    /*!
     * generate a unique signature at given time for specifyed user
     * 
     * @method      sign
     * @public
     * @param       str     $dtime  given time in sqldatetime format
     * @param       str     $login  the user login
     * @param       str     $key    the user key
     * @return      str
     */
    public function sign($dtime, $login, $key, $length=null)
    {
        $str = Tool::concat($this->config['hash']['sep'], [$dtime, $login, $this->getUserSalt($login), $key]);
        return substr(hash($this->config['hash']['algo'], $str), is_null($length) ? - $this->config['hash']['length'] : - $length);
    }

    /*!
     * generate the salt for a specific user
     * 
     * @method      getUserSalt
     * @public
     * @param       str     $login   the user login
     * @return      str
     */
    public function getUserSalt($login)
    {
        return substr(
            hash(self::DEFAULT_ALGO, $login . $this->config['salt']['common']), 
            $this->config['salt']['user.index'], 
            $this->config['salt']['user.length']
        );
    }

    /*!
     * generate noise to obfuscate token
     * 
     * @method      obfuscate
     * @orivate
     * @param       str     $data
     * @return      str
     */
    private function obfuscate($data, $date)
    {
        return substr(
            hash(self::DEFAULT_ALGO, $date . $data . $this->config['salt']['common']), 
            - $this->config['hash']['session.index']
        );
    }

    /*!
     * check valid noise obfuscation
     * 
     * @method      checkObfuscatePart
     * @public
     * @param       Pluie\Auth\Token    $token
     * @return      bool
     */
    public function checkObfuscatePart(Token $token)
    {
        $tokenValue = $token->getValue();
        return substr($tokenValue, 0, $this->config['hash']['session.index']) == $this->obfuscate($this->deobfuscate($tokenValue), $token->getDate());
    }

    /*!
     * deoffuscate token
     * 
     * @method      deobfuscate
     * @orivate
     * @param       str     $data
     * @return      str
     */
    private function deobfuscate($data)
    {
        return substr($data, $this->config['hash']['session.index']);
    }

    /*!
     * @method      getSessionId
     * @orivate
     * @param       Pluie\Auth\Token    $token
     * @return      str
     */
    public function getSessionId(Token $token)
    {
        return $this->deobfuscate($token->getValue());
    }

    /*!
     * check validity of Token
     * 
     * @mehtod      check
     * @public
     * @param       Pluie\Auth\Token     $token
     * @param       str                  $login
     * @return      bool
     */
    public function check(Token $token = null, $login = '')
    {
        return !is_null($token) && !empty($login) && $this->deobfuscate($token->getValue()) == $this->sign($token->getDate(), $login, $token->getIdent());
    }

    /*!
     * @method      generateNoise
     * @public
     * @param       str     $data
     * @return      str
     */
    public function generateNoise($data)
    {
        return substr(hash(self::DEFAULT_ALGO, str_shuffle($data)), - $this->config['hash']['noise.length']); 
    }

    /*!
     * @method      generateToken
     * @public
     * @param       str     $login
     * @param       str     $key
     * @param       str     $sessid|null
     * @return      Pluie\Auth\Token
     */
    public function generateToken($login, $key, $sessid=null)
    {
        $date       = Tool::now();
        $sessid     = is_null($sessid) ? $this->sign($date, $login, $key) : $sessid;
        $dt         = Tool::formatDate($date, Tool::TIMESTAMP_SQLDATETIME, self::DATE_FORMAT);
        $tokenValue = $dt . $this->obfuscate($sessid, $date) . $sessid;
        $noise      = $this->generateNoise($tokenValue);
        return new Token($this->config['type'], $key, $date, $tokenValue, $noise);
    }

    /*!
     * @method      generateHeader
     * @public
     * @param       str     $login
     * @param       str     $key
     * @param       str     $sessid
     * @return      []
     */
    public function generateHeader($login, $key, $sessid=null)
    {
        $token = $this->generateToken($login, $key, $sessid);
        return array(
            $this->config['header']['auth'] .': ' . $token->getType() . ' ' . $token->getValue() . $token->getNoise(),
            $this->config['header']['ident'].': ' . $token->getIdent()
        );
    }

    /*!
     * get token from specified $header or request headers.
     * 
     * @method      getToken
     * @public
     * @param       [assoc]     $headers
     * @throw       Pluie\Auth\AuthenticateException
     * @return      Pluie\Auth\Token
     */
    public function getToken($headers = null)
    {
        $token = null;
        try {
            if (is_null($headers)) {
                $headers = apache_request_headers();
            }
            if (isset($headers[$this->config['header']['auth']]) && isset($headers[$this->config['header']['ident']])) {
                $tokenValue = $headers[$this->config['header']['auth']];
                $ident      = $headers[$this->config['header']['ident']];
                if (preg_match('/(?P<type>[a-z\d]+) (?P<date>\d{'.self::DATE_LENGTH.'})(?P<id>[a-z\d]+)/i', $tokenValue, $rs)) {
                    $date       = Tool::formatDate($rs['date'], self::DATE_FORMAT, Tool::TIMESTAMP_SQLDATETIME);
                    $tokenValue = substr($rs['id'], 0, -$this->config['hash']['noise.length']);
                    $noise      = substr($rs['id'], -$this->config['hash']['noise.length']);
                    $token      = new Token($rs['type'], $ident, $date, $tokenValue, $noise);
                }
            }
            else {
                throw new \Exception('missing required headers');
            }
        }
        catch(\Exception $e) {
            throw new AuthenticateException("invalid authentication protocol : ".$e->getMessage());
        }
        return $token;
    }

    /*!
     * read header generate by generateHeader
     *
     * @method      readHeader
     * @public
     * @param       [str]   $arrHeaders
     * @return      [assoc]
     */
    public function readHeader($arrHeaders)
    {
        $headers = [];
        if (is_array($arrHeaders)) {
            foreach($arrHeaders as $h) {
                $rs = preg_split('/:/', $h);
                if (count($rs)==2) {
                    $headers[$rs[0]] = trim($rs[1]);
                }
            }
        }
        return $headers;
    }
}