SearchSubscriber.php in Acquia Connector 8
Namespace
Drupal\acquia_search\EventSubscriberFile
acquia_search/src/EventSubscriber/SearchSubscriber.phpView source
<?php
namespace Drupal\acquia_search\EventSubscriber;
use Drupal\acquia_connector\CryptConnector;
use Drupal\acquia_connector\Helper\Storage;
use Drupal\Component\Utility\Crypt;
use Drupal\search_api_solr\Solarium\EventDispatcher\EventProxy;
use Solarium\Core\Client\Adapter\AdapterHelper;
use Solarium\Core\Client\Response;
use Solarium\Core\Event\Events;
use Solarium\Core\Plugin\AbstractPlugin;
use Solarium\Exception\HttpException;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Extends Solarium plugin: authenticate, etc.
*/
class SearchSubscriber extends AbstractPlugin implements EventSubscriberInterface {
/**
* Solarium client.
*
* @var \Solarium\Core\Client\Client
*/
protected $client;
/**
* Array of derived keys, keyed by environment id.
*
* @var array
*/
protected $derivedKey = [];
/**
* Nonce.
*
* @var string
*/
protected $nonce = '';
/**
* URI.
*
* @var string
*/
protected $uri = '';
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
return [
Events::PRE_EXECUTE_REQUEST => 'preExecuteRequest',
Events::POST_EXECUTE_REQUEST => 'postExecuteRequest',
];
}
/**
* Build Acquia Search Solr Authenticator.
*
* @param \Drupal\search_api_solr\Solarium\EventDispatcher\EventProxy $event
* PreExecuteRequest event.
*/
public function preExecuteRequest(EventProxy $event) {
// If no client exists, its not an acquia solr server.
if (!$this->client) {
return;
}
/** @var \Solarium\Core\Client\Request $request */
$request = $event
->getRequest();
$request
->addParam('request_id', uniqid(), TRUE);
// If we're hosted on Acquia, and have an Acquia request ID,
// append it to the request so that we map Solr queries to Acquia search
// requests.
if (isset($_ENV['HTTP_X_REQUEST_ID'])) {
$xid = empty($_ENV['HTTP_X_REQUEST_ID']) ? '-' : $_ENV['HTTP_X_REQUEST_ID'];
$request
->addParam('x-request-id', $xid);
}
$endpoint = $this->client
->getEndpoint();
$this->uri = AdapterHelper::buildUri($request, $endpoint);
$this->nonce = Crypt::randomBytesBase64(24);
$raw_post_data = $request
->getRawData();
// We don't have any raw POST data for pings only.
if (!$raw_post_data) {
$parsed_url = parse_url($this->uri);
$path = $parsed_url['path'] ?? '/';
$query = isset($parsed_url['query']) ? '?' . $parsed_url['query'] : '';
$raw_post_data = $path . $query;
}
$cookie = $this
->calculateAuthCookie($raw_post_data, $this->nonce, time());
$request
->addHeader('Cookie: ' . $cookie);
$request
->addHeader('User-Agent: ' . 'acquia_search/' . \Drupal::config('acquia_search.settings')
->get('version'));
}
/**
* Validate response.
*
* @param \Drupal\search_api_solr\Solarium\EventDispatcher\EventProxy $event
* postExecuteRequest event.
*
* @throws \Solarium\Exception\HttpException
*/
public function postExecuteRequest(EventProxy $event) {
// If no client exists, its not an acquia solr server.
if (!$this->client) {
return;
}
$response = $event
->getResponse();
if ($response
->getStatusCode() != 200) {
throw new HttpException($response
->getStatusMessage());
}
if ($event
->getRequest()
->getHandler() == 'admin/ping') {
return;
}
$this
->authenticateResponse($event
->getResponse(), $this->nonce, $this->uri);
}
/**
* Validate the hmac for the response body.
*
* @param \Solarium\Core\Client\Response $response
* Solarium Response.
* @param string $nonce
* Nonce.
* @param string $url
* Url.
*
* @return \Solarium\Core\Client\Response
* Solarium Response.
*
* @throws \Solarium\Exception\HttpException
*/
protected function authenticateResponse(Response $response, $nonce, $url) {
$hmac = $this
->extractHmac($response
->getHeaders());
if (!$this
->validateResponse($hmac, $nonce, $response
->getBody())) {
throw new HttpException('Authentication of search content failed url: ' . $url);
}
return $response;
}
/**
* Look in the headers and get the hmac_digest out.
*
* @param mixed $headers
* Headers array.
*
* @return string
* Hmac_digest or empty string.
*/
public function extractHmac($headers) {
$reg = [];
if (is_array($headers)) {
foreach ($headers as $value) {
if (stristr($value, 'pragma') && preg_match("/hmac_digest=([^;]+);/i", $value, $reg)) {
return trim($reg[1]);
}
}
}
return '';
}
/**
* Validate the authenticity of returned data using a nonce and HMAC-SHA1.
*
* @param string $hmac
* HMAC.
* @param string $nonce
* Nonce.
* @param string $string
* Data string.
* @param string $derived_key
* Derived key.
* @param string $env_id
* Environment Id.
*
* @return bool
* TRUE if response is valid.
*/
public function validateResponse($hmac, $nonce, $string, $derived_key = NULL, $env_id = NULL) {
if (empty($derived_key)) {
$derived_key = $this
->getDerivedKey($env_id);
}
return $hmac == hash_hmac('sha1', $nonce . $string, $derived_key);
}
/**
* Get the derived key.
*
* Get the derived key for the solr hmac using the information shared with
* acquia.com.
*
* @param string $env_id
* Environment Id.
*
* @return string
* Derived Key.
*/
public function getDerivedKey($env_id = NULL) {
if (empty($env_id)) {
$env_id = $this->client
->getEndpoint()
->getKey();
}
// Get derived key for search v3 core if enabled.
$search_v3_enabled = \Drupal::config('acquia_search.settings')
->get('search_v3_enabled');
if ($search_v3_enabled) {
$search_v3_index = $this
->getSearchV3IndexKeys();
if ($search_v3_index) {
$this->derivedKey[$env_id] = CryptConnector::createDerivedKey($search_v3_index['product_policies']['salt'], $search_v3_index['key'], $search_v3_index['secret_key']);
return $this->derivedKey[$env_id];
}
}
if (!isset($this->derivedKey[$env_id])) {
$server = $this->client
->getEndpoint();
// If derived_key comes from configuration, use that.
// @todo make sure the derived_key doesnt make it permanently into the DB.
if (!empty($server
->getOption('derived_key'))) {
return $server
->getOption('derived_key');
}
$acquia_index_id = $server
->getOption('index_id');
$storage = new Storage();
$key = $storage
->getKey();
// See if we need to overwrite these values.
// @todo Fix Implement the derived key per solr environment storage.
// In any case, this is equal for all subscriptions. Also
// even if the search sub is different, the main subscription should be
// active.
$derived_key_salt = $this
->getDerivedKeySalt();
// We use a salt from acquia.com in key derivation since this is a shared
// value that we could change on the AN side if needed to force any
// or all clients to use a new derived key. We also use a string
// ('solr') specific to the service, since we want each service using a
// derived key to have a separate one.
if (empty($derived_key_salt) || empty($key) || empty($acquia_index_id)) {
// Expired or invalid subscription - don't continue.
$this->derivedKey[$env_id] = '';
}
elseif (!isset($this->derivedKey[$env_id])) {
$this->derivedKey[$env_id] = CryptConnector::createDerivedKey($derived_key_salt, $acquia_index_id, $key);
}
}
return $this->derivedKey[$env_id];
}
/**
* Returns the subscription's salt used to generate the derived key.
*
* The salt is stored in a system variable so that this module can continue
* connecting to Acquia Search even when the subscription data is not
* available.
* The most common reason for subscription data being unavailable is a failed
* heartbeat connection to rpc.acquia.com.
*
* Acquia Connector versions <= 7.x-2.7 pulled the derived key salt directly
* from the subscription data. In order to allow for seamless upgrades, this
* function checks whether the system variable exists and sets it with the
* data in the subscription if it doesn't.
*
* @return string
* The derived key salt.
*
* @see http://drupal.org/node/1784114
*/
public function getDerivedKeySalt() {
$salt = \Drupal::config('acquia_search.settings')
->get('derived_key_salt');
if (!$salt) {
// If the variable doesn't exist, set it using the subscription data.
$subscription = \Drupal::state()
->get('acquia_subscription_data');
if (isset($subscription['derived_key_salt'])) {
\Drupal::configFactory()
->getEditable('acquia_search.settings')
->set('derived_key_salt', $subscription['derived_key_salt'])
->save();
$salt = $subscription['derived_key_salt'];
}
}
return $salt;
}
/**
* Creates an authenticator based on a data string and HMAC-SHA1.
*
* @param string $string
* Data string.
* @param string $nonce
* Nonce.
* @param int $time
* Request time.
* @param string $derived_key
* Derived key.
* @param string $env_id
* Environment Id.
*
* @return string
* Auth cookie string.
*/
public function calculateAuthCookie($string, $nonce, $time, $derived_key = NULL, $env_id = NULL) {
if (empty($derived_key)) {
$derived_key = $this
->getDerivedKey($env_id);
}
if (empty($derived_key)) {
// Expired or invalid subscription - don't continue.
return '';
}
else {
return 'acquia_solr_time=' . $time . '; acquia_solr_nonce=' . $nonce . '; acquia_solr_hmac=' . hash_hmac('sha1', $time . $nonce . $string, $derived_key) . ';';
}
}
/**
* Fetches the search v3 index keys.
*
* @return array|null
* Search v3 index keys, NULL if unavailable.
*/
public function getSearchV3IndexKeys() {
$core_service = acquia_search_get_core_service();
if (!$core_service
->isPreferredCoreAvailable()) {
return;
}
$core = $core_service
->getPreferredCore();
// Check the core version to see if it's v2 or v3 core.
if (empty($core['version']) || $core['version'] !== 'v3') {
return;
}
$search_v3_client = acquia_search_get_v3_client();
if (!$search_v3_client) {
return;
}
$storage = new Storage();
$acquia_identifier = $storage
->getIdentifier();
$search_v3_index = $search_v3_client
->getKeys($core['core_id'], $acquia_identifier);
if (is_array($search_v3_index) && !empty($search_v3_index)) {
return $search_v3_index;
}
}
}
Classes
Name | Description |
---|---|
SearchSubscriber | Extends Solarium plugin: authenticate, etc. |