DEV Community

Cover image for WSGI Hello world.
1N0T
1N0T

Posted on

WSGI Hello world.

Si te interesa el desarrollo de aplicaciones web en python, es más que probable que te suenen estas siglas, o incluso, que conceptualmente tengas claro de qué se trata.

WSGI (Web Server Gateway Interface) es simplemente una convención, que define cómo debe ser la comunicación entre un servidor web y una aplicación, que se ajusten a este patrón. Esta definición, se encuentra regogida en el PEP 3333. Bueno, en realidad, ésta es de aplicación para las versiones 3.x de python (la definición inicial para versiones 2.x es la PEP 333.

Entre los paquetes incluidos en python, podemos encontrar un servidor web WSGI de referencia (dentro de wsgiref.simple_server) y una aplicación de demo.

Una vez hechas las presentaciones, vamos a ponernos manos a la obra e implementar nuestra primera versión.

from wsgiref.simple_server import make_server, demo_app

IP = "0.0.0.0"
PUERTO = 8080
APP = demo_app

print(f"Iniciando aplicación APP en servidor WSGI en { IP }: { PUERTO }")
with make_server(IP, PUERTO, APP) as servidor_wsgi:
    print("Puedes finalizar con CTRL+C")
    servidor_wsgi.serve_forever()
Enter fullscreen mode Exit fullscreen mode

Si ejecutamos el código que acabamos de escribir, habremos levantado un servidor web WSGI, esperando peticiones HTTP al puerto 8080.

Ahora podemos abrir nuestro navegador favorito y solicitar la siguiente URL (http://localhost:8080). Y, ¡oh sorpresa!, aparece una página con, la tan manida frase en el mundo de la programación Hello world!, seguida de una probablemente larga lista de VARIABLE = 'valor'.

Llegados a este punto, ye está conseguido el objetivo indicado en el titular, por lo que podríamos ya dar por concluido el artículo. Pero no te preocupes, aunque no mucho, vamos a profundizar un poco más en el tema.

Resumiendo al extremo, una aplicación WSGI, tiene que ser un callable (una función, o un objeto que implemente el método __call__) que recibirá 2 parámetros. El primero es el entorno de ejecución y, el segundo, un callback, que debe ser llamado para construir la respuesta.

Nuestra aplicación de demo, nos muestra el contenido del entorno de ejecución. Un diccionario con los valores de nuestras variables de entorno, a las que se han añadido algunos valores adicionales incluidos por nuestro servidor WEB (como PATH_INFO = '/' que contiene ruta del recurso que hemos solicitado anteriroremnte al servidor). Por otra parte, se espara que nuestra aplicación devuelva la respuesta a entregar al solicitante.

Vamos pues, a crear nuestra primera aplicación WSGI, dejando nuestro código, con algo parecido a:

from wsgiref.simple_server import make_server


#=============================================================================
# Esta función es nuestra aplicación WSGI y, como tal, al ser llamada
# recibirá los 2 parámetros esperados:
#   * diccionario con el entorno de ejecución.
#   * callback para crear respuesta.
#=============================================================================
def mi_app_wsgi(entorno, responder):

    # Configuramos opciones de respuesta por defecto
    cabeceras = [('Content-type', 'text/plain; charset=utf-8')]

    # Iniciamos respuesta
    responder('200 OK', cabeceras)    
    respuesta = "Contenido devuelto por mi_app_wsgi."

    # El cuerpo de la respuesta no puede ser un string.
    # Debe ser una lista de bytes.
    return [respuesta.encode("utf-8")]


IP = "0.0.0.0"
PUERTO = 8080
APP = mi_app_wsgi


print(f"Iniciando aplicación APP en servidor WSGI en { IP }: { PUERTO }")
with make_server(IP, PUERTO, APP) as servidor_wsgi:
    print("Puedes finalizar con CTRL+C")
    servidor_wsgi.serve_forever()
Enter fullscreen mode Exit fullscreen mode

La nueva versión, no ha cambiado mucho. Hemos añadido la función mi_app_wsgi, que recibirá los 2 parámetros esperados y, nota importante, devolverá una lista de bytes (si devolvemos un string saltará una excepción, ya que no se corresponde con lo esperado).

También es necesario que llamemos a la función de calback (la hemos llamado responder, en nuestro caso concreto). Por lo demás, creo que el código es lo suficientemente explícito, como para no requerir de ninguna explicación adicional.

Si ejecutamos nuestra nueva versión y, volvemos a solicitar la URL http://localhost:8080 creo que es bastante previsible cual será el resultado que obtendremos.

También lo podríamos dejar aquí pero, ya que estamos puestos, vamos ha hacer algo un poquito más elaborado.

from wsgiref.simple_server import make_server
from base64 import b64decode



#=============================================================================
# Esta función es una aplicación WSGI y, como tal, al ser llamada
# recibirá los 2 parámetros esperados:
#   * diccionario con el entorno de ejecución.
#   * callback para crear respuesta.
#=============================================================================
def mi_app_wsgi(entorno, responder):

    # Configuramos opciones de respuesta por defecto
    cabeceras = [('Content-type', 'text/plain; charset=utf-8')]

    # Iniciamos respuesta
    responder('200 OK', cabeceras)    
    respuesta = "Contenido devuelto por mi_app_wsgi."

    # El cuerpo de la respuesta no puede ser un string.
    # Debe ser una lista de bytes.
    return [respuesta.encode("utf-8")]



#=============================================================================
# Esta clase es nuestra aplicación WSGI y, como tal, al ser llamada
# recibirá los 2 parámetros esperados:
#   * diccionario con el entorno de ejecución.
#   * callback para crear respuesta.
#=============================================================================
class AutenticacionBasica():

    USUARIO = "mi_usuario"
    PASSWORD = "mi contraseña"


    def __init__(self, app):
        self._app = app


    def __call__(self, entorno, responder):
        # Comprobamos si la solicitud tiene una cabecera de autenticación válida.
        # Si esto es así, llamamos a nuestra otra aplicación WSGI y devolveremos
        # lo que ésta nos devuelva.
        if self._autenticado(entorno.get('HTTP_AUTHORIZATION')):
            return self._app(entorno, responder)

        # Solicitamos autenticación para peticiones no autenticadas-
        return self._no_autenticado(entorno, responder)


    def _autenticado(self, cabecera_autenticacion):
        # Comprobamos si hemos recibido cabedera de autenticación
        if not cabecera_autenticacion:
            return False

        # Si hemos recibido cabedera de autenticación, decodificamos contenido y
        _, valor_codificado = cabecera_autenticacion.split(None, 1)
        valor_decodificado = b64decode(valor_codificado).decode('utf-8')
        username, password = valor_decodificado.split(':', 1)

        # comprobamos si usuario y contraseña se corresponden con valores aceptados
        # como válidos.
        return username == self.USUARIO and password == self.PASSWORD


    def _no_autenticado(self, entorno, responder):
        # Configuramos cabecras por defecto de respuesta
        cabeceras = [
            ('Content-type', 'text/plain; charset=utf-8'),
            ('WWW-Authenticate', 'Basic realm="Login"')
        ]

        # Iniicamos respuesta indicando que se requiere autenticación previa.
        responder('401 Authentication Required', cabeceras)
        respuesta = "Autenticación de usuario es requerida."

        return [respuesta.encode("utf-8")]



IP = "0.0.0.0"
PUERTO = 8080
APP = AutenticacionBasica(mi_app_wsgi)


print(f"Iniciando aplicación APP en servidor WSGI en { IP }: { PUERTO }")
with make_server(IP, PUERTO, APP) as servidor_wsgi:
    print("Puedes finalizar con CTRL+C")
    servidor_wsgi.serve_forever()

Enter fullscreen mode Exit fullscreen mode

En este caso, hemos añadido una clase callable como aplicación WSGI. Su método __call__ recibirá los parámetros esperados según lo especificado en WSGI y, adicionalmente (durante su instanciación), la misma aplicación WSGI del ejemplo anterior sin modificaciones.

Con este ejemplo, vemos como podemos interponer un midleware que, en nuestro caso, forzará la autenticación previa del usuario, antes de poder a nuestra aplicación inicial (sin tener que modificar ésta). Por cuestión de simplicidad del código, hemos mantenido todo en un único fuente, pero, mi_app_wsgi, en lugar de una simple función, podría ser una compleja aplicación Flask p.ej. y la clase AutenticacionBasica podría implementar una elaborada validación de tokens JWT.

Creo que, ahora sí, es un buen momento para concluir el artículo. Espero haber aportado algo que te haya resultado de utilidad.

Top comments (0)