View source
<?php
namespace Drupal\acquia_search\EventSubscriber;
use Drupal\acquia_search\AcquiaCryptConnector;
use Drupal\acquia_search\Helper\Flood;
use Drupal\acquia_search\Helper\Runtime;
use Drupal\acquia_search\Helper\Storage;
use Drupal\Component\Utility\Crypt;
use Solarium\Core\Client\Adapter\AdapterHelper;
use Solarium\Core\Client\Client;
use Solarium\Core\Client\Response;
use Solarium\Core\Event\Events;
use Solarium\Core\Event\PostExecuteRequest;
use Solarium\Core\Event\PreExecuteRequest;
use Solarium\Core\Plugin\AbstractPlugin;
use Solarium\Exception\HttpException;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class SearchSubscriber extends AbstractPlugin implements EventSubscriberInterface {
protected $client;
protected $derivedKey = [];
protected $nonce = '';
protected $uri = '';
public static function getSubscribedEvents() {
return [
Events::PRE_EXECUTE_REQUEST => 'preExecuteRequest',
Events::POST_EXECUTE_REQUEST => 'postExecuteRequest',
];
}
public function preExecuteRequest(PreExecuteRequest $event) {
$request = $event
->getRequest();
if (!$this->client instanceof Client) {
return;
}
if (!Flood::isAllowed($request
->getHandler())) {
$message = 'The Acquia Search flood control mechanism has blocked a Solr query due to API usage limits. You should retry in a few seconds. Contact the site administrator if this message persists.';
\Drupal::messenger()
->addError($message);
$response = new Response($message, [
'HTTP/1.1 429 Too Many Requests',
]);
$event
->setResponse($response);
$event
->stopPropagation();
return;
}
$request
->addParam('request_id', uniqid(), TRUE);
if ($request
->getFileUpload()) {
$helper = new AdapterHelper();
$body = $helper
->buildUploadBodyFromRequest($request);
$request
->setRawData($body);
}
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, TRUE);
}
$endpoint = $this->client
->getEndpoint();
$this->uri = AdapterHelper::buildUri($request, $endpoint);
$this->nonce = Crypt::randomBytesBase64(24);
$raw_post_data = $request
->getRawData();
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);
$request
->addHeader('Cookie: ' . $cookie);
$request
->addHeader('User-Agent: ' . 'acquia_search/' . Storage::getVersion());
}
public function postExecuteRequest(PostExecuteRequest $event) {
if (!$this->client instanceof Client) {
return;
}
$response = $event
->getResponse();
if ($response
->getStatusCode() != 200) {
throw new HttpException($response
->getStatusMessage(), $response
->getStatusCode(), $response
->getBody());
}
if ($event
->getRequest()
->getHandler() == 'admin/ping') {
return;
}
$this
->authenticateResponse($event
->getResponse(), $this->nonce, $this->uri);
}
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;
}
public function extractHmac(array $headers) : string {
$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 '';
}
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);
}
public function getDerivedKey($env_id = NULL) : ?string {
if (empty($env_id)) {
$env_id = $this->client
->getEndpoint()
->getKey();
}
$search_v3_index = $this
->getSearchIndexKeys();
if ($search_v3_index) {
$this->derivedKey[$env_id] = AcquiaCryptConnector::createDerivedKey($search_v3_index['product_policies']['salt'], $search_v3_index['key'], $search_v3_index['secret_key']);
return $this->derivedKey[$env_id];
}
return NULL;
}
public function calculateAuthCookie($string, $nonce, $derived_key = NULL, $env_id = NULL) {
if (empty($derived_key)) {
$derived_key = $this
->getDerivedKey($env_id);
}
if (empty($derived_key)) {
return '';
}
$time = time();
$hmac = hash_hmac('sha1', $time . $nonce . $string, $derived_key);
return sprintf('acquia_solr_time=%s; acquia_solr_nonce=%s; acquia_solr_hmac=%s;', $time, $nonce, $hmac);
}
public function getSearchIndexKeys() : ?array {
$core_service = Runtime::getPreferredSearchCoreService();
if (!$core_service
->isPreferredCoreAvailable()) {
return NULL;
}
return $core_service
->getPreferredCore()['data'];
}
}