You are here

class Client in Acquia Connector 8

Same name and namespace in other branches
  1. 8.2 src/Client.php \Drupal\acquia_connector\Client
  2. 3.x src/Client.php \Drupal\acquia_connector\Client

Acquia connector client.

@package Drupal\acquia_connector

Hierarchy

Expanded class hierarchy of Client

5 files declare their use of Client
AcquiaConnectorUnitTest.php in tests/src/Unit/AcquiaConnectorUnitTest.php
Contains \Drupal\Tests\acquia_connector\Unit\AcquiaConnectorUnitTest.
CredentialForm.php in src/Form/CredentialForm.php
SettingsForm.php in src/Form/SettingsForm.php
SetupForm.php in src/Form/SetupForm.php
SpiController.php in src/Controller/SpiController.php
1 string reference to 'Client'
acquia_connector.services.yml in ./acquia_connector.services.yml
acquia_connector.services.yml
1 service uses Client
acquia_connector.client in ./acquia_connector.services.yml
Drupal\acquia_connector\Client

File

src/Client.php, line 20

Namespace

Drupal\acquia_connector
View source
class Client {
  use LoggerChannelTrait;
  use StringTranslationTrait;
  use MessengerTrait;

  /**
   * The HTTP client to fetch the feed data with.
   *
   * @var \GuzzleHttp\Client
   */
  protected $client;

  /**
   * Request headers.
   *
   * @var array
   */
  protected $headers;

  /**
   * Acquia SPI server.
   *
   * @var string
   */
  protected $server;

  /**
   * Config Factory Interface.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $config;

  /**
   * The config factory.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $configFactory;

  /**
   * The state service.
   *
   * @var \Drupal\Core\State\StateInterface
   */
  protected $state;

  /**
   * Client constructor.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   Config Factory Interface.
   * @param \Drupal\Core\State\StateInterface $state
   *   The state service.
   */
  public function __construct(ConfigFactoryInterface $configFactory, StateInterface $state) {
    $this->configFactory = $configFactory;
    $this->config = $configFactory
      ->get('acquia_connector.settings');
    $this->server = $this->config
      ->get('spi.server');
    $this->state = $state;
    $this->headers = [
      'Content-Type' => 'application/json',
      'Accept' => 'application/json',
    ];
    $this->client = \Drupal::service('http_client_factory')
      ->fromOptions([
      'verify' => (bool) $this->config
        ->get('spi.ssl_verify'),
      'http_errors' => FALSE,
    ]);
  }

  /**
   * Get account settings to use for creating request authorizations.
   *
   * @param string $email
   *   Acquia account email.
   * @param string $password
   *   Plain-text password for Acquia account. Will be hashed for communication.
   *
   * @return array|false
   *   Credentials array or FALSE.
   *
   * @throws \Drupal\acquia_connector\ConnectorException
   */
  public function getSubscriptionCredentials($email, $password) {
    $body = [
      'email' => $email,
    ];
    $authenticator = $this
      ->buildAuthenticator($email, \Drupal::time()
      ->getRequestTime(), [
      'rpc_version' => ACQUIA_CONNECTOR_ACQUIA_SPI_DATA_VERSION,
    ]);
    $data = [
      'body' => $body,
      'authenticator' => $authenticator,
    ];

    // Don't use nspiCall() - key is not defined yet.
    $communication_setting = $this
      ->request('POST', '/agent-api/subscription/communication', $data);
    if ($communication_setting) {
      $crypt_pass = new CryptConnector($communication_setting['algorithm'], $password, $communication_setting['hash_setting'], $communication_setting['extra_md5']);
      $pass = $crypt_pass
        ->cryptPass();
      $body = [
        'email' => $email,
        'pass' => $pass,
        'rpc_version' => ACQUIA_CONNECTOR_ACQUIA_SPI_DATA_VERSION,
      ];
      $authenticator = $this
        ->buildAuthenticator($pass, \Drupal::time()
        ->getRequestTime(), [
        'rpc_version' => ACQUIA_CONNECTOR_ACQUIA_SPI_DATA_VERSION,
      ]);
      $data = [
        'body' => $body,
        'authenticator' => $authenticator,
      ];

      // Don't use nspiCall() - key is not defined yet.
      $response = $this
        ->request('POST', '/agent-api/subscription/credentials', $data);
      if ($response['body']) {
        return $response['body'];
      }
    }
    return FALSE;
  }

  /**
   * Get Acquia subscription from Acquia.
   *
   * @param string $id
   *   Acquia Subscription ID.
   * @param string $key
   *   Acquia Subscription key.
   * @param array $body
   *   Optional.
   *
   * @return array|false
   *   Acquia Subscription array or FALSE.
   *
   * @throws \Exception
   */
  public function getSubscription($id, $key, array $body = []) {
    $body['identifier'] = $id;

    // There is an identifier and key, so attempt communication.
    $subscription = [];
    $this->state
      ->set('acquia_subscription_data.timestamp', \Drupal::time()
      ->getRequestTime());

    // Include version number information.
    acquia_connector_load_versions();
    if (IS_ACQUIA_DRUPAL) {
      $body['version'] = ACQUIA_DRUPAL_VERSION;
      $body['series'] = ACQUIA_DRUPAL_SERIES;
      $body['branch'] = ACQUIA_DRUPAL_BRANCH;
      $body['revision'] = ACQUIA_DRUPAL_REVISION;
    }
    if ($search_info = $this
      ->getSearchModulesData()) {
      $body['search_version'] = $search_info;
    }
    try {
      $response = $this
        ->nspiCall('/agent-api/subscription', $body);
      if (!empty($response['result']['authenticator']) && $this
        ->validateResponse($key, $response['result'], $response['authenticator'])) {
        $subscription += $response['result']['body'];

        // Subscription activated.
        if (is_numeric($this->state
          ->get('acquia_subscription_data')) && is_array($response['result']['body'])) {
          $this->state
            ->set('acquia_subscription_data', $subscription);
        }
        return $subscription;
      }
    } catch (ConnectorException $e) {
      $this
        ->messenger()
        ->addError($this
        ->t('Error occurred while retrieving Acquia subscription information. See logs for details.'));
      if ($e
        ->isCustomized()) {
        $this
          ->getLogger('acquia connector')
          ->error($e
          ->getCustomMessage() . '. Response data: @data', [
          '@data' => json_encode($e
            ->getAllCustomMessages()),
        ]);
      }
      else {
        $this
          ->getLogger('acquia connector')
          ->error($e
          ->getMessage());
      }
      throw $e;
    }
    return FALSE;
  }

  /**
   * Get information on Acquia Search modules.
   *
   * @return array|null
   *   Versions for enabled search modules, NULL otherwise.
   */
  protected function getSearchModulesData() : ?array {

    // This is the only search module compatible with this version of Acquia
    // Connector for now.
    if (!\Drupal::moduleHandler()
      ->moduleExists('acquia_search')) {
      return NULL;
    }

    // Include Acquia Search Solr for Search API module version number.
    $modules = [
      'acquia_search',
      'search_api_solr',
    ];
    $result = [];
    foreach ($modules as $name) {
      $extension_list = \Drupal::service('extension.list.module');
      $info = $extension_list
        ->getExtensionInfo($name);

      // Send the version, or at least the core compatibility as a fallback.
      $result[$name] = isset($info['version']) ? (string) $info['version'] : (string) $info['core_version_requirement'];
    }
    return $result;
  }

  /**
   * Get Acquia subscription from Acquia.
   *
   * @param string $id
   *   Acquia Subscription ID.
   * @param string $key
   *   Acquia Subscription key.
   * @param array $body
   *   Optional.
   *
   * @return array|false
   *   Response result or FALSE.
   */
  public function sendNspi($id, $key, array $body = []) {
    $body['identifier'] = $id;
    try {
      $response = $this
        ->nspiCall('/spi-api/site', $body);
      if (!empty($response['result']['authenticator']) && $this
        ->validateResponse($key, $response['result'], $response['authenticator'])) {
        return $response['result'];
      }
    } catch (ConnectorException $e) {
      $this
        ->getLogger('acquia connector')
        ->error('Error: ' . $e
        ->getCustomMessage());
    }
    return FALSE;
  }

  /**
   * Get SPI definition.
   *
   * @param string $apiEndpoint
   *   API endpoint.
   *
   * @return array|bool
   *   Definition array or FALSE.
   */
  public function getDefinition($apiEndpoint) {
    try {
      return $this
        ->request('GET', $apiEndpoint, []);
    } catch (ConnectorException $e) {
      $this
        ->getLogger('acquia connector')
        ->error($e
        ->getCustomMessage());
    }
    return FALSE;
  }

  /**
   * Validate the response authenticator.
   *
   * @param string $key
   *   Acquia Subscription key.
   * @param array $response
   *   Response.
   * @param array $requestAuthenticator
   *   Authenticator array.
   *
   * @return bool
   *   TRUE if valid response, FALSE otherwise.
   */
  protected function validateResponse($key, array $response, array $requestAuthenticator) {
    $responseAuthenticator = $response['authenticator'];
    if (!($requestAuthenticator['nonce'] === $responseAuthenticator['nonce'] && $requestAuthenticator['time'] < $responseAuthenticator['time'])) {
      return FALSE;
    }
    $hash = $this
      ->hash($key, $responseAuthenticator['time'], $responseAuthenticator['nonce']);
    return $hash === $responseAuthenticator['hash'];
  }

  /**
   * Create and send a request.
   *
   * @param string $method
   *   Method to call.
   * @param string $path
   *   Path to call.
   * @param array $data
   *   Data to send.
   *
   * @return array|false
   *   Response array or FALSE.
   *
   * @throws ConnectorException
   */
  protected function request($method, $path, array $data) {
    $uri = $this->server . $path;
    $options = [
      'headers' => $this->headers,
      'body' => Json::encode($data),
    ];
    try {
      switch ($method) {
        case 'GET':
          $response = $this->client
            ->get($uri);
          $status_code = $response
            ->getStatusCode();
          $stream_size = $response
            ->getBody()
            ->getSize();
          $data = Json::decode($response
            ->getBody()
            ->read($stream_size));
          if ($status_code < 200 || $status_code > 299) {
            throw new ConnectorException($data['message'], $data['code'], $data);
          }
          return $data;
        case 'POST':
          $response = $this->client
            ->post($uri, $options);
          $status_code = $response
            ->getStatusCode();
          $stream_size = $response
            ->getBody()
            ->getSize();
          $data = Json::decode($response
            ->getBody()
            ->read($stream_size));
          if ($status_code < 200 || $status_code > 299) {
            throw new ConnectorException($data['message'], $data['code'], $data);
          }
          return $data;
      }
    } catch (RequestException $e) {
      throw new ConnectorException($e
        ->getMessage(), $e
        ->getCode());
    }
    return FALSE;
  }

  /**
   * Build authenticator to sign requests to the Acquia.
   *
   * @param string $key
   *   Secret key to use for signing the request.
   * @param int $request_time
   *   Such as from \Drupal::time()->getRequestTime().
   * @param array $params
   *   Optional parameters to include.
   *   'identifier' - Network Identifier.
   *
   * @return array
   *   Authenticator array.
   */
  protected function buildAuthenticator($key, int $request_time, array $params = []) {
    $authenticator = [];
    if (isset($params['identifier'])) {

      // Put Subscription ID in authenticator but do not use in hash.
      $authenticator['identifier'] = $params['identifier'];
      unset($params['identifier']);
    }
    $nonce = $this
      ->getNonce();
    $authenticator['time'] = $request_time;
    $authenticator['hash'] = $this
      ->hash($key, $request_time, $nonce);
    $authenticator['nonce'] = $nonce;
    return $authenticator;
  }

  /**
   * Calculates a HMAC-SHA1 according to RFC2104.
   *
   * @param string $key
   *   Key.
   * @param int $time
   *   Timestamp.
   * @param string $nonce
   *   Nonce.
   *
   * @return string
   *   HMAC-SHA1 hash.
   *
   * @see http://www.ietf.org/rfc/rfc2104.txt
   */
  protected function hash($key, $time, $nonce) {
    $string = $time . ':' . $nonce;
    return CryptConnector::acquiaHash($key, $string);
  }

  /**
   * Get a random base 64 encoded string.
   *
   * @return string
   *   Random base 64 encoded string.
   */
  protected function getNonce() {
    return Crypt::hashBase64(uniqid(mt_rand(), TRUE) . random_bytes(55));
  }

  /**
   * Prepare and send a REST request to Acquia with an authenticator.
   *
   * @param string $method
   *   HTTP method.
   * @param array $params
   *   Parameters to pass to the NSPI.
   * @param string $key
   *   Acquia Key or NULL.
   *
   * @return array
   *   NSPI response.
   *
   * @throws ConnectorException
   */
  public function nspiCall($method, array $params, $key = NULL) {
    if (empty($key)) {
      $storage = new Storage();
      $key = $storage
        ->getKey();
    }

    // Used in HMAC validation.
    $params['rpc_version'] = ACQUIA_CONNECTOR_ACQUIA_SPI_DATA_VERSION;
    $ip = \Drupal::request()->server
      ->get('SERVER_ADDR', '');
    $host = \Drupal::request()->server
      ->get('HTTP_HOST', '');
    $ssl = \Drupal::request()
      ->isSecure();
    $data = [
      'authenticator' => $this
        ->buildAuthenticator($key, \Drupal::time()
        ->getRequestTime(), $params),
      'ip' => $ip,
      'host' => $host,
      'ssl' => $ssl,
      'body' => $params,
    ];
    $data['result'] = $this
      ->request('POST', $method, $data);
    return $data;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
Client::$client protected property The HTTP client to fetch the feed data with.
Client::$config protected property Config Factory Interface.
Client::$configFactory protected property The config factory.
Client::$headers protected property Request headers.
Client::$server protected property Acquia SPI server.
Client::$state protected property The state service.
Client::buildAuthenticator protected function Build authenticator to sign requests to the Acquia. 1
Client::getDefinition public function Get SPI definition.
Client::getNonce protected function Get a random base 64 encoded string.
Client::getSearchModulesData protected function Get information on Acquia Search modules.
Client::getSubscription public function Get Acquia subscription from Acquia.
Client::getSubscriptionCredentials public function Get account settings to use for creating request authorizations.
Client::hash protected function Calculates a HMAC-SHA1 according to RFC2104.
Client::nspiCall public function Prepare and send a REST request to Acquia with an authenticator.
Client::request protected function Create and send a request.
Client::sendNspi public function Get Acquia subscription from Acquia.
Client::validateResponse protected function Validate the response authenticator.
Client::__construct public function Client constructor. 1
LoggerChannelTrait::$loggerFactory protected property The logger channel factory service.
LoggerChannelTrait::getLogger protected function Gets the logger for a specific channel.
LoggerChannelTrait::setLoggerFactory public function Injects the logger channel factory.
MessengerTrait::$messenger protected property The messenger. 29
MessengerTrait::messenger public function Gets the messenger. 29
MessengerTrait::setMessenger public function Sets the messenger.
StringTranslationTrait::$stringTranslation protected property The string translation service. 1
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.