Publi

Recibir notificaciones de Amazon SNS y procesarlas automáticamente

Una gran herramienta que nos brinda Amazon, y que podemos combinar con otros servicios, es SNS (Simple Notification Service), básicamente este servicio se encarga de enviarnos una notificación cuando ocurre un evento dentro de los servicios contratados.
Un ejemplo muy sencillo para manejar esto, es la gestión de quejas y rebotes de los envíos de correo de Amazon SES (Simple Email Service). Cuando se envía un e-mail con este servicio de correo, es posible monitorizar los mensajes que no han llegado (y por tanto se ha recibido un e-mail de notificación), o por ejemplo, los e-mails cuyos destinatarios han establecido como correo basura y han notificado al servidor de origen.

Para esto, podemos configurar, desde Amazon, para que éste nos notifique mediante SNS, aquellos e-mails rebotados o con queja por parte del destinatario, ahorrándonos la tarea de tener que configurar un servicio de recepción de correo que discrimine ese tipo de mensajes y extraiga información de ellos.

Las formas que tienen Amazon para notificarnos, pueden ser por HTTP, Email o SMS entre otros métodos. Vamos a aprovechar el primer método (HTTP/HTTPS) para crear un script que reciba la información por parte de Amazon y la procese, en la URL que queramos (no tiene por qué ser de Amazon).

Cuando lo configuramos, justo después de dar de alta la URL donde queremos que nos lleguen las notificaciones, Amazon nos enviará una notificación que debemos responder (nuestro script se encargará de responderla automáticamente), cuando ya esté dada de alta, ya podemos empezar a recibir notificaciones.

La información se enviará a nuestra entrada estándar, como el ejemplo es en php, lo leeremos desde php://stdin por lo que en principio podemos hacer una pequeña prueba con un script como este:

1
2
3
<?php
file_put_contents('entrada', file_get_contents('php://stdin'));
?>

Probamos enviando una notificación ( o una solicitud de alta de notificaciones) a la URL donde hemos subido el script anterior, para ver lo que recibimos, será un stream en JSON con el contenido de la notificación.

Pero nos encontramos ante un primer problema, la URL es pública, y cualquier persona podrá entrar en ella, por lo que, lo primero que tenemos que hacer es asegurarnos de que quien intenta acceder a esta URL es Amazon, para ello, siempre que nos mandan un mensaje, nos mandarán una firma de dicho mensaje, y un certificado con el cual se genera la firma. Podemos encontrar documentación sobre el tema en los foros de Amazon AWS.

A continuación os paso mi script para verificar que el mensaje viene de Amazon en realidad:
AmazonSns.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<?php
class AmazonSns
{
  public static function verify($inputJson, $account, $region, $topics)
  {
    // Probamos si la región es la que indicamos, si la cuenta a la que va dirigido es la nuestra
    // y si el topic es uno de los que hemos indicado nosotros
    $topicarn = explode(':', $inputJson->TopicArn);
    $_region = $topicarn[3];
    $_account = $topicarn[4];
    $_topic = $topicarn[5];

    if ( ($region != $_region) || ($account != $_account) || (!in_array($_topic, $topics)) )
    return false;

    // Miramos que la URL del certificado pertenece a AmazonAWS
    if(!self::endswith(parse_url($inputJson->SigningCertURL, PHP_URL_HOST), '.amazonaws.com'))
      return false;

    // Descargamos el certificado y extraemos la clave pública
    $cert = file_get_contents($inputJson->SigningCertURL);
    $pubkey = openssl_get_publickey($cert);
    if(!$pubkey)
      return false;

    // Esto nos sirve para generar la cadena de verificación del certificado
    $validationGeneration = array('Notification' => array('Message', 'MessageId', 'Subject', 'Timestamp', 'TopicArn', 'Type'),
                  'SubscriptionConfirmation' => array('Message', 'MessageId', 'SubscribeURL', 'Timestamp', 'Token', 'TopicArn', 'Type'));

    $text='';
    $valid = (isset($validationGeneration[$inputJson->Type]))?$validationGeneration[$inputJson->Type]:false;

    if (!$valid)
      return false;

    foreach ($valid as $t) {
      if ( (isset($inputJson->{$t})) && ($inputJson->{$t}) ) {
    $text.=$t."\n".$inputJson->{$t}."\n";
      }
    }

    // Decodificamos la firma
    $signature = base64_decode($inputJson->Signature);

    // Miramos si el mensaje se corresponde
    if(openssl_verify($text, $signature, $pubkey, OPENSSL_ALGO_SHA1))
      return true;

    return false;
  }

  private static function endswith($string, $test)
  {
    $strlen = strlen($string);
    $testlen = strlen($test);
    if ($testlen > $strlen) return false;
    return substr_compare($string, $test, -$testlen) === 0;
  }
}
?>

Amazontest.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php
require_once('AmazonSns.php');

try {
  $contents = file_get_contents('php://stdin');
  if (!$contents)
    throw new Exception('No se ha recibido información');

  $contentsJson = json_decode($contents);
  if (!$contentsJson)
    throw new Exception('La entrada no tiene un código JSON válido');

  if (!AmazonSns::verify($contentsJson,'xxxxxxxxxxxxxxxxxxxx', 'zona-amazon', array('amazon-ses-bounces', 'amazon-ses-complaints')))
    throw new Exception('Petición incorrecta');

  if ($contentsJson->Type == 'SubscriptionConfirmation')
    {
      /* Aquí podremos enviar un mensaje a un usuario para que manualmente lo acepte, tendremos 3 días para hacerlo */
      file_get_contents($contentsJson->SubscribeURL);
    }
  elseif ($contentsJson->Type == 'Notification')
    {
      /* procesaMensaje(); */
    }
}
catch (Exception $e)
{
  echo $e->getMessage();
}
?>

Donde en la función procesaMensaje() podemos llamar a cualquier función que analice el contenido del mensaje y haga lo que sea necesario con el mensaje.

Primero vemos la clase AmazonSns, aquí:

  • Verificamos que la región de donde viene el mensaje, es de donde lo esperamos
  • Verificamos que el identificador de cuenta es el nuestro
  • Verificamos que el motivo de contacto es uno de los que esperamos. Esto podríamos quitarlo, aunque es una verificación más, porque nuestro futuro script de procesamiento del mensaje, puede que no sepa qué hacer con esta notificación
  • Verificamos que el certificado viene de un dominio de amazon
  • Verifica el certificado

Dentro de Amazontest.php, lo que hacemos es decodificar el json que se nos envía en un array, y verificarlo, pero además, como primer paso, miramos si es una notificación de confirmación de suscripción y la aceptamos.
Para aceptar una suscripción, sólo tenemos que acceder a la URL que nos indica el mensaje, aunque si queremos más seguridad podemos analizar el contenido que nos envían en la confirmación, esta vez un XML.

Nota, el script que he puesto en esta página es una reducción muy grande del script que utilizo en realidad para verificar las notificaciones de Amazon, por lo que agradezco cualquier comentario si este script no funciona como debe.

Para saber más sobre cómo configurar en el panel de Amazon el tema de las quejas y los rebotes de correo electrónico de Amazon SES con Amazon SNS, os dejo un enlace con el videotutorial oficial. La voz no es muy agraciada, pero al final funciona, eso sí, tenéis que tener cuidado con la zona donde corréis el servicio SNS, porque SES no corre en todas las zonas

También podría interesarte...

There are 4 comments left Ir a comentario

  1. Pingback: Bitacoras.com /

  2. Destajador /
    Usando Google Chrome Google Chrome 30.0.1599.101 en Windows Windows 7

    Muchas gracias, me fue de gran ayuda. seria bueno que subas tu código a github 🙂

  3. Destajador /
    Usando Google Chrome Google Chrome 30.0.1599.101 en Windows Windows 7

    por cierto a mi me funciono con file_get_contents(‘php://input’);

    gracias de nuevo!!

  4. admin / Post Author
    Usando Mozilla Firefox Mozilla Firefox 24.0 en Ubuntu Linux Ubuntu Linux

    @Destajador
    Gracias, tengo algunas cosillas en github: https://github.com/blakeyed aunque todavía no tengo mucho, poco a poco irá creciendo 🙂

Leave a Reply