Python por ejemplo
Tugurium/Python

Python, por ejemplo

Servidor HTTP

5.4 Servidor HTTP

Un servidor web es parte de la infraestructura de Internet que lleva a la práctica el modelo cliente-servidor con el protocolo HTTP. Cuando un navegador cliente realiza una petición HTTP al servidor este la procesa y responde con los ficheros necesarios para presentar una página web.

Una de las muchas clasificaciones de servidores que podemos hacer diferencia entre servidores estáticos y dinámicos. Los servidores web estáticos sólo devuelven los ficheros tal cual, sin procesar, mientras que los dinámicos realizan tratamientos que pueden implicar la consulta a bases de datos para construir las respuestas sobe la marcha.

Python incorpora un servidor web que puede emplearse para una comunicación sencilla cliente-servidor web. Aunque no es un servidor web completo, puede servir ficheros estáticos que nos permiten hacer pruebas localmente.

5.4.1 Módulo http.server

Un servidor web HTTP no es más que un proceso que escucha las peticiones HTTP entrantes en una dirección TCP específica, establecida por una dirección IP y un número de puerto, y envía una respuesta al usuario.

El módulo http.server forma parte de la biblioteca estándar de Python que nos proporciona un servidor HTTP ligero y sencillo que puede ser utilizado para manejar peticiones HTTP y servir ficheros estáticos. Este módulo soporta HTTP/1.0 y HTTP/1.1

Es recomendable acceder a la documentación oficial de http.server para ver todas sus posibilidades.

El servidor HTTP de Python es una forma cómoda de compartir ficheros con usuarios que estén conectados a la misma red local. O también para realizar pruebas sencillas con el protocolo HTTP en un servidor local sin pretensiones.

La forma más sencilla de lanzar el servidor web http.server es desde la consola del sistema mediante el comando:

python -m http.server [port] [--bind address] [--directory path]

Donde

port: Número de puerto en el que el servidor escuchará las conexiones entrantes. El valor predeterminado es 8000.

address: Dirección a la que se enlazará el servidor. Por defecto es 0.0.0.0, lo que significa que el servidor será accesible desde cualquier dirección IP. Con localhost o 127.0.0.1, obtenemos un servidor local.

path: Directorio desde el cual el servidor servirá los ficheros. Por defecto es el directorio de trabajo desde donde arranquemos el servidor.

Por razones de seguridad, es conveniente restringir el acceso al servidor http.server empleando la dirección IP del bucle de retorno localhost, de esta manera solo podremos acceder nosotros desde el equipo desde el que se lance el servidor. Poner la dirección IP del equipo amplía las posibilidades de servicio a todos los usuarios de la red local, pero también incrementa la inseguridad.

Así, con:

python -m http.server 8000 -b 127.0.0.1 -d C:\TestPython\redes\http\web

Tendremos un servidor HTTP local (127.0.0.1) que atiende peticiones en el puerto 8000 y que accederá a los ficheros que se encuentran en el directorio 'C:\TestPython\redes\http\web'.

Servidor lanzado

Para acceder al servidor utilizando el navegador web basta escribir en la barra de direcciones http://localhost:8000 o http://127.0.0.1:8000.

Por defecto, el servidor presentará los ficheros y carpetas ubicados en el directorio de trabajo desde donde se lanzó. Cada entrada es un enlace al fichero correspondiente. Haciendo clic en cualquier fichero o subdirectorio el navegador enviará una petición GET al servidor para acceder al fichero/directorio y mostrar su contenido.

Si se encuentra un fichero index.html en el directorio de trabajo el servidor web enviará este fichero al navegador.

Para las pruebas crearemos un fichero index.html, para que nos lo presente en lugar de listar el contenido del directorio.

<html lang="es">
<head>
    <meta charset="utf-8">
    <title>Python servidor HTTPServer</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="keywords" content="Python, por ejemplo">
</head>

<body>
    <h3>HTTPServer</h3>
    <p>Html para prueba del servidor Python HTTPServer</p>
    <p>En un subdirectorio</p>
</body>
</html>

En el terminal donde arrancó el servidor obtenemos las siguientes salidas

 

Entre las salidas encontramos un error 404, ya que no tenemos un icono asociado al fichero html.

En el navegador tendremos la página index.html que hemos preparado.

Página index.html

Pero podemos ir más allá, ya que el módulo http.server nos permite ampliar y personalizar el servidor a través de las clases que posee.

Las peticiones entrantes son gestionadas por un manejador. El módulo http.server dispone de varios manejadores.

ManejadorDescripción
BaseHTTPRequestHandler Trata las peticiones HTTP entrantes.

Proporciona métodos para analizar solicitudes, enviar respuestas y gestionar errores.

Sobreescribiendo sus métodos es posible personalizar el comportamiento del servidor HTTP.

SimpleHTTPRequestHandler Es una subclase de BaseHTTPRequestHandler que proporciona una implementación básica para servir ficheros desde directorio donde corre el servidor y de cualquiera de sus subdirectorios.

Soporta los métodos GET y HEAD, y genera automáticamente listados de directorios.

Si encuentra un fichero index.html mostrará su contenido en lugar de la lista de ficheros del directorio.

ThreadingHTTPServer Extiende la clase HTTPServer proporcionando un servidor HTTP multihilo.

Crea un nuevo hilo para cada petición entrante, lo que puede mejorar el rendimiento cuando se manejan múltiples conexiones simultáneas.

CGIHTTPRequestHandler Es una subclase de BaseHTTPRequestHandler que proporciona soporte para servir scripts CGI. Ejecuta el script y devuelve la salida como respuesta HTTP.

Veamos a continuación un ejemplo sencillo de servidor HTTP haciendo uso de http.server.

from http.server import HTTPServer, SimpleHTTPRequestHandler

# dirección y puerto del servidor
host = 'localhost'
puerto = 8000

# crear una instancia del manejador por defecto
manejador = SimpleHTTPRequestHandler

# crear una instancia del servidor
# con el host, puerto y manejador preestablecidos
servidor = HTTPServer((host, puerto), manejador)

# aviso del servidor en funcionamiento
print(f'Servidor ejecutándose en http://{host}:{puerto}/')

# iniciar el servidor
servidor.serve_forever()

Una vez lanzado el script el método serve_forever() inicia el servidor que comienza a escuchar y responder a las peticiones entrantes. Este script crea un servidor HTTP básico que sirve ficheros desde el directorio de trabajo actual.

Para acceder al servidor utilizaremos el navegador web, como hemos hecho al arrancar el servidor desde línea de comandos. Con este script accederemos a los ficheros en el directorio desde el que lo lancemos, y como también tenemos un fichero index.html será este el que se envíe al navegador.

En el terminal donde arrancó el servidor obtenemos las siguientes salidas

Servidor ejecutándose en http://localhost:8000/
127.0.0.1 - - [25/Dec/2023 11:50:54] "GET /index.html HTTP/1.1" 200 -

En el ejemplo anterior hacemos uso de la clase SimpleHTTPRequestHandler, que es un manejador incorporado para servir archivos estáticos desde el directorio donde se lanzó.

Para un comportamiento más avanzado disponemos de las distintas clases que están pensadas para personalizar los métodos del protocolo HTTP del servidor y modificar su comportamiento para adaptarlo a nuestras necesidades.

En el siguiente ejemplo vamos a redirigir el servicio a un directorio diferente, para ello crearemos un manejador personalizado ampliando la clase base SimpleHTTPRequestHandler.

from http.server import HTTPServer, SimpleHTTPRequestHandler

# directorio de trabajo
directorio = r'C:\TestPython\redes\http\web'

# crear un manejador propio
class ManejadorPersonal(SimpleHTTPRequestHandler):
    def __init__(self, *args, **kwargs):
        # establecer un directorio de trabajo específico
        super().__init__(*args, directory=directorio, **kwargs)

if __name__ == "__main__":
    # dirección y puerto del servidor
    host = 'localhost'
    puerto = 8000

    # crear una instancia del manejador personalizado
    manejador = ManejadorPersonal

    # crear una instancia del servidor
    # con el host, puerto y el manejador propio
    servidor = HTTPServer((host, puerto), manejador)

    # aviso del servidor en funcionamiento
    print(f'Servidor ejecutándose en http://{host}:{puerto}/')

    # iniciar el servidor
    servidor.serve_forever()

Nos ha bastado con reescribir el método __init__ para pasarle a la clase base la nueva ruta de acceso para los ficheros.

Vamos ahora a analizar los parámetros de la petición con SimpleHTTPRequestHandler que están almacenados como atributos de instancia de la petición.

En este ejemplo, sobrescribimos el método do_GET() para definir un comportamiento personalizado para el manejo de peticiones GET, donde mostramos los atributos de la petición y acabamos llamando a super().do_GET(), para que el servidor envíe el fichero solicitado originalmente.

from http.server import HTTPServer, SimpleHTTPRequestHandler

# crear un manejador propio
class ManejadorPersonal(SimpleHTTPRequestHandler):
    def do_GET(self):
        lineas_mensaje = [
                '\n___ Cliente ___\n',
                f'client_address={self.client_address} ({self.address_string()})',
                f'{self.command=}',
                f'path={self.path}',
                f'request_version={self.request_version}',
                '\n___ Servidor ___\n',
                f'server_version={self.server_version}',
                f'sys_version={self.sys_version}',
                f'protocol_version={self.protocol_version}',
                '',
                '_ Cabeceras _',
                ]
        for nombre, valor in sorted(self.headers.items()):
            lineas_mensaje.append(f'{nombre}={valor.rstrip()}')
        mensaje = '\n'.join(lineas_mensaje)
        self.send_response(200)
        self.end_headers()
        self.wfile.write(bytes(mensaje, 'utf-8'))

        self.wfile.write(b'\n\n_ super().do_GET _\n\n')
        super().do_GET()
        return

if __name__ == "__main__":
    # dirección y puerto del servidor
    host = 'localhost'
    puerto = 8000

    # crear una instancia del manejador personalizado
    manejador = ManejadorPersonal

    # crear una instancia del servidor
    # con el host, puerto y el manejador propio
    servidor = HTTPServer((host, puerto), manejador)

    # aviso del servidor en funcionamiento
    print(f'Servidor ejecutándose en http://{host}:{puerto}/')

    # iniciar el servidor
    servidor.serve_forever()

La petición con "http://127.0.0.1:8000/index.html", nos proporciona en este caso.

___ Cliente ___

client_address=('127.0.0.1', 26057) (127.0.0.1)
self.command='GET'
path=/index.html
request_version=HTTP/1.1

___ Servidor ___

server_version=SimpleHTTP/0.6
sys_version=Python/3.8.1
protocol_version=HTTP/1.0

_ Cabeceras _
Accept=*/*
Accept-Encoding=gzip, deflate
Connection=keep-alive
Host=127.0.0.1:8000
User-Agent=python-requests/2.31.0

_ super().do_GET _

HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.8.1
Date: Tue, 26 Dec 2023 21:01:51 GMT
Content-type: text/html
Content-Length: 432
Last-Modified: Mon, 25 Dec 2023 11:07:18 GMT

<html lang="es">
<head>
    <meta charset="utf-8">
    <title>Python servidor HTTPServer</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="keywords" content="Python, por ejemplo"> 
</head>

<body>
    <h3>HTTPServer</h3>
    <p>Html para prueba del servidor Python HTTPServer</p>
    <p>En el directorio de trabajo</p>
    <a href='otra.html'>Siguiente página html</a>
</body>
</html>

Si utilizamos BaseHTTPRequestHandler no disponemos de la posibilidad de llamar a do.GET(), en este caso, debemos manejar manualmente el servicio de ficheros.

En el siguiente ejemplo usamos BaseHTTPRequestHandler, así que deberemos tratar manualmente el fichero solicitado. En el método do_GET() intentamos abrir y servir el fichero solicitado, y si no se encuentra se envía un error 404. A continuación podemos añadir el tratamiento que deseemos.

from http.server import HTTPServer, BaseHTTPRequestHandler

# crear un manejador propio
class ManejadorPersonal(BaseHTTPRequestHandler):
    def do_GET(self):
        # servir el fichero solicitado
        try:
            with open("." + self.path, 'rb') as file:
                content = file.read()
                self.send_response(200)
                self.send_header('Content-type', 'text/html')
                self.end_headers()
                self.wfile.write(content)
        except FileNotFoundError:
            self.send_error(404, f'File Not Found: {format(self.path)}')
        return

if __name__ == "__main__":
    # dirección y puerto del servidor
    host = 'localhost'
    puerto = 8000

    # crear una instancia del manejador personalizado
    manejador = ManejadorPersonal

    # crear una instancia del servidor
    # con el host, puerto y el manejador propio
    servidor = HTTPServer((host, puerto), manejador)

    # aviso del servidor en funcionamiento
    print(f'Servidor ejecutándose en http://{host}:{puerto}/')

    # iniciar el servidor
    servidor.serve_forever()

El resultado visualiza la página index.html que ya conocemos.

Cuando deseemos finalizar el servidor HTTP podemos pulsar Ctrl+C en el teclado sobre la ventana del terminal, lo que envía al servidor una señal de apagado.

Si el servidor está haciendo algún proceso que precise liberar algún tipo de recurso antes de su cierre es preciso realizar una detención controlada, detención que dependerá de la situación específica en que nos encontremos.

En este ejemplo capturaremos una excepción del tipo KeyboardInterrupt, que se lanza cuando el usuario pulsa Ctrl+C en el terminal. Cuando se capture la excepción apagaremos el servidor de una manera controlada y acabaremos llamando al método shutdown().

from http.server import HTTPServer, SimpleHTTPRequestHandler

# dirección y puerto del servidor
host = 'localhost'
puerto = 8000

# crear una instancia del manejador por defecto
manejador = SimpleHTTPRequestHandler

# crear una instancia del servidor
# con el host, puerto y manejador prestablecidos
servidor = HTTPServer((host, puerto), manejador)

# aviso del servidor en funcionamiento
print(f'Servidor ejecutándose en http://{host}:{puerto}/')
print('Pulse Ctrl+C para detener el servidor')

try:
    # iniciar el servidor
    servidor.serve_forever()

except KeyboardInterrupt:
    # interrupción de teclado (Ctrl+C)
    print('\nDeteniendo el servidor...')
    # apagar el servidor
    servidor.shutdown()
    print('Servidor parado')

El bucle lanzado con serve_forever() mantiene el proceso del servidor web, por lo que el bloque except sólo se ejecutará cuando el usuario interrumpa el script, en este caso al pulsar Ctrl+C.

Cuando se captura la excepción KeyboardInterrupt procedemos a realizar una parada controlada, en este caso un simple mensaje que avisa sobre la detención del servidor, y terminamos cerrando el servidor usando el método shutdown().

Debemos ser realistas y pensar que el http.server, con una implementación tan simple, no puede ser seguro, por lo que no debemos utilizar este servidor en un entorno de cierta importancia, ya que podría convertirse en un agujero de seguridad que potencialmente puede ser pirateado.

La misma documentación oficial ya nos advierte en la introducción:

Advertencia: http.server no se recomienda para producción. Sólo implementa comprobaciones de seguridad básicas.

Así pues, solo para pruebas, en local, y cerrándolo al terminar.

Servidor HTTP

    • Servidor HTTP
      • Módulo http.server