|
| 1 | +<?php |
| 2 | + |
| 3 | +declare(strict_types=1); |
| 4 | + |
| 5 | +// Docs |
| 6 | +//https://docs.sendgrid.com/for-developers/tracking-events/event |
| 7 | +//Config |
| 8 | +//https://app.sendgrid.com/settings/mail_settings |
| 9 | +//Key Verification |
| 10 | +//https://docs.sendgrid.com/for-developers/tracking-events/event#security-features |
| 11 | + |
| 12 | +/** |
| 13 | + * test |
| 14 | + * curl -X POST http://localhost:8765/send-grid/webhooks -H 'Content-Type: application/json' -d '[{"timestamp": 1700762652, "event": "processed", "sg_message_id": "14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0"}]' |
| 15 | + * |
| 16 | + * security test |
| 17 | + * curl -X POST http://localhost:8765/send-grid/webhooks -H 'Content-Type: application/json' -H 'X-Twilio-Email-Event-Webhook-Signature: MFk..........2mg==' -d '[{"timestamp": 1700762652, "event": "processed", "sg_message_id": "14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0"}]' |
| 18 | + * |
| 19 | + * |
| 20 | + */ |
| 21 | + |
| 22 | + |
| 23 | +namespace SendGrid\Controller; |
| 24 | + |
| 25 | +use SendGrid\Controller\AppController; |
| 26 | + |
| 27 | +use Cake\View\JsonView; |
| 28 | +use Cake\Core\Configure; |
| 29 | +use Cake\I18n\DateTime; |
| 30 | +use Cake\Event\EventInterface; |
| 31 | +use Cake\Log\Log; |
| 32 | +use SendGrid\Util\EllipticCurve\Ecdsa; |
| 33 | +use SendGrid\Util\EllipticCurve\PublicKey; |
| 34 | +use SendGrid\Util\EllipticCurve\Signature; |
| 35 | + |
| 36 | +class WebHooksController extends AppController |
| 37 | +{ |
| 38 | + |
| 39 | + const SIGNATURE = "X-Twilio-Email-Event-Webhook-Signature"; |
| 40 | + const TIMESTAMP = "X-Twilio-Email-Event-Webhook-Timestamp"; |
| 41 | + /** |
| 42 | + * beforeFilter callback. |
| 43 | + * |
| 44 | + * @param \Cake\Event\EventInterface<\Cake\Controller\Controller> $event Event. |
| 45 | + * @return \Cake\Http\Response|null|void |
| 46 | + */ |
| 47 | + public function beforeFilter(EventInterface $event) |
| 48 | + { |
| 49 | + // If the authentication plugin is loaded then open up the index action |
| 50 | + if (isset($this->Authentication)) { |
| 51 | + $this->Authentication->allowUnauthenticated(['index']); |
| 52 | + } |
| 53 | + } |
| 54 | + |
| 55 | + public function viewClasses(): array |
| 56 | + { |
| 57 | + return [JsonView::class]; |
| 58 | + } |
| 59 | + |
| 60 | + |
| 61 | + public function index() |
| 62 | + { |
| 63 | + $this->request->allowMethod(['post']); |
| 64 | + $this->viewBuilder()->setClassName("Json"); |
| 65 | + $result = $this->request->getData(); |
| 66 | + |
| 67 | + $this->set('result', $result); |
| 68 | + $config = Configure::read('sendgridWebhook'); |
| 69 | + |
| 70 | + if (isset($config['debug']) && $config['debug'] == 'true') { |
| 71 | + Log::debug(json_encode($result)); |
| 72 | + Log::debug(json_encode($this->request->getHeaders())); |
| 73 | + } |
| 74 | + |
| 75 | + if (isset($config['secure']) && $config['secure'] == 'true') { |
| 76 | + $this->request->getBody()->rewind(); |
| 77 | + $payload = $this->request->getBody()->getContents(); |
| 78 | + // Log::debug($payload); |
| 79 | + |
| 80 | + if (!isset($config['verification_key'])) { |
| 81 | + if (isset($config['debug']) && $config['debug'] == 'true') { |
| 82 | + Log::debug("Verfication Failed: Webhook Signature verification key not set in app_local.php"); |
| 83 | + } |
| 84 | + $this->set('error', "Invalid Signature"); |
| 85 | + $this->viewBuilder()->setOption('serialize', "error"); |
| 86 | + return; |
| 87 | + } |
| 88 | + |
| 89 | + $publicKey = PublicKey::fromString($config['verification_key']); |
| 90 | + |
| 91 | + $timestampedPayload = $this->request->getHeaderLine($this::TIMESTAMP) . $payload; |
| 92 | + $decodedSignature = Signature::fromBase64($this->request->getHeaderLine($this::SIGNATURE)); |
| 93 | + |
| 94 | + if (!Ecdsa::verify($timestampedPayload, $decodedSignature, $publicKey)) { |
| 95 | + if (isset($config['debug']) && $config['debug'] == 'true') { |
| 96 | + Log::debug("Verfication Failed: Webhook Signature does not verify against the verification key"); |
| 97 | + } |
| 98 | + $this->set('error', "Invalid Signature"); |
| 99 | + $this->viewBuilder()->setOption('serialize', "error"); |
| 100 | + return; |
| 101 | + } |
| 102 | + } |
| 103 | + |
| 104 | + $emailTable = $this->getTableLocator()->get($config['tableClass']); |
| 105 | + $count = 0; |
| 106 | + foreach ($result as $event) { |
| 107 | + $message_id = explode(".", $event['sg_message_id'])[0]; |
| 108 | + $email_record = $emailTable->find('all')->select([ |
| 109 | + "id", |
| 110 | + $config['uniqueIdField'], |
| 111 | + $config['statusField'], |
| 112 | + $config['statusMessageField'], |
| 113 | + ])->where([$config['uniqueIdField'] => $message_id])->first(); |
| 114 | + if ($email_record) { |
| 115 | + $count++; |
| 116 | + $email_record->set($config['statusMessageField'], $email_record->get($config['statusMessageField']) . (new DateTime())->format('d/m/Y H:i:s') . ": " . $event['event'] . " " . |
| 117 | + (isset($event['response']) ? $event['response'] : "") . " " . |
| 118 | + (isset($event['reason']) ? $event['reason'] : "<br>")); |
| 119 | + $email_record->set($config['statusField'], $event['event']); |
| 120 | + $emailTable->save($email_record); |
| 121 | + } |
| 122 | + } |
| 123 | + if (isset($config['debug']) && $config['debug'] == 'true') { |
| 124 | + Log::debug("Updated $count Email records"); |
| 125 | + } |
| 126 | + $this->set('OK', "OK"); |
| 127 | + $this->viewBuilder()->setOption('serialize', "OK"); |
| 128 | + } |
| 129 | + |
| 130 | +} |
0 commit comments