Perla cultivada: Perl and the Amazon cloud, Part 2

Carga segura de datos a S3 mediante formulario HTML

Esta serie de cinco partes le permitirá generar un sencillo sitio Web para compartir fotos utilizando Perl y Apache para acceder a Simple Storage Service (S3) y a SimpleDB de Amazon. En esta parte, usted aprenderá cómo cargar un archivo en S3 desde una página Web mediante un formulario HTML para minimizar la carga en el servidor manteniendo una estricta política de seguridad.

Teodor Zlatanov, Programmer, Gold Software Systems

Teodor Zlatanov graduated with an M.S. in computer engineering from Boston University in 1999. He has worked as a programmer since 1992, using Perl, Java, C, and C++. His interests are in open source work on text parsing, 3-tier client-server database architectures, UNIX system administration, CORBA, and project management.



08-04-2009

Existen diferentes maneras de cargar un archivo en Simple Storage Service (S3) de Amazon desde una página Web:

  • Desde la línea de comandos utilizando los módulos pertinentes de CPAN
  • Desde la línea de comandos utilizando los módulos pertinentes de Amazon
  • Directamente desde un formulario HTML

Saque el mejor provecho de esta serie

Esta serie requiere un conocimiento básico de HTTP y HTML, así como un conocimiento intermedio de JavaScript y Perl (dentro de un proceso de Apache mod_perl). También es útil tener un conocimiento de bases de datos relacionales, almacenamiento en disco y redes. La serie se vuelve cada vez más técnica; consulte la sección de Recursos si necesita ayuda con alguno de estos temas.

Este artículo muestra cómo cargar un archivo directamente desde un formulario HTML para, de este modo, minimizar la carga del servidor. Las cargas exitosas se almacenarán en una dirección URL personalizada que se utilizará en las últimas partes de la serie para configurar el resto de los elementos del sitio de fotos. En esta serie, usaremos share.lifelogs.com como nombre de dominio.

Cargas en S3

Es posible cargar datos en S3 mediante un formulario POST. (También es posible utilizar el método PUT HTTP y la llamada PutObject SOAP). En este artículo, emplearemos el formulario POST, ya que es sencillo y no utiliza recursos de disco (disco, CPU o ancho de banda).

Un problema grave que presentan las cargas en S3 es que los metadatos no pueden ser modificados. Esto puede deberse a la naturaleza distribuida de S3 o bien a la voluntad de Amazon de simplificar el mecanismo. Sin embargo, en los foros de S3, Amazon ha manifestado que esto puede cambiar en el futuro.

De cualquier modo, esta circunstancia significa que hay que fijar el Content-Type al cargar el contenido; de lo contrario aparecerá una desagradable secuencia de octetos/binaria. Los otros metadatos no son demasiado importantes, ya que es posible utilizar SimpleDB de Amazon para hacer un seguimiento de las cargas y almacenar allí los metadatos. Más adelante se ofrece una solución JavaScript satisfactoria para el problema de Content-Type.

El nombre de usuario será parte de la dirección URL exitosa. También se lo puede colocar en los metadatos de la carga para que permanezca asociado con el archivo; sin embargo, en ese caso, el usuario sólo podrá cambiar su nombre si de alguna manera se rehacen todas las cargas S3 para poder almacenar nuevos metadatos. Es posible mantener los ID de usuario que sean independientes del nombre de usuario y asociar el ID de usuario con el objeto S3, pero esto es innecesariamente complicado a los fines de este artículo—la generación de un sencillo sitio para compartir fotos.

Nosotros (es decir, los operadores del sitio Web) hemos configurado un sector de almacenamiento llamado images.share.lifelogs.com en S3. Este sector de almacenamiento cuenta con controles de acceso que permiten una lectura pública. Si usted tiene problemas para configurar el sector de almacenamiento a partir de la documentación de Amazon, utilice una herramienta como S3Fox, JungleDisk o alguna de las tantas interfaces de S3. Una vez alcanzado este nivel, usted tendrá las claves secreta y de acceso de AWS.

Para controlar las cargas se utilizará una directiva, que consiste en un conjunto de reglas expresadas en formato de datos JSON. La directiva será firmada con la clave secreta de Amazon.

La firma de la directiva se efectúa con el módulo Perl Digest::HMAC_SHA1. En primer lugar, usted deberá convertir la directiva en Base64, quitar todas las nuevas líneas, firmar los datos resultantes y codificar la firma en Base64; luego deberá dar tres vueltas, tocarse los dedos de los pies, arrancarse un pelo y quemarlo y, por último, enviar USD 1,28 en monedas de 1 centavo a Elle Cowalsky y esperar a que ella le envíe un correo con la firma que usted debe utilizar. ¡Mentira! Quemar el pelo es opcional. Pruebe con esto:

Listado 1. Firma de la directiva de carga
my $aws_secret_access_key = 'get it from 
 http://aws-portal.amazon.com/gp/aws/developer/account/index.html?action=access-key';

my $policy = 'entire policy here';
$policy = encode_base64($policy);
$policy =~ s/\n//g;

my $signature = encode_base64(hmac_sha1($policy, $aws_secret_access_key));

$signature =~ s/\n//g;

Directiva de carga en S3

Es necesario establecer la directiva antes de construir el formulario; esto significa que usted deberá decidir sus objetivos de seguridad y usabilidad antes de escribir HTML.

El documento de directiva es bastante simple:

Listado 2. Documento de directiva de carga
{
 "expiration": "3000-01-01T00:00:00Z",
  "conditions": [ 
    {"bucket": "images.share.lifelogs.com"}, 
    {"acl": "public-read"},
    ["starts-with", "$key", ""],
    {"success_action_redirect": "http://share.lifelogs.com/s3uploaded/$user"},
    ["content-length-range", 0, 1048576]
  ]
}

En la documentación para desarrolladores de S3 encontrará una explicación detallada de este tema (ver enlace en Recursos). El sector de almacenamiento debe ser el nombrado anteriormente, la ACL del sector de almacenamiento debe coincidir, la clave puede comenzar de cualquier forma y, si usted tiene éxito, será trasladado a una dirección URL determinada. El tamaño de los documentos cargados se encuentra limitado entre 0 bytes y 1 megabyte. Tenga en consideración la fecha de vencimiento (ampliaremos más adelante).

El contenido de la directiva estará firmado y podrá verse públicamente, por que será muy difícil falsificarlo. Este atributo convierte la directiva en un elemento de seguridad de su sitio—se asegura que las cargas en S3 estén permitidas únicamente en función de criterios específicos—. Recuerde que usted paga por el uso de S3, por lo tanto esto es importante.

La fecha de expiración se ha fijado en 3000 (sí, el año 3000). La idea es que la directiva, a efectos prácticos, no expire. También se puede fijar la fecha de expiración 10 minutos en el futuro; de esta manera, usted tendrá la seguridad de que la directiva no pueda ser utilizada por usuarios eliminados más de 10 minutos después de su último acceso legítimo. Sin embargo, los usuarios que tarden más de 10 minutos en cargar un archivo y sean rechazados se quejarán de este plazo. Por eso es importante elegir un plazo adecuado, y no fijarlo arbitrariamente.

La directiva debe tener condiciones para todos los campos especificados en el formulario. Esto evitará falsificaciones y fomentará documentos de directiva completos.

Ya hemos establecido una directiva; ahora es momento de configurar el formulario de carga.


Formulario de carga en S3

Ya hemos hablado de los metadatos de Content-Type asociados con objetos S3 y la manera de configurarlos antes de cargar el objeto. Desgraciadamente, este mecanismo no funciona bien con las cargas de imágenes ya que no se conoce de antemano qué archivos cargará el usuario. Las imágenes JPEG y PNG, por ejemplo, tienen diferentes tipos de contenido (se los conoce como tipos MIME, y literalmente existen cientos de tipos comunes).

Esta solución es un JavaScript de nivel intermedio. Debido a que se coloca todo dentro de una secuencia de comandos Perl, es necesario evitar todos los caracteres \ y $ (por eso el carácter un poco confuso del contenido). Observe el HTML generado para el JavaScript real; si bien en esta misma serie usted utilizará el Template Toolkit para hacerlo correctamente, la idea era hacer una secuencia de comandos autónoma y sencilla. Es algo simple, pero, por desgracia, la legibilidad se vio afectada.

Listado 3. s3form.pl
#!/usr/bin/perl

use warnings;
use strict;
use Data::Dumper;
use Digest::HMAC_SHA1 qw(hmac_sha1 hmac_sha1_hex);
use MIME::Base64;

my $aws_access_key_id     = 'get it from
 http://aws-portal.amazon.com/gp/aws/developer/account/index.html?action=access-key';
my $aws_secret_access_key = 'get it from 
 http://aws-portal.amazon.com/gp/aws/developer/account/index.html?action=access-key';

my $user = 'username'; # this is the user name for the upload

my $policy = '{"expiration": "3000-01-01T00:00:00Z",
  "conditions": [ 
    {"bucket": "images.share.lifelogs.com"}, 
    {"acl": "public-read"},
    ["starts-with", "$key", ""],
    ["starts-with", "$Content-Type", ""],
    {"success_action_redirect": "http://share.lifelogs.com/s3uploaded/$user"},
    ["content-length-range", 0, 1048576]
  ]
}';

$policy = encode_base64($policy);
$policy =~ s/\n//g;

my $signature = encode_base64(hmac_sha1($policy, $aws_secret_access_key));

$signature =~ s/\n//g;
print <<EOHIPPUS;
<html> 
  <head>
    <title>S3 POST Form</title> 
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> 
    <script src="http://ajax.googleapis.com/ajax/libs/prototype/1.6.0.3/prototype.js"
                 type="text/javascript"></script>

  </head>

  <body> 
  <script language="JavaScript">
function submitUploaderForm()
{
 var form = \$('uploader'); // note the escapes we do from Perl
 var file = form['file'];
 var ct = form['Content-Type'];

 var filename = ''+\$F(file); // note the escapes we do from Perl
 var f = filename.toLowerCase(); // always compare against the lowercase version

 if (!navigator['mimeTypes'])
 {
  alert("Sorry, your browser can't tell us what type of file you're uploading.");
  return false;
 }

 var type = \$A(navigator.mimeTypes).detect(function(m)
 {
  // does any of the suffixes match?
  // note the escapes we do from Perl
  return m.type.length > 3 && m.type.match('/') &&
       \$A(m.suffixes.split(',')).detect(function(suffix)
  {
    return f.match('\\.' + suffix.toLowerCase() + '\$'); 
      // note the escapes we do from Perl
  });
 });

 if (type && type['type'])
 {
  ct.value = type.type;
  return true;
 }

 alert("Sorry, we don't know the type for file " + filename);
 return false;
}

</script>
    <form id="uploader" action="https://images.share.lifelogs.com.s3.amazonaws.com/"
     method="post" enctype="multipart/form-data"
     onSubmit="return submitUploaderForm();">
      <input type="hidden" name="key" value="\${filename}">
      <input type="hidden" name="AWSAccessKeyId" value="$aws_access_key_id"> 
      <input type="hidden" name="acl" value="public-read">
      <input type="hidden" name="success_action_redirect"
       value="http://share.lifelogs.com/s3uploaded/$user">
      <input type="hidden" name="policy" value="$policy">

      <input type="hidden" name="Content-Type" value="image/jpeg">
      <input type="hidden" name="signature" value="$signature">
      Select File to upload to S3: 
      <input name="file" type="file"> 
      <br> 
      <input type="submit" value="Upload File to S3"> 
    </form> 
  </body>

</html>
EOHIPPUS

Pido disculpas si alguien se sintió ofendido con este JavaScript poco elegante, pero debería funcionar en la mayoría de los exploradores modernos. Básicamente, interceptamos el botón Enviar y Return True sólo si conocemos el tipo de archivo que se carga.

Nota del editor: Es posible descargar esta secuencia de comandos. Las dos líneas que aparecen en negrita en el Listado 3 no deberían estar separadas de esta forma, pero el ancho de pantalla es limitado. Si copia y pega la secuencia de comandos desde el artículo, restablezca su longitud original de una sola línea. Las líneas aparecen correctamente en la secuencia que se puede descargar.


Formulario de carga: JavaScript y razonamiento

Cargue Prototype desde el sitio de API de Google (ver Recursos). También podrá alojarlo usted mismo si es paranoico, obsesivo o controlador ("ser paranoico no significa que no lo estén vigilando").

Obtenga en el formulario el nombre de archivo utilizando las funciones de Prototype. Obsérvelo (está en minúsculas). Para cada tipo MIME conocido por el explorador, use el método de matrices detect() de Prototype para encontrar la primera coincidencia con las siguientes condiciones:

  • El tipo debe ser mayor de tres caracteres.
  • Debe contener un carácter /.
  • Todos los sufijos del tipo MIME deben coincidir con el nombre de archivo.

El criterio de los tres caracteres y el criterio del carácter / resultan necesarios ya que en Firefox, por lo menos, hay un tipo "*" MIME que coincide con todos ellos. Teniendo en cuenta su inutilidad, debe existir la posibilidad de omitir tanto este tipo como (si sale todo bien) otros tipos MIME que no sirven para nuestros propósitos.

Procese una iteración en los sufijos utilizando el método de cadenas split() de JavaScript, que produce una matriz de fragmentos. Por eso, si los sufijos son jpg,jpeg, procese una iteración en ellos separando en la coma. Una vez más, utilice el método de matrices detect() de Prototype para encontrar, entre esos sufijos, la primera coincidencia con el nombre de archivo. Compare la versión en minúsculas del sufijo con la versión en minúsculas del archivo.

Si no entiende nada, vuelva a estudiar Prototype y JavaScript en general. Por ahora, sólo basta con suponer que esto funciona en la mayoría de los casos más comunes. Sin embargo, es posible que se interrumpa en los exploradores Web antiguos o poco comunes y, por supuesto, no funcionará si el usuario ha deshabilitado JavaScript. Así es la vida. Apuntamos a un mecanismo que funcione para la mayoría de los visitantes.

Si falla la detección del tipo, también fallaremos nosotros. Si bien aparecerá un mensaje, esto se puede hacer de una mejor manera. Se podrían hacer algunos intentos, por ejemplo, y tal vez revertir a imagen/jpeg si no se conoce el tipo. Depende de usted realizar una refinación. La función devolverá false si esto ocurre, y se anulará la carga.

Tenga en cuenta que con una carga exitosa, se está redirigiendo a una dirección URL que contiene el nombre de usuario. Consulte la sección "Cargas en S3" para conocer los motivos.

Finalmente, esta secuencia de comandos combina Perl, JavaScript y HTML en un grupo interesante (imagine un auto deportivo con velas y timón, tocando el saxofón). Los ejemplos que se dan con el expreso propósito de mostrar una determinada técnica no deberían ser una guía para el estilo y la arquitectura. Sería genial que no copiara y pegara la secuencia de comandos que se incluye aquí sin por lo menos considerar su división en fragmentos de plantilla y su refactorización. Les prometo que más adelante les explicaré cómo funciona todo esto en el contexto de un sitio Web completo de mod_perl.


Conclusión

Ya hemos visto cómo se puede configurar un formulario de carga HTML para cargar archivos directamente en S3. Utilizamos Perl, JavaScript y HTML. Se presenta, con ciertas advertencias, una secuencia de comandos simple que utiliza la biblioteca Prototype JavaScript, los módulos MIME::Base64 y Digest::HMAC_SHA1 de Perl, y JavaScript y HTML alineados. La secuencia de comandos cargará un único archivo en S3 para un usuario determinado, y finalmente se redirigirá a una dirección URL determinada del sitio share.lifelogs.com.

La parte 3 muestra cómo la dirección URL crea un registro de SimpleDB para el archivo cargado. Usted aprenderá cómo crear, editar y comentarios como registros de SimpleDB en una foto para un usuario determinado. Las partes 4 y 5 le permitirán organizar un sitio Web mod_perl. Siga conectado.


Descargar

DescripciónNombretamaño
Sample script for this articles3form.zip2KB

Recursos

Aprender

Obtener los productos y tecnologías

Comentar

Comentarios

developerWorks: Ingrese

Los campos obligatorios están marcados con un asterisco (*).


¿Necesita un IBM ID?
¿Olvidó su IBM ID?


¿Olvidó su Password?
Cambie su Password

Al hacer clic en Enviar, usted está de acuerdo con los términos y condiciones de developerWorks.

 


La primera vez que inicie sesión en developerWorks, se creará un perfil para usted. La información en su propio perfil (nombre, país/región y nombre de la empresa) se muestra al público y acompañará a cualquier contenido que publique, a menos que opte por la opción de ocultar el nombre de su empresa. Puede actualizar su cuenta de IBM en cualquier momento.

Toda la información enviada es segura.

Elija su nombre para mostrar



La primera vez que inicia sesión en developerWorks se crea un perfil para usted, teniendo que elegir un nombre para mostrar en el mismo. Este nombre acompañará el contenido que usted publique en developerWorks.

Por favor elija un nombre de 3 - 31 caracteres. Su nombre de usuario debe ser único en la comunidad developerWorks y debe ser distinto a su dirección de email por motivos de privacidad.

Los campos obligatorios están marcados con un asterisco (*).

(Por favor elija un nombre de 3 - 31 caracteres.)

Al hacer clic en Enviar, usted está de acuerdo con los términos y condiciones de developerWorks.

 


Toda la información enviada es segura.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=90
Zone=Lotus, Rational
ArticleID=394981
ArticleTitle=Perla cultivada: Perl and the Amazon cloud, Part 2
publish-date=04082009