Windows Phone: Subiendo un fichero a un servidor WEB con credenciales básicas sin tener que reintentar

Rafael Ontivero

Programador sistemas embebidos

http://geeks.ms/blogs/rfog/

Microsoft MVP

LinkedIn: http://es.linkedin.com/pub/rafael-ontivero/54/622/8aa

1.- El problema

La utilización de un componente WebRequest con verificación de credenciales dentro de una aplicación Windows Phone tiene la contrapartida de que se realizan dos operaciones de subida o de bajada. Primero el componente prueba a subir sin ellas y si el servidor da el error adecuado, con ellas. En contra de la versión de escritorio, no existe la posibilidad de realizar la transacción de forma directa.

Pero a veces los servidores no responden bien o simplemente queremos ahorrarnos unos cuantos bytes de nuestras caras conexiones 3G.

La solución es de Perogrullo, pero vamos a explicarla en detalle.

2.- El proceso de subir un fichero a un servidor WEB

Vamos a suponer que necesitamos bajar (o subir) cualquier tipo de fichero desde un servicio Web. Es decir, tenemos una aplicación que necesita realizar algún tipo de operación remota mediante el procesamiento de un fichero (o flujo, ya sea binario o de texto) cualquiera.

Ejemplos válidos serían bajarnos la actualización de unos datos de nuestra empresa, como unas tarifas, o datos de un cliente que en este momento no tenemos localmente, o simplemente queremos subir un presupuesto o incluso una fotografía que hemos tomado.

No estamos limitados a nada. La única consideración a tener en cuenta en una situación de esta índole es que dicho servicio ha de estar protegido con algún tipo de seguridad para evitar que nadie, desde fuera, pueda utilizarlo para sus propósitos personales o para envenenar cualesquiera datos que haya en dichos servidor.

Aplicaciones típicas son programas de chat que suben o bajan imágenes o vídeos, ficheros de música o simples documentos empresariales.

Pese a existir una solución más sencilla con el componente WebClient, utilizar un WebRequest nos da la potencia para poder enviar ficheros de mayor tamaño que la memoria disponible en el terminal entre otras cosas. Por experiencia propia, podemos afirmar que es prácticamente trivial que el sistema nos dispare una excepción de falta de memoria subiendo una simple foto tomada desde la cámara aunque llamemos manualmente al recolector de basura de vez en cuando durante la operación, sobre todo si antes le hemos hecho algún tipo de post-procesado.

La idea de todo esto es bastante sencilla (ver el código de abajo):

(1) Creamos un objeto de tipo WebRequest a partir de la URL de nuestro servidor. En ese instante asignamos las cabeceras que sean necesarias e imprescindibles (un truco de seguridad es definir una cabecera especial con algún valor extra).

(2) También debemos asignar las credenciales, que no dejan de ser una cabecera más.

(3) Otra cosa que tenemos que hacer es o bien cargar el fichero a enviar en un array de bytes o bien disponer de un stream sobre el que leeremos poco a poco lo que vamos a enviar.

(4) A partir de ahí llamamos a BeginGetResponse(), que recibe una acción que se ejecutará cuando la cosa esté lista para empezar a subir bytes al servidor. Allí dentro es donde debemos, en bloques, enviar los bytes al servidor.

(5) Una vez finalizada la subida, debemos llamar a BeginGetResponse(), que recibe una nueva acción, sobre la que leeremos la respuesta del servidor, que en nuestro ejemplo es un JSON. Notemos que el stream de recepción es de sólo lectura, por lo que conforme vamos avanzando no podremos ir hacia atrás, y si necesitamos eso, debemos copiarlo primero a algún otro lado.

Ya está. Ya hemos subido el fichero. El ejemplo de abajo esquematiza la operación.

        private void UploadFile(string user, string pass, Action<int> progress, Action<MultimediaJsonApi2Answer> action)
        {
            try
            {
                //Punto 1 del texto
                var web = WebRequest.CreateHttp(GlobalDefinitions.MultimediaUrl);
                web.Method = "POST";
                web.Credentials = new NetworkCredential(user, pass);    //Punto 2
                web.Headers["user-agent"] = ComposeUserAgent();
                web.ContentType = m_mmediaElement.ContentType;

                //Obtener un stream a lo que vamos a subir. También se puede hacer
                //dentro de BeginGetRequestStream. Punto 3.

                web.ContentLength = bytes.Length; //Esto no es necesario a no ser que queramos subir sin buffer.
                web.AllowWriteStreamBuffering = true; //Con esto a true subimos sin buffer (más lento)

                //Punto 4
                web.BeginGetRequestStream((asynResult) =>
                    {
                        var request = (HttpWebRequest) asynResult.AsyncState;
                        var sendStream = request.EndGetRequestStream(asynResult);

                        //En bucle, enviar poco a poco los bytes y notificar del progreso
                        //for(;;)
                        {
                            if (progress != null)
                                progress(/*currentProgress*/0);
                        }
                        sendStream.Close();

                        //No nos olvidemos de cerrar todos los streams

                        //Punto 5
                        request.BeginGetResponse((respAsyncResult) =>
                            {
                                MultimediaJsonApi2Answer answer = null;
                                try
                                {
                                    var respRequest = (HttpWebRequest) asynResult.AsyncState;
                                    var response = respRequest.EndGetResponse(respAsyncResult);
                                    var respStream = response.GetResponseStream();

                                    /*
                                    //Esto es para ver el JSON recibido, pero ya no se puede rehidratar pq el stream es RO.
                                    var reader = new StreamReader(respStream);
                                    var responseSting = reader.ReadToEnd();
                                    Debug.WriteLine(responseSting);
                                    */

                                    answer = MultimediaJsonApi2Answer.Deserialize(respStream);

                                    respStream.Close();
                                }
                                catch (Exception e)
                                {
                                    Debug.WriteLine(e);
                                }

                                if (action == null)
                                    return;

                                action(answer);
                            }, request);
                    }, web);
            }
            catch (Exception)
            {
                if (progress != null)
                    progress(-1);
                if (action != null)
                    action(null);
            }
        }

Podemos ver que el proceso es relativamente sencillo. También debemos tener en cuenta que tanto el código dentro de BeginGetResponse() como de BeginGetResponse() se ejecuta en un hilo independiente al del interfaz del usuario, por lo que si accedemos a algún componente visual habremos de hacerlo mediante el Dispatcher() correspondiente.

3.- Qué ocurre en las bambalinas

Cuando se inicia la negociación HTTP con el servidor, el componente envía las cabeceras sin autentificación. Si el servidor responde con el error adecuado, el componente vuelve a enviar las cabeceras, esta vez con la parte de clave/usuario asignadas.

Esto genera un gasto de ancho de banda innecesario ya que en general sabemos de antemano que nuestro servidor va a requerir autentificación.

En las versiones de escritorio tenemos la propiedad PreAuthenticate que fuerza esto mismo, pero en Windows Phone no existe tal, por lo que, aparte de perder el envío de unos cuantos bytes, podría generarse algún tipo de problema en la subida cuando el servidor no devolviera el código de error adecuado.

4.- La solución

¿Podemos evitar esto? Sí, es tan sencillo como eliminar la parte de asignación de las credenciales y poner nosotros, a mano, la cabecera adecuada. Es decir, tenemos que cambiar la línea

                web.Credentials = new NetworkCredential(user, pass);    //Punto 2

por algo similar a

                web.Headers[“Authenticate”] = GenerateCredentials(user, pass);    

Tan solo nos queda definir el método GenerateCredentials(), que es el siguiente:

private static string ComposeBasicCredentials(string user, string pass)
        {
            var credentials = user + ":" + pass;
            var buffer = System.Text.Encoding.UTF8.GetBytes(credentials);
            return "Basic " + Convert.ToBase64String(buffer);
        }

Es tan sencillo como crear una cabecera llamada Authenticate y cuyo valor sea la cadena Basic seguida de un espacio en blanco, además del usuario y la contraseña, separadas por el carácter de dos puntos y todo ello codificado en Base64, que es lo que realmente hace, internamente, la clase NetwotkCredential(). Pero nosotros, al ponerlo a mano, aseguramos que el servidor nos va a aceptar en primera instancia.

La autentificación basic y sus cabeceras está documentada aquí y aquí.

| Página de inicio |Artículos Técnicos | Comunidad