View source
<?php
namespace Drupal\acquia_contenthub\Controller;
use Acquia\ContentHubClient\hmacv1\ResponseSigner;
use Drupal\acquia_contenthub\Client\ClientManagerInterface;
use Drupal\acquia_contenthub\ContentHubSubscription;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class ContentHubWebhookController extends ControllerBase {
protected $loggerFactory;
protected $request;
protected $configFactory;
protected $clientManager;
protected $moduleHandler;
protected $contentHubSubscription;
protected $config;
protected $contentHubReindex;
public function __construct(LoggerChannelFactoryInterface $logger_factory, Request $current_request, ConfigFactoryInterface $config_factory, ClientManagerInterface $client_manager, ModuleHandlerInterface $module_handler, ContentHubSubscription $contenthub_subscription, ContentHubReindex $contentHubReindex) {
$this->loggerFactory = $logger_factory;
$this->request = $current_request;
$this->configFactory = $config_factory;
$this->clientManager = $client_manager;
$this->moduleHandler = $module_handler;
$this->contentHubSubscription = $contenthub_subscription;
$this->contentHubReindex = $contentHubReindex;
$this->config = $this->configFactory
->get('acquia_contenthub.admin_settings');
}
public static function create(ContainerInterface $container) {
return new static($container
->get('logger.factory'), $container
->get('request_stack')
->getCurrentRequest(), $container
->get('config.factory'), $container
->get('acquia_contenthub.client_manager'), $container
->get('module_handler'), $container
->get('acquia_contenthub.acquia_contenthub_subscription'), $container
->get('acquia_contenthub.acquia_contenthub_reindex'));
}
public function receiveWebhook() {
$webhook = $this->request
->getContent();
$response = NULL;
$logger = $this->loggerFactory
->get('acquia_contenthub');
if (!$this
->validateWebhookSignature($this->request)) {
$ip_address = $this->request
->getClientIp();
$logger
->debug('Webhook [from IP = @IP] rejected (Signatures do not match): @whook', [
'@IP' => $ip_address,
'@whook' => print_r($webhook, TRUE),
]);
return new Response('');
}
$log_msg = 'Webhook landing: ';
$context = [
'@webhook' => print_r($webhook, TRUE),
];
if ($webhook = Json::decode($webhook)) {
$log_msg .= '(Request ID: @request_id - Entity: @uuid.) ';
$context += [
'@request_id' => $webhook['requestid'] ?? 'NOT_FOUND',
'@uuid' => $webhook['uuid'],
];
if (isset($webhook['status'])) {
switch ($webhook['status']) {
case 'successful':
$response = $this
->processWebhook($webhook);
break;
case 'pending':
$response = $this
->registerWebhook($webhook);
break;
case 'shared_secret_regenerated':
$response = $this
->updateSharedSecret($webhook);
break;
default:
$response = new Response('');
break;
}
}
}
$logger
->debug($log_msg . '@webhook', $context);
return $response;
}
public function validateWebhookSignature(Request $request) {
$headers = array_map('current', $request->headers
->all());
$webhook = $request
->getContent();
$request_date = isset($headers['date']) ? $headers['date'] : "1970";
$request_timestamp = strtotime($request_date);
$timestamp = time();
if (abs($request_timestamp - $timestamp) > 900) {
$message = new FormattableMarkup('The Webhook request seems that was issued in the past [Request timestamp = @t1, server timestamp = @t2]: rejected: @whook', [
'@t1' => $request_timestamp,
'@t2' => $timestamp,
'@whook' => print_r($webhook, TRUE),
]);
$this->loggerFactory
->get('acquia_contenthub')
->debug($message);
return FALSE;
}
$authorization_header = isset($headers['authorization']) ? $headers['authorization'] : '';
$webhook_array = Json::decode($webhook);
$status = $webhook_array['status'];
$authorization = '';
switch ($status) {
case 'shared_secret_regenerated':
$this->contentHubSubscription
->getSettings();
$secret_key = $this->contentHubSubscription
->getSharedSecret();
$signature = $this->clientManager
->getRequestSignature($request, $secret_key);
$authorization = 'Acquia Webhook:' . $signature;
$this->loggerFactory
->get('acquia_contenthub')
->debug('Received Webhook for shared secret regeneration. Settings updated.');
break;
case 'successful':
case 'processing':
case 'in-queue':
case 'failed':
$secret_key = $this->contentHubSubscription
->getSharedSecret();
$signature = $this->clientManager
->getRequestSignature($request, $secret_key);
$authorization = 'Acquia Webhook:' . $signature;
break;
case 'pending':
$api = $this->config
->get('api_key');
$secret_key = $this->config
->get('secret_key');
$signature = $this->clientManager
->getRequestSignature($request, $secret_key);
$authorization = "Acquia {$api}:" . $signature;
break;
}
if ($authorization !== $authorization_header) {
$message = new FormattableMarkup('The Webhook request failed HMAC validation. [authorization = %authorization]. [authorization_header = %authorization_header]', [
'%authorization' => $authorization,
'%authorization_header' => $authorization_header,
]);
$this->loggerFactory
->get('acquia_contenthub')
->debug($message);
}
return (bool) ($authorization === $authorization_header);
}
public function processWebhook(array $webhook) {
if ($webhook['crud'] === 'reindex') {
$this
->processReindexWebhook($webhook);
return new Response('');
}
$assets = isset($webhook['assets']) ? $webhook['assets'] : FALSE;
if (count($assets) > 0) {
$this->moduleHandler
->alter('acquia_contenthub_process_webhook', $webhook);
}
else {
$message = new FormattableMarkup('Error processing Webhook (It contains no assets): @whook', [
'@whook' => print_r($webhook, TRUE),
]);
$this->loggerFactory
->get('acquia_contenthub')
->debug($message);
}
return new Response('');
}
public function registerWebhook(array $webhook) {
$uuid = isset($webhook['uuid']) ? $webhook['uuid'] : FALSE;
$origin = $this->config
->get('origin');
$api_key = $this->config
->get('api_key');
if ($uuid && $webhook['initiator'] == $origin && $webhook['publickey'] == $api_key) {
$secret = $this->config
->get('secret_key');
$response = new ResponseSigner($api_key, $secret);
$response
->setContent('{}');
$response
->setResource('');
$response
->setStatusCode(ResponseSigner::HTTP_OK);
$response
->signWithCustomHeaders(FALSE);
$response
->signResponse();
return $response;
}
else {
$ip_address = $this->request
->getClientIp();
$message = new FormattableMarkup('Webhook [from IP = @IP] rejected (initiator and/or publickey do not match local settings): @whook', [
'@IP' => $ip_address,
'@whook' => print_r($webhook, TRUE),
]);
$this->loggerFactory
->get('acquia_contenthub')
->debug($message);
return new Response('');
}
}
private function processReindexWebhook(array $webhook) {
if ($this->contentHubReindex
->isReindexSent()) {
$this->contentHubReindex
->setReindexStateFinished();
}
}
}