<?php
//+------------------------------------------------------------------+
//|                                             MetaTrader 5 Web API |
//|                   Copyright 2000-2021, MetaQuotes Software Corp. |
//|                                        http://www.metaquotes.net |
//+------------------------------------------------------------------+
/**
 * Class authorization on MetaTrader 5 Server
 */
class MTAuthProtocol
  {
  private $m_connect = null;
  private $m_agent = '';
  /**
   * @param MTConnect $connect connection to server
   * @param string $agent - name of agent
   * @return \MTAuthProtocol
   *
   */
  public function __construct($connect, $agent)
    {
    $this->m_connect = $connect;
    $this->m_agent = $agent;
    }
  /**
   * Authorization on MetaTrader 5 server
   * @param string $login - manager login
   * @param string $password - manager password
   * @param bool $is_crypt - need crypt connection
   * @param string $crypt_rand - crypt rand string
   * @return MTRetCode
   */
  public function Auth($login, $password, $is_crypt, &$crypt_rand)
    {
    //--- send request to mt server
    if (($error_code = $this->SendAuthStart($login, $is_crypt, $auth_start_answer)) != MTRetCode::MT_RET_OK)
      {
      if (MTLogger::getIsWriteLog()) MTLogger::write(MTLoggerType::ERROR, 'auth start failed: [' . $error_code . ']' . MTRetCode::GetError($error_code));
      return $error_code;
      }
    //--- get code from hex string
    $rand_code = MTUtils::GetFromHex($auth_start_answer->SrvRand);
    //--- random string for MT server
    $random_cli_code = MTUtils::GetRandomHex(16);
    //--- get hash password with random code
    $hash = MTUtils::GetHashFromPassword($password, $rand_code);
    //--- send answer to server
    if (($error_code = $this->SendAuthAnswer($hash, $random_cli_code, $auth_answer)) != MTRetCode::MT_RET_OK)
      {
      if (MTLogger::getIsWriteLog()) MTLogger::write(MTLoggerType::ERROR, 'auth answer failed: [' . $error_code . ']' . MTRetCode::GetError($error_code));
      return $error_code;
      }
    //--- check password with another random code from MT server
    $hash_password = MTUtils::GetHashFromPassword($password, MTUtils::GetFromHex($random_cli_code));
    //--- check hash of password
    if ($hash_password != $auth_answer->CliRand)
      {
      if (MTLogger::getIsWriteLog()) MTLogger::write(MTLoggerType::ERROR, 'server sent incorrect password hash: is:' . $auth_answer->CliRand . ', my: ' . $hash_password);
      return MTRetCode::MT_RET_AUTH_SERVER_BAD;
      }
    //--- get crypt rand from MT server
    $crypt_rand = $auth_answer->CryptRand;
    //---
    return MTRetCode::MT_RET_OK;
    }
  /**
   * Send AUTH_ANSWER to MT server
   * @param string $hash - password hash
   * @param string $random_cli_code client random string
   * @param MTAuthAnswer $auth_answer - result from server
   * @return MTRetCode
   */
  private function SendAuthAnswer($hash, $random_cli_code, &$auth_answer)
    {
    //--- send first request, with login, webapi version
    $data = array(MTProtocolConsts::WEB_PARAM_SRV_RAND_ANSWER => $hash, MTProtocolConsts::WEB_PARAM_CLI_RAND => $random_cli_code
    );
    //--- send request
    if (!$this->m_connect->Send(MTProtocolConsts::WEB_CMD_AUTH_ANSWER, $data))
      {
      if (MTLogger::getIsWriteLog()) MTLogger::write(MTLoggerType::ERROR, 'send auth answer failed');
      return MTRetCode::MT_RET_ERR_NETWORK;
      }
    //--- get answer
    if (($answer = $this->m_connect->Read(true)) == null)
      {
      if (MTLogger::getIsWriteLog()) MTLogger::write(MTLoggerType::ERROR, 'answer auth answer is empty');
      return MTRetCode::MT_RET_ERR_NETWORK;
      }
    //--- parse answer
    if (($error_code = $this->ParseAuthAnswer($answer, $auth_answer, $error)) != MTRetCode::MT_RET_OK)
      {
      if (!empty($error)) if (MTLogger::getIsWriteLog()) MTLogger::write(MTLoggerType::ERROR, 'parse auth answer failed: ' . $error);
      return $error_code;
      }
    //--- ok
    return MTRetCode::MT_RET_OK;
    }
  /**
   * check answer from MetaTrader 5 server
   * @param  string $answer
   * @param  MTAuthStartAnswer $auth_answer
   * @param  string $error
   * @return MTRetCode
   */
  private function ParseAuthStart(&$answer, &$auth_answer, &$error)
    {
    $pos = 0;
    //--- get command answer
    $command = $this->m_connect->GetCommand($answer, $pos);
    if ($command != MTProtocolConsts::WEB_CMD_AUTH_START)
      {
      $error = 'type answer "' . $command . '" is incorrect, is not ' . MTProtocolConsts::WEB_CMD_AUTH_START;
      return MTRetCode::MT_RET_ERR_DATA;
      }
    //---
    $auth_answer = new MTAuthStartAnswer();
    //--- get param
    $pos_end = -1;
    while (($param = $this->m_connect->GetNextParam($answer, $pos, $pos_end)) != null)
      {
      switch ($param['name'])
      {
        case MTProtocolConsts::WEB_PARAM_RETCODE:
          $auth_answer->RetCode = $param['value'];
          break;
        case MTProtocolConsts::WEB_PARAM_SRV_RAND:
          $auth_answer->SrvRand = $param['value'];
          break;
      }
      }
    //--- check ret code
    if (($error_code = MTConnect::GetRetCode($auth_answer->RetCode)) != MTRetCode::MT_RET_OK) return $error_code;
    //--- get srv rand
    if (empty($auth_answer->SrvRand) || $auth_answer->SrvRand == 'none')
      {
      $error = 'srv rand incorrect';
      return MTRetCode::MT_RET_ERR_PARAMS;
      }
    //---
    return MTRetCode::MT_RET_OK;
    }
  /**
   * Send auth_start request
   * @param string $login  - user login
   * @param bool $is_crypt - need crypt protocol
   * @param MTAuthStartAnswer $auth_answer  - answer from server
   * @return MTRetCode
   */
  private function SendAuthStart($login, $is_crypt, &$auth_answer)
    {
    //--- send first request, with login, webapi version
    $data = array(MTProtocolConsts::WEB_PARAM_VERSION => WebAPIVersion, MTProtocolConsts::WEB_PARAM_AGENT => $this->m_agent, MTProtocolConsts::WEB_PARAM_LOGIN => $login, MTProtocolConsts::WEB_PARAM_TYPE => 'MANAGER', MTProtocolConsts::WEB_PARAM_CRYPT_METHOD => $is_crypt
            ? MTProtocolConsts::WEB_VAL_CRYPT_AES256OFB : MTProtocolConsts::WEB_VAL_CRYPT_NONE
    );

    //--- send request
    if (!$this->m_connect->Send(MTProtocolConsts::WEB_CMD_AUTH_START, $data, true)) return MTRetCode::MT_RET_ERR_NETWORK;
    //--- get answer
    if (($answer = $this->m_connect->Read(true)) == null)
      {
      if (MTLogger::getIsWriteLog()) MTLogger::write(MTLoggerType::ERROR, 'answer auth start is empty');
      return MTRetCode::MT_RET_ERR_NETWORK;
      }
    //--- parse answer
    if (($error_code = $this->ParseAuthStart($answer, $auth_answer, $error)) != MTRetCode::MT_RET_OK)
      {
      if (MTLogger::getIsWriteLog()) MTLogger::write(MTLoggerType::ERROR, 'parse auth start failed: [' . $error_code . ']' . $error);
      return $error_code;
      }
    //---
    return MTRetCode::MT_RET_OK;
    }
  /**
   * Parse answer from request AUTH_ANSWER
   * @param string $answer - answer from server
   * @param MTAuthAnswer $auth_answer - result
   * @param string $error
   * @return MTRetCode
   */
  private function ParseAuthAnswer($answer, &$auth_answer, &$error)
    {
    $pos = 0;
    //--- get command answer
    $command = $this->m_connect->GetCommand($answer, $pos);
    if ($command != MTProtocolConsts::WEB_CMD_AUTH_ANSWER)
      {
      $error = 'type answer "' . $command . '" is incorrect, is not ' . MTProtocolConsts::WEB_CMD_AUTH_ANSWER;
      return MTRetCode::MT_RET_ERR_DATA;
      }
    //---
    $auth_answer = new MTAuthAnswer();
    //--- get param
    $pos_end = -1;
    while (($param = $this->m_connect->GetNextParam($answer, $pos, $pos_end)) != null)
      {
      switch ($param['name'])
      {
      //--- ret code
        case MTProtocolConsts::WEB_PARAM_RETCODE:
          $auth_answer->RetCode = $param['value'];
          break;
      //--- cli rand
        case MTProtocolConsts::WEB_PARAM_CLI_RAND_ANSWER:
          $auth_answer->CliRand = $param['value'];
          break;
      //--- crypt rand
        case MTProtocolConsts::WEB_PARAM_CRYPT_RAND:
          $auth_answer->CryptRand = $param['value'];
          break;
      }
      }
    //--- check ret code
    if (($ret_code = MTConnect::GetRetCode($auth_answer->RetCode)) != MTRetCode::MT_RET_OK) return $ret_code;
    //--- check CliRand
    if (empty($auth_answer->CliRand) || $auth_answer->CliRand == 'none')
      {
      $error = 'cli rand answer incorrect';
      return MTRetCode::MT_RET_ERR_PARAMS;
      }
    //---
    return MTRetCode::MT_RET_OK;
    }
  }

/**
 * Class get answer on request AUTH_START
 */
class MTAuthStartAnswer
  {
  public $RetCode = '-1';
  public $SrvRand = 'none';
  }

/**
 * Class get answer on request AUTH_ANSWER
 */
class MTAuthAnswer
  {
  public $RetCode = '-1';
  public $CliRand = 'none';
  public $CryptRand = '';
  }

?>
 
