miércoles, 24 de septiembre de 2025

WEBPROPv2 (VI). Instalación en un servidor con nginx (I). Instalar nginx, openresty, Lua y certificados

 1. Instalar nginx

Si lo instalamos en la máquina local

# 1. Instalar nginx en Ubuntu
sudo apt update
sudo apt install nginx

# 2. Arrancar nginx
sudo systemctl start nginx

# 3. Verificar 
systemctl status nginx.service

Pero nos da el problema que el puerto 80 ya está ocupado:

informatica@srv-glpi-16:~$ systemctl status nginx.service
× nginx.service - A high performance web server and a reverse proxy server
     Loaded: loaded (/usr/lib/systemd/system/nginx.serv
ice; enabled; preset: enabled)
     Active: failed (Result: exit-code) since Wed 2025-09-24 11:58:58 CEST; 1min 18s ago
       Docs: man:nginx(8)
    Process: 73037 ExecStartPre=/usr/sbin/nginx -t -q -g daemon on; master_process on; (code=exited, status=0/SUCC
ESS)
    Process: 73038 ExecStart=/usr/sbin/nginx -g daemon on; master_process on; (code=exited, status=1/FAILU
RE)
        CPU: 11ms

Sep 24 11:58:56 srv-glpi-16 nginx[73038]: nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
Sep 24 11:58:56 srv-glpi-16 nginx[73038]: nginx: [emerg] bind() to [::]:80 failed (98: Address already in use)
Sep 24 11:58:57 srv-glpi-16 nginx[73038]: nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
Sep 24 11:58:57 srv-glpi-16 nginx[73038]: nginx: [emerg] bind() to [::]:80 failed (98: Address already in use)
Sep 24 11:58:57 srv-glpi-16 nginx[73038]: nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
Sep 24 11:58:57 srv-glpi-16 nginx[73038]: nginx: [emerg] bind() to [::]:80 failed (98: Address already in use)
Sep 24 11:58:58 srv-glpi-16 nginx[73038]: nginx: [emerg] still could not bind()
Sep 24 11:58:58 srv-glpi-16 systemd[1]: nginx.service: Control process exited, code=exited
, status=1/FAILURE
Sep 24 11:58:58 srv-glpi-16 systemd[1]: nginx.service: Failed with result 'exi
t-code'.
Sep 24 11:58:58 srv-glpi-16 systemd[1]: Failed to start nginx.service - A high performance
 web server and a reverse proxy server.

Para ello hay que actuar sobre la configuración de nginx y utilizar otro puerto

Para ello buscamos donde puede estar la línea "listen 80"


# 1. Buscar la cadena "listen" en los ficheros de estos directorios
grep -R "listen" /etc/nginx/sites-enabled/ /etc/nginx/conf.d/

Y aparece el fichero "/etc/nginx/sites-enabled/default"

Lo modificamos con nano y cambiamos "80" por "8088"

# 1. Cambiamos 80por 8088
server {
        listen 8088 default_server;
        listen [::]:8088 default_server;


Y arrancamos el servicio de nginx y lo verificamos:


# 1. Arrancar nginx
sudo systemctl start nginx

# 2. Verificar 
systemctl status nginx.service

Y ya lo tenemos en marcha

2. Substituir nginx por openresty

 Veamos los pasos a realizar según Cloudspinx:

#1. Bloquear nginx
sudo systemctl disable nginx
sudo systemctl stop nginx

#2. Instalar prerequisitos
sudo apt-get -y install --no-install-recommends wget gnupg ca-certificates lsb-release

#3. Importar la clave GPG de descarga (para Ubuntu 22 +)
wget -O - https://openresty.org/package/pubkey.gpg | sudo gpg --dearmor -o /usr/share/keyrings/openresty.gpg

#4. Añadir el repositorio en sistemas x86_64 or amd64 para Ubuntu 22 +
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/openresty.gpg] http://openresty.org/package/ubuntu $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/openresty.list > /dev/null

#5. Actualizar el índice APT
sudo apt-get update

#6. Instalar el paquete
sudo apt-get -y install openresty

#6.1. Aquí da un error de que el puerto 80 está ocupado
#     Por tanto hay que entrar en /usr/local/openresty/nginx/conf/nginx.conf
#     y cambiar "listen       80;" 
#           por "listen       8080;" u otro puerto que no esté pillado
#     y volver a ejecutar :
#     sudo apt-get -y install openresty


#7. Verificar la instalación que debe devolver: nginx version: openresty/X.Y.Z
openresty -v

#8. Arrancar/rearrancar/ para rel servicio
sudo systemctl start openresty
sudo systemctl restart openresty
sudo systemctl stop openresty

#9. Permitir el servicio al arrancar (boot)
sudo systemctl enable openresty

#10. Ver el estado del servicio
sudo systemctl status openresty


Ojo, ahora el fichero de configuración esta en una de estas 2 carpetas:
  • /etc/openresty
  • /usr/local/openresty

Y los logs están en:

  • /usr/local/openresty/nginx/logs/error.log

Y aparece el mismo error que el puerto 80 está ocupado (después del punto 6)

Para solucionarlo hay que ir al fichero de configuración y cambiar el listen 80 por el puerto 8088 en el fichero "/usr/local/openresty/nginx/conf/nginx.conf"

Para ver que funciona, arrancar el servicio de openresty y abrimos el navegador con esta direccion:

http://IP_SERVIDOR:8088/

y vemos que funciona


3. Instalación de luarocks 

Luarocks es un paquete de instación de módulos Lua para openresty

Para instalarlo hacemos:

# 1.Prerequisitos
sudo apt install build-essential libreadline-dev unzip

# 2. Descargar
wget https://luarocks.org/releases/luarocks-3.12.2.tar.gz

# 3. Descomprimir
tar zxpf luarocks-3.12.2.tar.gz

# 4. Compilar e instalar 
cd luarocks-3.12.2
sudo ./configure && make && sudo make install
sudo luarocks install luasocket
lua

# Debe de aparecer
Lua 5.4.7  Copyright (C) 1994-2024 Lua.org, PUC-Rio

# 1.Instalar paquetes.
# En este caso instala el paquete lua-python para poder ejecutar modulos python
sudo luarocks install lua-resty-http

sudo luarocks install lua-resty-openssl

sudo luarocks install lua-resty-iputils

# 2. Ver los módulos instalados
luarocks list


Veamos algunos usos de luarocks

A la hora de instalar lua han aparecido algunos problemas que se han solucionado con chatgpt

4. Copiar los ficheros de configuración de openresty

Los ficheros a copiar al directorio /usr/local/openresty/nginx/conf són:

1. nginx.conf: 
que es el fichero de configuración principal. Para cada una de las rutas permitidas, se redirige a otros ficheros de configuración. En principio actua en http://localhost:5001/ que se encuentra en la entrada upstream softpropsrv {server 127.0.0.1:5001;

Utiliza el fichero eu_filter.lua para filtrar las IPs de los usuarios de Europa
Entre otras cosas realiza:
  • con el fichero mime.types definimos los tipos de ficheros
  • se definen las rutas con location (/, /login, /softprop,/ /static,/protected,/logout ...) y sus ficheros de includes
  • un "rate limiting" para evitar un número de peticiones excesivas
  • filtra para solo los usuarios de Europa
  • definimos los ficheros donde se guardarán los accesos y los errores
  • definimos donde estan los certificados y claves para ssl
  • exigimos al cliente que presente certifiacado
  • en ca_all_certificates.cer tenemos todos los certificados de las CA de confianza
  • esd importante epecificar los ficheros "static" que se recogerán

worker_processes  auto;

#error_log  logs/error.log;
error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    error_log   /usr/local/openresty/nginx/logs/error.log info;

    access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;
    
    #--- BEGIN EDU
    # Enable Lua shared dictionary for sessions
    lua_shared_dict sessions 10m;
    
    # Load the resty modules we need
    lua_package_path '/usr/local/share/lua/5.1/?.lua;/usr/local/lib/lua/5.1/?.lua;/usr/local/share/lua/5.1/resty/?.lua;/usr/local/openresty/lualib/?.lua;/usr/local/openresty/nginx/conf/?.lua;/usr/local/openresty/site/lualib/?.lua;/usr/local/openresty/site/lualib/resty/?.lua;;';
    lua_package_cpath './?.so;/usr/local/lib/lua/5.1/?.so;/usr/lib/x86_64-linux-gnu/lua/5.1/?.so;/usr/lib/lua/5.1/?.so;/usr/local/lib/lua/5.1/loadall.so;/home/eduard/.luarocks/lib/lua/5.1/?.so;;';
        
    # Initialize the IP filter module at the http level
    init_by_lua_block {
        require("resty.core")
        require("resty.iputils").enable_lrucache()
    }
    # Enable cache
    proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=100m inactive=60m use_temp_path=off;

    # Creamos una bateria de servidores para softprop con un solo servidor
    upstream softpropsrv {
	server 127.0.0.1:5001;
        #server 192.168.10.5:5001;
#server edu.tavernes.es:5001; #server 0.0.0.0:5001; ##server 127.0.0.1:5001; ##server el_que_sea.com; } # Hacemos un "rate limiting" para evitar un número de peticiones excesivas # Define rate limit (10 requests per second, with a burst of 20) por IP limit_req_zone $binary_remote_addr zone=my_api_limit:10m rate=10r/s; #resolver 192.168.10.5; # Servidor de DNS #--- END EDU server { #listen 8080; listen 8449 ssl; #->New server_name localhost; #server_name edu.tavernes.es; #charset koi8-r; #access_log logs/host.access.log main; #-> Begin new # SSL certificate configuration #ssl_certificate /usr/local/openresty/nginx/conf/wildcard2023Nginx.crt; #ssl_certificate_key /usr/local/openresty/nginx/conf/wildcard2023Nginx.rsa; ssl_certificate /usr/local/openresty/nginx/conf/wildcard.tavernes.es.crt; ssl_certificate_key /usr/local/openresty/nginx/conf/wildcard.tavernes.es.key; # Client certificate part ssl_client_certificate /usr/local/openresty/nginx/conf/ca_all.cer; # Set of all the CA certrificates ssl_verify_client on; # Require client certificate ssl_verify_depth 2; # Optional SSL settings for security and performance ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; # Common proxy settings (optional) proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto https; location / { # Aplicamos el rate limiting a esta ruta limit_req zone=my_api_limit burst=30 delay=10; # Get info from client certificate #proxy_pass https://0.0.0.0:5001/login; # Forward request to Python app proxy_set_header X-SSL-Client-Cert $ssl_client_cert; proxy_set_header X-SSL-Client-Verify $ssl_client_verify; proxy_set_header X-SSL-Client-DN $ssl_client_s_dn; proxy_set_header X-SSL-Client-Serial $ssl_client_serial; # Filter by UE country content_by_lua_block{ local filter = require("eu_filter") local client_ip = ngx.var.remote_addr if not filter.filter() then ngx.status = ngx.HTTP_FORBIDDEN ngx.say("Access denied: Your location is not permitted.") return ngx.exit(ngx.HTTP_FORBIDDEN) end } #root html; #index index.html index.htm; return 301 /login; } # Location of static files location /static/ { #1. Solo ficheros dentro del servidor openresty (nginx) ## Are good both root and alias, but in alias you must append "static/" to the path ##root /home/eduard/MyPython/11.softprop-01; # Path where static files are stored #alias /home/eduard/MyPython/11.softprop-01/static/; # Path where static files are stored ## Use browser cache !!! #expires max; #access_log off; #gzip on; #gzip_types text/css application/javascript image/svg+xml; #2. Solo ficheros dentro del uno de los servidores que actuan como web servers del upstream "softpropsrv" proxy_pass https://softpropsrv/static/; proxy_set_header Host $host; proxy_ssl_verify off; # If using SSL proxy_cache my_cache; proxy_cache_valid 200 30d; proxy_cache_use_stale error timeout updating; add_header X-Cache-Status $upstream_cache_status; } # Serve the login form location = /login { include nginx.conf.login; } location = /initial { include nginx.conf.initial; } # Protected proxy location location /protected { include nginx.conf.protected; } location /softprop { # Timeout settings # Problems with slow processes #proxy_connect_timeout 90s; # Default is 60s #proxy_send_timeout 90s; # Default is 60s # Time for waiting a response proxy_read_timeout 1000s; # Default is 60s #send_timeout 90s; # Default is 60s include nginx.conf.softprop; } #location = /success { # include nginx.conf.success; #} location = /error { include nginx.conf.error; } location = /logout { include nginx.conf.logout; } #error_page 404 /404.html; # redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } }


2. nging.conf.login:
se aplica a la ruta /login y muestra un formulario que pide usuario y contraseña; para validar envía a la ruta /inicial. Si ya se había validado anteriormente reenvia a /protected/softprop/tree. Si detecta la cookie que se ha generado en /initial te da como ya validado.
Lee tres ficheros que definen la pantalla (form_style.css, form_script.js y form_body.html) y tambien el fichero xmauth.lua

 default_type text/html;
            content_by_lua_block {
                -- Check if already authenticated
                -- local session_id = ngx.var.cookie_session_id
                -- local tab_id = ngx.var.cookie_tab_id
                -- local sessions = ngx.shared.sessions
                -- local tabs = ngx.shared.tabs
                
                -- if session_id and sessions:get(session_id) and tab_id and tabs:get(tab_id) then 
                --     return ngx.redirect("/protected/softprop/tree")
                -- end
                
                -- Helper function to read the entire content of a file.
                local function read_file(filename)
                    local mypath="/usr/local/openresty/nginx/conf/"
                    local new_filename = mypath .. filename
                    local f, err = io.open(new_filename, "r")
                    if not f then
                        ngx.log(ngx.ERR, "failed to open file ", new_filename, ": ", err)
                        return nil
                    end
                    local content = f:read("*a")
                    f:close()
                    return content
                end
                -- End Helper function
                
                -- Create the tab_id
                --local resty_random = require "resty.random"
                --local tab_id = ngx.encode_base64(resty_random.bytes(32))
                local xmauth = require "xmauth"
                local tab_id = xmauth.random_string(32)
                local n = 0
                
                -- Read content of three files into three string variables.
                local form_style  = read_file("form_style.css","r")
                local form_script = read_file("form_script.js","r")
                local form_body =   read_file("form_body.html","r")
                local login_form = [[
                    <!DOCTYPE html>
                    <html>    
                        <head>
                            <title>Login</title>
                            <style>
                                $STYLE$
                            </style
                            
                        </head>
                        <body>
                            <script>
                                $SCRIPT$ 
                            </script>
                            $BODY$
                        </body>    
                    </html>
                ]]   
                login_form, n =string.gsub(login_form, "%$STYLE%$" , form_style)  
                login_form, n =string.gsub(login_form, "%$SCRIPT%$", form_script)  
                    
                form_body , n =string.gsub(form_body , "%$TAB_ID%$", tab_id)                            
                
                login_form, n =string.gsub(login_form, "%$BODY%$"  , form_body) 
                -- ngx.log(ngx.INFO, login_form)
                
                ngx.say(login_form)
            }


3. nginx.conf.initial

            content_by_lua_block {
                -- Get authentication credentials
                ngx.req.read_body()
                local args = ngx.req.get_post_args() or {}
                local username = args["username"]
                local password = args["password"]
                local tab_id   = args["tab_id"]
                
                -- certificate info
                -- local cert = ngx.var.ssl_client_cert
                local cert_verify=ngx.var.ssl_client_verify
                local cert_dn=ngx.var.ssl_client_s_dn
                
                ngx.log(ngx.INFO, "initial username " .. username )
                ngx.log(ngx.INFO, "initial password " .. password )
                ngx.log(ngx.INFO, "initial tab_id " .. tab_id )
                ngx.log(ngx.INFO, "initial cert_dn " .. cert_dn )
                
                if not username or not password or not tab_id then
                    ngx.redirect("/login")
                    return
                end
                
                -- Create HTTP connection
                ngx.log(ngx.INFO, "initial validating user to python service" )
                local http = require "resty.http"
                local cjson = require "cjson"
                local httpc = http.new()
                
                -- Connect to Python auth service
                -- local res, err = httpc:request_uri("http://127.0.0.1:5000/auth", {
                -- Per a que accepte un nom de domini (com "edu.tavernes.es" cal tenir 
                --    una sentencia "resolver IP_del_servidor_DNS".
                --    sinó no pot trobar el domini. Per tant cal ficar directament la Ip
                --    La sentencia següent no va, perquè bno troba "edu.tavernes.es" 
                --    local res, err = httpc:request_uri("https://edu.tavernes.es:5000/auth", {
                local res, err = httpc:request_uri("https://192.168.10.5:5000/auth", {
                    method = "POST",
                    body = cjson.encode({
                        username = username,
                        password = password,
                        cert_verify = cert_verify,
                        cert_dn = cert_dn                        
                    }),
                    headers = {
                        ["Content-Type"] = "application/json",
                    },
                    -- if we have problems with the certificates ???
                    ssl_verify = false  -- Disable SSL verification (useful for self-signed certs)
                })
                ngx.log(ngx.INFO, "initial after validating user to python service" )
                if not res then
                    ngx.log(ngx.ERR, "Failed to request: ", err)
                    ngx.redirect("/error")
                    return
                end
                
                local body = cjson.decode(res.body)
                local body_json=cjson.encode(body)
                ngx.log(ngx.INFO, "initial body=" .. body_json )
                
                if body.authenticated then
                    --- ngx.redirect("/success")
                    --- ngx.redirect("https://www.gva.es")
                    -- Create session
                    local sessions = ngx.shared.sessions
                    --local resty_random = require "resty.random"
                    local xmauth = require "xmauth"
                    --local session_id = ngx.encode_base64(resty_random.bytes(32))
                    --local session_id = xmauth.random_string(32)
                    local my_data = { username = username }
                    local my_data_json = cjson.encode(my_data)
                     
                    --sessions:set(session_id, my_data_json , 3600) -- 1 hour expiration
                    sessions:set(tab_id, my_data_json , 3600) -- 1 hour expiration

                    local user_tab = sessions:get(tab_id) or "5"
                    ngx.log(ngx.INFO,"Initail tab_id + user_tab: " .. tab_id .. '    ' .. user_tab)
                    
                    -- Set session and tab_id cookies for authentication
                    ngx.header["Set-Cookie"] = {
                        -- "session_id=" .. my_data_json .. "; HttpOnly; Path=/",
                        tab_id .. "=" .. my_data_json .. "; HttpOnly; Path=/",
                        -- "username=" .. username .. "; HttpOnly; Path=/"
                    }
                    
                    local cookies = ngx.header["Set-Cookie"]
                    if type(cookies) == "table" then
                        for _, cookie in ipairs(cookies) do
                            ngx.log(ngx.INFO,"Set-Cookie Header: " .. cookie)
                        end
                    else
                        ngx.log(ngx.INFO,"Set-Cookie Header: " .. cookies)
                    end
                    
                    --ngx.log(ngx.INFO, "initial redirecting to /protected/softprop/tree" )
                    ngx.log(ngx.INFO, "initial redirecting to /softprop/tree" )
                    
                    -- Redirect to protected area
                    --local my_url="/protected/softprop/tree?tab_id=" .. ngx.escape_uri(tab_id)
                    -- Redirect to softprop instead of protected area
                    local my_url="/softprop/tree?tab_id=" .. ngx.escape_uri(tab_id)
                    ngx.log(ngx.INFO, "initial my_url=" .. my_url)
                    
                    return ngx.redirect(my_url)
                else
                    ngx.redirect("/error")
                end
            }


nginx.conf.protected

            #root /home/eduard/MyPython/05.fasthtml/menus;  # Base directory for static files
            access_by_lua_block {
                local xmauth = require "xmauth"
                local tab_id = ngx.var.arg_tab_id  -- Get the 'tab_id' parameter
                if not tab_id then
                   ngx.log(ngx.INFO, "nginx.conf.protected: 1 NO parameter tab_id")
                   return ngx.redirect("/login")
                end
                ngx.log(ngx.INFO, "nginx.conf.protected: 2 parameter tab_id= '" .. tab_id )
                local user = xmauth.is_authenticated(tab_id)
                if not user then
                   ngx.log(ngx.INFO, "nginx.conf.protected: 3 NO user")
                   return ngx.redirect("/login")
                end
                -- If the user is authenticated, you can proceed:
                -- ngx.say("Welcome, " .. user .. "!")
                ngx.log(ngx.INFO, "Protected: Welcome authenticated," .. user .. "!")
                return ngx.redirect("/login")
            
                
            }
            
            # Remove /protected from the forwarded URI
            #rewrite ^/protected/(.*) /$1 break;
            rewrite ^/protected/(.*) /$1 redirect;
            
            #include nginx.conf.softprop;
            
            #proxy_pass http://softpropsrv;  # Forward to a backend if needed


4. nginx.conf.error

            default_type text/html;
            content_by_lua_block {
                local error_page = [[
                    <!DOCTYPE html>
                    <html>
                    <head>
                        <title>Error</title>
                        <style>
                            body { 
                                font-family: Arial, sans-serif;
                                display: flex;
                                justify-content: center;
                                align-items: center;
                                height: 100vh;
                                margin: 0;
                                background-color: #f0f2f5;
                            }
                            .error-container {
                                background: white;
                                padding: 20px;
                                border-radius: 8px;
                                box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
                                text-align: center;
                                color: #f44336;
                            }
                            .back-button {
                                margin-top: 15px;
                                padding: 8px 16px;
                                background-color: #f44336;
                                color: white;
                                border: none;
                                border-radius: 4px;
                                text-decoration: none;
                                display: inline-block;
                            }
                            .back-button:hover {
                                background-color: #da190b;
                            }
                        </style>
                    </head>
                    <body>
                        <div class="error-container">
                            <h2>Authentication Failed</h2>
                            <p>Invalid username or password.</p>
                            <a href="/login" class="back-button">Try Again</a>
                        </div>
                    </body>
                    </html>
                ]]
                ngx.say(error_page)
            }


5. nginx.softprop.conf

           access_by_lua_block {
               
           
               local url = ngx.var.request_uri
	           ngx.log (ngx.INFO, "nginx.conf - location /softprop: 0 The current URL is: " .. url)
               local xmauth = require "xmauth"
               local tab_id = ngx.var.arg_tab_id  -- Get the 'tab_id' parameter
               if tab_id then
                   tab_id= tab_id:gsub("[^%w]","")
                   ngx.log(ngx.INFO, "0. tab_id=" .. tab_id .. "0") 
               end    
               if url ~= "/softprop/login" then
                   if not tab_id then 
                       -- Test if the tab_id is a post parameter
                       local method = ngx.req.get_method()
                       if method == "POST" then
                           -- Read request body
                           ngx.req.read_body()
                           local post_args = ngx.req.get_post_args()  -- Get POST parameters
                           ngx.log(ngx.INFO, "nginx.conf - AAAAA-> post_args",table.concat(post_args, ", "))
                           local body_data = ngx.req.get_body_data() 
                           ngx.log(ngx.INFO, "nginx.conf - BBBBB-> body_data",body_data)
                           -- 1. Assume the `hx-vals` passed a parameter named "tab_id"
                           tab_id = post_args["tab_id"]
                           if tab_id then
                               tab_id= tab_id:gsub("[^%w]","")
                               ngx.log(ngx.INFO, "1. tab_id=" .. tab_id .. "1") 
                           end
                           
                           -- 2. Maybe the it is a multipart/form-data
                           if not tab_id and body_data then
                               tab_id = body_data:match('name="tab_id"%s*(.-)%-%-')
                               if tab_id then
                                   tab_id= tab_id:gsub("[^%w]","")
                                   ngx.log(ngx.INFO, "2. tab_id=" .. tab_id .. "2") 
                               end  
                           end    
                       end    
                       if not tab_id then
                           ngx.log(ngx.INFO, "nginx.conf - location /softprop: 1 NO parameter tab_id redirecting to /login")
                           return ngx.redirect("/login")
                       end
                   end
                   --else 
                       ngx.log(ngx.INFO, "nginx.conf - location /softprop: 2 parameter tab_id= " .. tab_id )
                       local user = xmauth.is_authenticated(tab_id)
                       if not user then
                           ngx.log(ngx.INFO, "nginx.conf - location /softprop: 3 NO user redirectiong to /login")
                           return ngx.redirect("/login")
                       else    
                           -- If the user is authenticated, you can proceed:
                           -- ngx.say("Welcome, " .. user .. "!")
                           ngx.log(ngx.INFO, "/SOFTPROP: Welcome authenticated," .. user .. "!")
                       end        
                   --end    
               else
                   ngx.log(ngx.INFO, "/SOFTPROP: Anem a /softprop/login per autehenticar-nos al python !")
               end
            }
            
           
        
            # Proxy to the backend server with the decoded path
            #proxy_pass http://0.0.0.0:5001;
            #proxy_pass http://softpropsrv;
            proxy_pass https://softpropsrv;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            
            # Ensure redirects from the backend work properly
            #proxy_redirect http://127.0.0.1:5001/ /;
            
            


6. nginx.conf.error:

            default_type text/html;
            content_by_lua_block {
                local error_page = [[
                    <!DOCTYPE html>
                    <html>
                    <head>
                        <title>Error</title>
                        <style>
                            body { 
                                font-family: Arial, sans-serif;
                                display: flex;
                                justify-content: center;
                                align-items: center;
                                height: 100vh;
                                margin: 0;
                                background-color: #f0f2f5;
                            }
                            .error-container {
                                background: white;
                                padding: 20px;
                                border-radius: 8px;
                                box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
                                text-align: center;
                                color: #f44336;
                            }
                            .back-button {
                                margin-top: 15px;
                                padding: 8px 16px;
                                background-color: #f44336;
                                color: white;
                                border: none;
                                border-radius: 4px;
                                text-decoration: none;
                                display: inline-block;
                            }
                            .back-button:hover {
                                background-color: #da190b;
                            }
                        </style>
                    </head>
                    <body>
                        <div class="error-container">
                            <h2>Authentication Failed</h2>
                            <p>Invalid username or password.</p>
                            <a href="/login" class="back-button">Try Again</a>
                        </div>
                    </body>
                    </html>
                ]]
                ngx.say(error_page)
            }

7. nginx.conf.logout:

            content_by_lua_block {
                local session_store = ngx.shared.sessions
                local tab_id     = ngx.var.cookie_tab_id
                -- local session_id = ngx.var.http_cookie and ngx.var.http_cookie:match("session=([^;]+)")

                -- if session_id then
                if tab_id then
                    session_store:delete(tab_id)
                    ngx.header["Set-Cookie"] = "tab_id=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT"
                end
                
                ngx.redirect("/login")
            }

5. Ficheros adicionales para el nginx.conf

1. mime.types:

types {
    text/html                                        html htm shtml;
    text/css                                         css;
    text/xml                                         xml;
    image/gif                                        gif;
    image/jpeg                                       jpeg jpg;
    application/javascript                           js;
    application/atom+xml                             atom;
    application/rss+xml                              rss;

    text/mathml                                      mml;
    text/plain                                       txt;
    text/vnd.sun.j2me.app-descriptor                 jad;
    text/vnd.wap.wml                                 wml;
    text/x-component                                 htc;

    image/avif                                       avif;
    image/png                                        png;
    image/svg+xml                                    svg svgz;
    image/tiff                                       tif tiff;
    image/vnd.wap.wbmp                               wbmp;
    image/webp                                       webp;
    image/x-icon                                     ico;
    image/x-jng                                      jng;
    image/x-ms-bmp                                   bmp;

    font/woff                                        woff;
    font/woff2                                       woff2;

    application/java-archive                         jar war ear;
    application/json                                 json;
    application/mac-binhex40                         hqx;
    application/msword                               doc;
    application/pdf                                  pdf;
    application/postscript                           ps eps ai;
    application/rtf                                  rtf;
    application/vnd.apple.mpegurl                    m3u8;
    application/vnd.google-earth.kml+xml             kml;
    application/vnd.google-earth.kmz                 kmz;
    application/vnd.ms-excel                         xls;
    application/vnd.ms-fontobject                    eot;
    application/vnd.ms-powerpoint                    ppt;
    application/vnd.oasis.opendocument.graphics      odg;
    application/vnd.oasis.opendocument.presentation  odp;
    application/vnd.oasis.opendocument.spreadsheet   ods;
    application/vnd.oasis.opendocument.text          odt;
    application/vnd.openxmlformats-officedocument.presentationml.presentation
                                                     pptx;
    application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
                                                     xlsx;
    application/vnd.openxmlformats-officedocument.wordprocessingml.document
                                                     docx;
    application/vnd.wap.wmlc                         wmlc;
    application/wasm                                 wasm;
    application/x-7z-compressed                      7z;
    application/x-cocoa                              cco;
    application/x-java-archive-diff                  jardiff;
    application/x-java-jnlp-file                     jnlp;
    application/x-makeself                           run;
    application/x-perl                               pl pm;
    application/x-pilot                              prc pdb;
    application/x-rar-compressed                     rar;
    application/x-redhat-package-manager             rpm;
    application/x-sea                                sea;
    application/x-shockwave-flash                    swf;
    application/x-stuffit                            sit;
    application/x-tcl                                tcl tk;
    application/x-x509-ca-cert                       der pem crt;
    application/x-xpinstall                          xpi;
    application/xhtml+xml                            xhtml;
    application/xspf+xml                             xspf;
    application/zip                                  zip;

    application/octet-stream                         bin exe dll;
    application/octet-stream                         deb;
    application/octet-stream                         dmg;
    application/octet-stream                         iso img;
    application/octet-stream                         msi msp msm;

    audio/midi                                       mid midi kar;
    audio/mpeg                                       mp3;
    audio/ogg                                        ogg;
    audio/x-m4a                                      m4a;
    audio/x-realaudio                                ra;

    video/3gpp                                       3gpp 3gp;
    video/mp2t                                       ts;
    video/mp4                                        mp4;
    video/mpeg                                       mpeg mpg;
    video/quicktime                                  mov;
    video/webm                                       webm;
    video/x-flv                                      flv;
    video/x-m4v                                      m4v;
    video/x-mng                                      mng;
    video/x-ms-asf                                   asx asf;
    video/x-ms-wmv                                   wmv;
    video/x-msvideo                                  avi;
}


2. eu_filter.lua:

-- EU IP Filtering for OpenResty with internal network detection
-- Save this as /usr/local/openresty/nginx/conf/eu_filter.lua

local _M = {}

-- Define EU country codes
local eu_countries = {
    AT = true, BE = true, BG = true, HR = true, CY = true, CZ = true, 
    DK = true, EE = true, FI = true, FR = true, DE = true, GR = true, 
    HU = true, IE = true, IT = true, LV = true, LT = true, LU = true, 
    MT = true, NL = true, PL = true, PT = true, RO = true, SK = true, 
    SI = true, ES = true, SE = true
}

-- Define your internal network ranges
local internal_networks = {
    "10.0.0.0/8",     -- RFC1918 private IPv4 range
    "172.16.0.0/12",  -- RFC1918 private IPv4 range
    "192.168.0.0/16", -- RFC1918 private IPv4 range
    "127.0.0.0/8"     -- Localhost
}

-- Precompile internal networks using iputils
local iputils = require("resty.iputils")
local internal_cidrs = iputils.parse_cidrs(internal_networks)

-- Function to get real client IP when behind proxies
function _M.get_client_ip()
    -- Check for X-Forwarded-For header (common in proxy setups)
    local xff = ngx.req.get_headers()["X-Forwarded-For"]
    if xff then
        -- Extract the original client IP (first in the chain)
        local ip = xff:match("^([^,]+)")
        if ip then
            return ip
        end
    end
    
    -- Check for other common proxy headers
    local real_ip = ngx.req.get_headers()["X-Real-IP"]
    if real_ip then
        return real_ip
    end
    
    -- Fall back to direct connection IP
    return ngx.var.remote_addr
end

-- Function to check if IP is in internal networks
function _M.is_internal_ip(ip)
    -- ngx.log(ngx.ERR, "is_internal_ip: " .. ip)
    -- ngx.log(ngx.ERR, "iputils.ip_in_cidrs: " .. tostring(iputils.ip_in_cidrs(ip, internal_cidrs)))
    return iputils.ip_in_cidrs(ip, internal_cidrs)
end

-- Function to check if IP is from EU using MaxMind GeoIP
function _M.is_eu_ip(ip)
    local geoip = require("resty.maxminddb")
    local mmdb, err = geoip.open("/usr/local/openresty/geoip/GeoLite2-Country.mmdb")
    
    if not mmdb then
        ngx.log(ngx.ERR, "Failed to open GeoIP database: ", err)
        return false
    end
    
    local res, err = mmdb:lookup(ip)
    if not res then
        ngx.log(ngx.ERR, "IP lookup failed: ", err)
        return false
    end
    
    local country_code = res:get("country", "iso_code")
    if not country_code then
        return false
    end
    
    return eu_countries[country_code] or false
end

-- Main filter function
function _M.filter()
    local client_ip = _M.get_client_ip()
    
    -- Allow internal network IPs
    if _M.is_internal_ip(client_ip) then
        return true
    end
    
    -- Allow EU IPs
    if _M.is_eu_ip(client_ip) then
        return true
    end
    
    -- Reject all other IPs
    return false
end

return _M


6. Ficheros adicionales para el nginx.conf.login

1. form_body.html: 

                    
                        <div class="login-container">
                            <h2>Login</h2>
                            <form action="/initial" method="POST">
                                <div class="form-group">
                                    <label for="username">Username:</label>
                                    <input type="text" id="username" name="username" required>
                                </div>
                                <div class="form-group">
                                    <label for="password">Password:</label>
                                    <input type="password" id="password" name="password" required>
                                </div>
                                <input type="hidden" id="tab_id" name="tab_id" value="$TAB_ID$"> <!-- hidden -->
                                <button type="submit">Login</button>
                            </form>
                        </div>
       


2. form_style.css:

                            body { 
                                font-family: Arial, sans-serif; 
                                display: flex;
                                justify-content: center;
                                align-items: center;
                                height: 100vh;
                                margin: 0;
                                background-color: #f0f2f5;
                            }
                            .login-container {
                                background: white;
                                padding: 20px;
                                border-radius: 8px;
                                box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
                            }
                            .form-group {
                                margin-bottom: 15px;
                            }
                            input {
                                width: 100%;
                                padding: 8px;
                                margin: 5px 0;
                                border: 1px solid #ddd;
                                border-radius: 4px;
                                box-sizing: border-box;
                            }
                            button {
                                width: 100%;
                                padding: 10px;
                                background-color: #4CAF50;
                                color: white;
                                border: none;
                                border-radius: 4px;
                                cursor: pointer;
                            }
                            button:hover {
                                background-color: #45a049;
                            }

3. form_script.js :

                                // Save to session storage the id_tab sent by the server
                                window.onload = () => {
                                	tabId = document.getElementById("tab_id").value;
                                	sessionStorage.setItem("id_tab", tabId);
                                };

4. xmauth.lua:

-- auth.lua
------------------------------------------------
-- Funció per a veure si s'esta autenticat
------------------------------------------------
local _M = {}

-- Function to test if the user is authenticated.
-- Returns the username if authenticated, or nil if not.
function _M.is_authenticated( tab_id )
    -- HttpOnly cookies cannot be accessed by ngx.var.http_cookie
    -- HttpOnly cookies must be accessed by ngx.var.cookie_<cookie_name> 
    -- So the names of the cookies must be known beforehand
    -- This code is for not HttpOnly cookies
    --    local cookie_header = ngx.var.http_cookie 
    --    local cookies = {}
    --    local cookie_header = ngx.var.http_cookie  -- Get all cookies from the request
    --    if cookie_header then
    --        for key, value in string.gmatch(cookie_header, "([^=]+)=([^;]+)") do
    --            cookies[key] = value  -- Store key-value pairs in a table
    --            ngx.log(ngx.INFO, "Cookie: " .. key .. " = " .. value)
    --        end
    --    end
    
      
    -- Let's access individual cookies: session_id
    --local session_id = ngx.var.cookie_session_id or "1"
    -- ngx.log(ngx.INFO, "xmauth: session_id=" .. session_id )
    --local tab_id     = ngx.var.cookie_tab_id or "2"
    -- ngx.log(ngx.INFO, "xmauth: tab_id=" .. tab_id )
    --local username   = ngx.var.cookie_username or "3"
    -- ngx.log(ngx.INFO, "xmauth: username=" .. username )
    
    --local my_cookie_key = "session_id"  -- Example dynamic cookie name

    -- Get the full cookie string from request headers
    
    if not tab_id then
    	ngx.log(ngx.INFO, "xmauth: -1 param tab_id NOT found")
    else
        ngx.log(ngx.INFO, "xmauth: 0 param tab_id= '" .. tab_id )
    end
    	
    local cookie_string = ngx.var.http_cookie

    if not cookie_string then
        ngx.log(ngx.INFO, "xmauth: 1  No cookies found")
        return nil   
    else
        -- Build the pattern to match the dynamic cookie name
        local pattern = tab_id .. "=([^;]+)"
    
        -- Extract the cookie value
        local cookie_value = cookie_string:match(pattern)

        if not cookie_value then
            ngx.log(ngx.INFO, "xmauth: 2 Cookie '" .. tab_id .. "' not found")
            return nil
        end
    end

    local sessions = ngx.shared.sessions
    local user_data_json = sessions:get(tab_id) 
    
    if not user_data_json then
        ngx.log(ngx.INFO, "xmauth: 3 Cookie '" .. tab_id .. "' has no data")
    end
    
    local cjson = require "cjson"
    local success_user, user_data = pcall(cjson.decode, user_data_json)
    
    if not success_user then
        ngx.log(ngx.INFO, "xmauth: 4 Error getting data from Cookie '" .. tab_id )
        return nil
    else
        return user_data.username
    end    

end



function _M.random_string(length)
    math.randomseed(ngx.time() + ngx.worker.pid())  -- Seed RNG with time & worker ID
    local chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
    local str = {}
    
    for i = 1, length do
        local rand_index = math.random(1, #chars)
        str[i] = chars:sub(rand_index, rand_index)
    end

    return table.concat(str)
end

return _M


5. Copiar los certificados y rearrancar el servicio

Copiamos los certificados que tenemos

ca_all.cer, widlcard*

y rearrancamos el servicio

sudo systemctl restart openresty

y vamos a https://IP_SERVIDOR:8449/softprop/openresty y nos pide un certificado para acceder


martes, 26 de agosto de 2025

DBeaver. Obtención de las contraseñas de las conexiones a las BD

 Desde el vídeo de Codewis explican como extraer las contraseñas de las conexxiones a las BBDD.

1. File - Export -DBeaver- Project -Next


2. Elegimos "General y le indicamos fichero y carpeta


3. Abrimos elfichero "General-20250826.dbp" con un editor de fichero comprimidos y extraemos el fichero "credentials-confog.json"

4. Abrimos una ventana de comando y nos vamos a la carpeta donde hemos dejado el fichero  "credentials-config.json" y ejecutamos:

openssl aes-128-cbc -d -K babb4a9f774ab853c96c2d653dfe544a -iv 00000000000000000000000000000000 -in "credentials-config.json" >credenciales.txt

Con ello desencriptamos el contenido en un fichero dentro de otro fichero "credenciales.txt" pero que hay que tener cuidado al abrirlo pues tiene algunos caracteresd extraños que algunos programas de trataiento de texto no pueden leer. 

A una mala , se puede obviar la parte que final de la sentincia que lo vuelca a un fichero:

openssl aes-128-cbc -d -K babb4a9f774ab853c96c2d653dfe544a -iv 00000000000000000000000000000000 -in "credentials-config.json" 

y se puede ver por pantalla. 




lunes, 25 de agosto de 2025

WEBPROPv2 (V). Validación de usuarios sin nginx (III). Log in. Segundo factor de autenticación

 0.Introdución

En el módulo routes01mnu.py, en el endpoint "/softprop/validate_login" que estaba definido en la función  post_login estaba este código que hace referencia al módulo xmtotp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
        # 2. Comprobamos si tiene TOTP
	has_totp, totp, image_path=xmtotp.get_totp(login.usuari)
	
	# 3. Si no ha introducido el código TOTP mostramos la pantalla 
	#    para que lo ingrese
	no_digits=login.totp_digits is None
	if no_digits:
		print("Mostrando el input para introducir el TOTP")
		return xmother.get_totp_html(fh, has_totp, image_path)

	# 4. No ha introducido el TOTP correcto
	totp_correcto= xmtotp.validate_totp(totp, login.totp_digits, login.usuari)
	if not totp_correcto:
		print("Mostrando otra vez el input para introducir el TOTP")
		return xmother.get_totp_html(fh, has_totp, image_path, True)	


Veamos pues el módulo xmtotp.py

 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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
import os
import pyotp
import qrcode

from basicutils import xmdb, xmother
from models.xmusermodels import UserStore, UserStoreHst

def get_totp(username:str, service:str='Softprop@Tav'):
	"""
	Generates a new TOTP secret and QR code for the given username and service.
	
	Args:
		username (str): The username for which to generate the TOTP secret.
		service (str): The service name for which the TOTP is being generated.
		
	Returns:
		tuple: A tuple containing:
			- has_totp (bool): True if the user already has a TOTP secret, False otherwise.
			- totp (pyotp.TOTP): The TOTP object.
			- image_path (str): The path to the saved QR code image.
	"""
	# Generate a new TOTP secret
	secret=pyotp.random_base32()
	has_totp=False
	has_2fa=False
	image_path_url=None
	myuser=None
	
	# Check if the user already has a TOTP secret
	with xmdb.session_local() as db:
		try:
			myuser=db.query(UserStore).filter(UserStore.ldap_user==username).first()
			#if myuser and myuser.totp_activated: 
			if myuser and myuser.totp_secret is not None and len(myuser.totp_secret.strip())>0: 
				secret=myuser.totp_secret
				has_2fa=True
				if myuser.totp_activated: 
					has_totp=True
		except Exception as e:
			print(f"An error occurred while querying the user: {e}")
			return False, None, None

		
		totp = pyotp.TOTP(secret)
		
		
		if not has_2fa:
			# Update the user with the new TOTP secret
			myuser.totp_secret=secret
			myuser.totp_user=username
			myuser.totp_service=service
			myuser.totp_activated=False
			
			xmdb.merge_to_main_and_histo(db, myuser, UserStore, UserStoreHst)
			db.commit() 

		# Si no se ha autenticado antes
		if not has_totp:	
			# Generate the provisioning URI and save image
			uri = totp.provisioning_uri(name=username, issuer_name=service)
			# Generate the QR code image	
			image_path_file=xmother.get_project_path()+os.sep+'static'+os.sep+'qrcodes'+os.sep+username+'_qrcode.png'
			image_path_url='/static'+os.sep+'qrcodes'+os.sep+username+'_qrcode.png'
			print (image_path_file, image_path_url)
			qrcode.make(uri).save(image_path_file)
		# Check if the image was saved successfully	
	return has_totp, totp, image_path_url

def validate_totp(totp, code:str, username:str)->bool:
	"""
	Validates the provided TOTP code against the generated TOTP object.
	
	Args:
		totp (pyotp.TOTP): The TOTP object.
		code (str): The TOTP code to validate.
		
	Returns:
		bool: True if the code is valid, False otherwise.
	"""
	si_verifica=totp.verify(code) or username=='eduard' # For testing purposes, allow 'eduard' to bypass TOTP verification
	if si_verifica:
		with xmdb.session_local() as db:
			try:
				myuser=db.query(UserStore).filter(UserStore.ldap_user==username).first()
				if myuser and myuser.totp_activated==False:
					myuser.totp_activated=True
					xmdb.merge_to_main_and_histo(db, myuser, UserStore, UserStoreHst)
					db.commit() 	
			except Exception as e:
				print(f"An error occurred while querying the user: {e}")
				return False
	return si_verifica








WEBPROPv2 (IV). Validación de usuarios sin nginx (II). Log in. Utilización del LDAP

 0. Introducción

En el módulo routes01menu.py en el endpoint "/softprop/validate_login" que está definido por la función post_login, hacemos esta llamada

ldap_authenticated= xmldap.autheticate_by_login(login.usuari, login.paraula_de_pass)

Por tanto todo el tema de autenticación por LDAP se realiza en módulo "xmldap.py"

1. el módulo xmldap.py

Este módulo accede a unas propiedades para poder conectarse al servidor LDAP.

En concreto el fichero que guarda las propiedades tiene este contenido:

ldapWindows:
  serverName: mi-servidor.edificio.poblacion
  userPrefix: edificio\

Veamos el código del módulo xmldap.py

 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
from ldap3 import Server, Connection, ALL, SUBTREE, ALL_ATTRIBUTES ,SAFE_SYNC, NTLM, ALL_ATTRIBUTES
import sys
import os
#------Imprescindible para poder importar de otras carpetas (de basicutils)
import sys
from pathlib import Path
path_root1 = Path(__file__).parents[1] # Damos un salto de directorio hacia arriba-> ximoutilsmod
sys.path.append(str(path_root1))
path_root0 = Path(__file__).parents[0] # El mismo directorio
sys.path.append(str(path_root0))
from basicutils import xmcrypto, xmfile
# ------Fin imprescindible


def get_props():
	logs=[]
	logs.append('env='+str(os.environ))
	logs.append('os.getenv("CONF_PYTHON")='+str(os.getenv("CONF_PYTHON")))
	xmfile.add_lines_to_file('/home/eduard/kk.log', logs)
	conf_file=os.getenv("CONF_PYTHON")+'ldap.encrypted.yml'
	config= xmcrypto.get_properties_from_file(conf_file)
	return config['ldapWindows']

'''Autheticate by login name and password'''
def autheticate_by_login(username: str, password: str)->bool:
	config=get_props()
	try:
		server = Server(config['serverName'], get_info=ALL)
		my_user=config['userPrefix']+username
		conn = Connection(server, user=my_user, password=password)
		if conn.bind():
			print('xmldap.autheticate_by_login: OK')
			return True
		else:
			print('xmldap.autheticate_by_login: FALSE')
			return False
	except Exception as e:
		print(f"LDAP error1 : {e}", file=sys.stderr)
		if 'conn' in locals():
			conn.unbind()
		return False

Lo importante de este código es:

  • La importación de los objetos necesarios de la biblioteca ldap3, en este caso Server y Connection que se encuentra en la primera línea
  • La función get_props solamente sirve para leer las propiedades que se muestran arriba que se encuentran en un fichero encriptado
  • Para ver si las credenciales son correctas primeramewnte se define el servidor con solo decir su nombre (Línea  28).
  • El usuario hay que pasarlo con un prefijo que se encuentra en propiedades (Línea 29)
  • Si se intenta hacer la conexión al servidor con el usuario (+ prefijo) y contraseña introducida, yu es correcta entonces las credenciales del usuario son correctas y quedan validadas.(Líneas 28-33)
  • Si no se establece la conexión, entonces hya un error (seguramente de credenciales) 



viernes, 22 de agosto de 2025

WEBPROPv2 (III). Validación de usuarios sin nginx (I). Log in. Visión general

 0. Introducción

Ya se vió en el apartado anterior que había una función "beforeware" que salvo los "endpoints" de recursos (imágenes, css, etc) verificaba que el usuario se había identificado correctamente, En caso contrario lo enviaba a una pantalla de "log in" con doble factor de identificación,

Por otra parte el tratamiento de los endpoints está definido en los siguientes módulos python:

  • routes00comp.py : Pruebas de acciones que fectan al departamento de informatica
  • routes01mnu.py: Acciones del menú y de "log in"
  • routes02form: Acciones del formulario
  • routes03grid:Acciones de la grid

1. Proceso de "Log in"

Para accedir a la pantalla de login de forma directa se introduce3 esta url:

https://edu.tavernes.es:5001/softprop/login

El tratamiento de este endpoint está en el módulo routes01mnu.py tal como se vió en la introducción 

La pantalla es esta:



Primero te pide usuario y contraseña y luego te pide el segundo factor de autenticación.

1.1 Primera pantalla del log in

Solamente nos pide usuario y contraseña. El endpoint tratado en routes01mnu.py tiene el siguiente código específico para esta pantalla, donde se hace una llamada a xmloginform.login_form()

@app.get("/softprop/login")
def get_login():
	''' Primero muestra el formulario de login sin campos de TOTP'''
	a=xmloginform.login_form() 
	return a 

Y el código específico para crear el formulario es:

 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
def login_form():
	''' Simple login form '''
	# 1. js to save the tab:id in th session storage
	with open(f"{xmother.get_project_path()}/static/js/form_script.js", newline='') as f: myjs=f.read()
	
	# 2. Crete the random string of the tab_id 	
	tab_id = xmother.get_random_string()

	# 3. Fields in the form
	fields=[['usuari','text'],['paraula_de_pass','password'],['tab_id','hidden']]
	buttons=[
		{'name':'Identificar-se', 'type':'submit', 'cls':'btn btn-primary'},
		{'name':'Cancel·lar'    , 'type':'reset' , 'cls':'btn btn-secondary'}
	]	
	
	return fh.Div(
		fh.Script(code=myjs), # Este js guarda en la session store el tab_id com id_tab
		fh.H2("Identificar-se al sistema"),
		fh.Form(
			*[
				fh.Div(
					fh.Label(field[0].capitalize().replace("_"," ") , **{'cls':"form-label", 'for':field[0]}) if field[0] !='tab_id' else '',
					fh.Input(
						type=field[1], 
						value=tab_id if field[0]=='tab_id' else "",
						** {'name':field[0], 'id':field[0], 'cls':"form-control"}),
					cls="mb-3" 
				) for field in fields 
			],
			fh.Div(
				fh.Div(id="error-message", cls='text-danger text-center mb-3'),
				id="totp_div"
			),	
			fh.Div(
				*[fh.Button(button.get("name"), ** button) for button in buttons ],
				cls="d-flex justify-content-between"
			),
			id="my_form",
			# COMMENT:hx_post="/softprop/validate_login?tab_id="+tab_id, hx_target="#error-message", hx_swap="innerHTML"
			hx_post="/softprop/validate_login?tab_id="+tab_id, hx_target="#totp_div", hx_swap="innerHTML"

			
		),
		#fh.Form(id="logout-form", style="display-none", action="/logout", method="POST"),
		cls="card shadow-lg p-4", style="width: 350px;",
		
	)

Donde:
  • Se recoge el código javascript desde el fichero específico "form_script.js" para ser insertado mediante la sentencia fh.Script() (Líneas 4 y 17)
  • Se definen los campos (usuario y contraseña) y los tipos de controles que son y se insertan en el formulario (Líneas 10, 20-29)
  • Se definen los botones de "Identificarse" y "cancelar" (Líneas 11-14, 33-36)
  • Se define el campo de solo lectura de mensajes (Línea 29-32)
  • Se redirige al endpoint "/softprop/validate_login" donde se recoge el mensaje resultante de la ejecución y se crea dicho mensaje mediante adición de código html (hx_target) en el "div" cuyo id es "totp_div"

1.2 Endpoint /softprop/validate de routes01menu.py que valida usuario y contraseña y el segundo factor de autenticación

Veamos el código:

 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
@app.post("/softprop/validate_login")
def post_login(req, login: xmclasses.Login):
	''' 1. Verifica usuario y contraseña
	 	2. Pregunta un doble factor de autenticación TOTP
		3. Añade una cookie httponly con la clave "tab_id" con valores:
		 	3.1 Usuario
			3.2 Duracion una hora (3600 segundos) 
	      
	'''
	# -------- COMPROBACION DE ERRORES -------
	# 1. Si no autenticamos al usuario con el login y la password
	#    Le devolvemos un error
	ldap_authenticated= xmldap.autheticate_by_login(login.usuari, login.paraula_de_pass)
	if not ldap_authenticated:
		print ("returning to first login page")
		return fh.Div("Error en l'usuari o la paraula de pas", cls="alert alert-danger")

	# 2. Comprobamos si tiene TOTP
	has_totp, totp, image_path=xmtotp.get_totp(login.usuari)
	
	# 3. Si no ha introducido el código TOTP mostramos la pantalla 
	#    para que lo ingrese
	no_digits=login.totp_digits is None
	if no_digits:
		print("Mostrando el input para introducir el TOTP")
		return xmother.get_totp_html(fh, has_totp, image_path)

	# 4. No ha introducido el TOTP correcto
	totp_correcto= xmtotp.validate_totp(totp, login.totp_digits, login.usuari)
	if not totp_correcto:
		print("Mostrando otra vez el input para introducir el TOTP")
		return xmother.get_totp_html(fh, has_totp, image_path, True)	
	
	#------ VAMOS A LA PANTALLA DE MENU ----------------
	#5. Si ha introducido el código TOTP correcto
	#   Mostramos el menu
	a_user=''
	with xmdb.session_local() as db:
		a_user, myerror=xmsession.set_session(req=req, db=db, user=login.usuari, tab_id=login.tab_id)
	
	if len(a_user.strip())==0: 
		print ("returning to the first login page")
		return fh.Div("Error al crear la sessió: "+myerror, cls="alert alert-danger")
	
	ck_dict={'httponly':True, 'max_age':3600}
	tab_id_dict={ "username": login.usuari }
	# COMMENT: sec_dict={'secure': True}
	cookies_dicts=[
		# COMMENT: {'key':"session_id", 'value':session_id }  | ck_dict,
		{'key':login.tab_id, 'value':json.dumps(tab_id_dict)} | ck_dict,
		# COMMENT: {'key':"username"  , 'value':login.usuari} | ck_dict 
	]	
	#COMMENT: redirection='/softprop/tree?tab_id='+urllib.parse.quote(login.tab_id)
	redirection='/softprop/tree?tab_id='+login.tab_id
	a=fh.RedirectResponse(redirection, status_code=303)
	for cookie in cookies_dicts:
		a.set_cookie(**cookie)
	print('passant per /softprop/validate_login i anem a /softprop/tree ja que ens hem validat')	
	return a

Donde:
  • Primeramente se consulta al "LDAP" para verificar el usuario y contraseña. (Línea 13)
  • En la primera pantalla solo se piede usuario y contraseña y no se tipde el código de 2º factor (TOTP). Es por ello que si despues de verificar el usuario y contraseña detecta que no se ha introducido el 2º factor, te añade el código HTML a la pantalla para pedir el 2º factor (Líneas 19-26)
  • Se verifica si el código TOTP (2º factor) és correcto. Si no es corre cto lo vuelve a pedir (Líneas  28-32)
  • Se guarda la sesión en BD. Si hubiera algún error al crearla, se reportaría (Líneas  37-44)
  • En las líneas 45 a 57, se crea una cookie httponly (que se guarda en el navegador y es solo accesible desde el servidor para dar mayor seguridad) cuya duración es de una hora (3600 segundos). En dicha cookie se guardan estos valores:
    • key: el tab_id generado
    • value.username: el nombre del usuario
    • httponly: true
    • max_age: 3600
  • Se redirige a la pantalla del menu tree (menú de arbol) con el código de status 303 que corresponde a la redirección








jueves, 21 de agosto de 2025

WEBPROPv2 (II). Configurar el objeto app de FastHTML (mnu_fh.py). Exception handllers. Beforeware. Mounting folders

0. Introducción

Se ha visto que el módulo que  tiene el "main" es mnu_main.py que ejecuta el servidor web uvicorn al que se el pasa entre otros parámetros el objeto "app".

Pues en este post veremos como se configura el objeto app para que utilice recursos como js o css, utilice rutas estáticas, redirija excepciones y utilice una función para que antes de ir a una ruta o "endpoint" compruebe que el usuario tiene permiso para ello.

1. Configuración del objeto app

Se realiza en el módulo menu_fh.py :

  • Crea la lista de "headers" (líneas 21-43):
    • Utiliza la codificación UTF-8
    • Par un diseño "responsive" utiliza la etiqueta "meta" para asignar toda la anchura del dispositivo y hacer una escalado del 100%
    • Adjunta o enlaza librerias js o css:
      • Font Awesome
      • El css propio para el tree.menu
      • Libreria bootstrap
      • Librería Ag-Grid
  • Crea un manejador de las excepciones 400, 403 y 404 (líneas 45-49)
  • Se define una funcion tipo "Beforeware" que se ejecutará en todos los "endpoints". Se especifica su funcionamiento más adelante (Líneas 52-88)
  • Se montan las carpetas "static" y "downloads" a un path de la app (líneas 93-94)
  • Se añaden estos objetos al objeto app (líneas 86-90)
 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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#1. Imports

from fastapi.staticfiles import StaticFiles
from fasthtml import common as fh


#------Imprescindible para poder importar de otras carpetas (de basicutils)
import sys
from pathlib import Path
path_root1 = Path(__file__).parents[1] # Damos un salto de directorio hacia arriba-> ximoutilsmod
sys.path.append(str(path_root1))
path_root0 = Path(__file__).parents[0] # El mismo directorio
sys.path.append(str(path_root0))
# ------Fin imprescindible

from basicutils import xmother
from session import xmsession


#1. Defining Headers (Styles, Scripts, etc)
my_headers = [
	fh.Meta(charset='UTF-8'),

	# Responsive design:  Uses the width of the device and renders the page at 100% zoom (no auto zoom)
	fh.Meta(name='viewport', content='width=device-width, initial-scale=1'),
	
	# Link libraries of js and css

	#  1. Font Awesome
	fh.Link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css'),
	
	#  2. My Menu-tree library
	fh.Link(rel='stylesheet', href='../static/css/tree-menu.css'),
	
	#  3. Bootstrap
	fh.Link(href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css", rel="stylesheet"),
        fh.Script(src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js"),
        fh.Script(src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js"),
	
	#  4. Ag-Grid
	fh.Script(src="https://cdn.jsdelivr.net/npm/ag-grid-community/dist/ag-grid-community.min.js"),
	
]

exception_handlers={
        400: lambda req, exc: fh.Titled("400: Bad Request: La petició no és correcta."),
	403: lambda req, exc: fh.Titled("403: Forbidden: No té permís a esta pàgina."),
	404: lambda req, exc: fh.Titled("404: Not Found: No existeix esta pàgina!"),
}

#2. Beforeware function to be executed in all endpoints
async def my_before_function(req, tab_id, sess):
	'''
	Beforeware function: checks that the user is authenticated
	'''
	print('Des de beforeware, la URL es:', req.url)
	# 1. Validem que haja tab_id i que l'usuari estiga en la sessió de la BD
	if tab_id is None: tab_id= req.query_params.get('tab_id', None)

	if tab_id is None:	
		try:
			tab_id= (await req.form()).get('tab_id', None)
		except Exception as e:
			print("Error al llegir el tab_id del form:", e)
			tab_id = None
	if tab_id is None:	
		try:
			tab_id= (await req.json()).get('tab_id', None)
		except Exception as e:
			print("Error al llegir el tab_id del JSON:", e)
			tab_id = None	

	my_response,myerror=xmsession.validate(tab_id=tab_id, req=req)
	if (len(myerror.strip())>1):
		print ("redirecting from my_before_function " )
		return my_response

#OJO: Aquelles rutes que comecen per "/softprop/nchk/" no seran comprovades l'usuari
my_skip=[r'/favicon\.ico', r'/static/.*', r'.*\.css', 
		 r'/softprop/nchk/.*',
		 '/softprop/login', '/softprop/validate_login', 
		]
my_beforeware= fh.Beforeware(my_before_function, skip=my_skip)

#3. Creating FastAPI App
app = fh.FastHTML(
	hdrs=my_headers, 
	before=my_beforeware,
	secret_key=xmother.new_id(25), 
	exception_handlers=exception_handlers)

#3. Mounting the static folder for accessing the menu.css
app.mount("/static"   , fh.StaticFiles(directory="static"   ), name="static"   )
app.mount("/downloads", fh.StaticFiles(directory="downloads"), name="downloads")

2. Exception handlers

Es un parámetro del objeto app que és una lista de diccionarios que se le indica el código hhtp del error y lo que se tiene que mostrar. En este caso se muestra una pantalla cuyo título es la descripción del error.

El código se muestra muestra el detalle:

exception_handlers={
        400: lambda req, exc: fh.Titled("400: Bad Request: La petició no és correcta."),
	403: lambda req, exc: fh.Titled("403: Forbidden: No té permís a esta pàgina."),
	404: lambda req, exc: fh.Titled("404: Not Found: No existeix esta pàgina!"),
}


3. La función "beforeware"

Un beforeware es una función que intercepta las peticiones HTTP antes de que lleguen al manejador (es decir, antes del código que atiende /ruta/...), para:
  • Validar autenticación/autorización.
  • Añadir datos a la petición.
  • Redirigir, denegar o registrar información.
  • Omitirse en rutas específicas si se configura.
En este caso, el beforeware comprueba si el usuario está autenticado mediante un tab_id.

Veamos que es lo que contiene el código:
  • Definimos una función asíncrona que recibe como parámetros la "request", el "tab_id" y la "sessión" (Línea 2)
  • Esta función recibe como parámetro el tab_id, y si no se le pasa dicho parámetro lo recoge de la "request" (Línea 8)
  • Si no lo encuentra lo busca primeramente en el FORM i si no en el BODY (Líneas 11-24)
  • Se valida mediante el tab_id con la llamada a la función xmsession.validate () (Línea 26)
  • Se definen aquellas rutas (endpoints) que no hace falta validar con el tab_id, que constituyen una lista de "strings" que puden incluir expresiones regulares. (Líneas 32-35)
  • Se asocia la función anterior como "beforeware global" que actuará en todos los endpoints excepto aquellos que se le indican en el parámetro "skip" (Línea 36)
  • Se le passa el beforeware con el parámetro "before" al objeto "app" (Línea 41)
 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
#2. Beforeware function to be executed in all endpoints
async def my_before_function(req, tab_id, sess):
	'''
	Beforeware function: checks that the user is authenticated
	'''
	print('Des de beforeware, la URL es:', req.url)
	# 1. Validem que haja tab_id i que l'usuari estiga en la sessió de la BD
	if tab_id is None: tab_id= req.query_params.get('tab_id', None)

	# 2. If tab_id is not in the query parameters, we try to get it from the FORM 
	if tab_id is None:	
		try:
			tab_id= (await req.form()).get('tab_id', None)
		except Exception as e:
			print("Error al llegir el tab_id del form:", e)
			tab_id = None
	
	# 3. If tab_id is still None, we try to get it from the JSON body
	if tab_id is None:	
		try:
			tab_id= (await req.json()).get('tab_id', None)
		except Exception as e:
			print("Error al llegir el tab_id del JSON:", e)
			tab_id = None	

	my_response,myerror=xmsession.validate(tab_id=tab_id, req=req)
	if (len(myerror.strip())>1):
		print ("redirecting from my_before_function " )
		return my_response

#OJO: Aquelles rutes que comecen per "/softprop/nchk/" no seran comprovades l'usuari
my_skip=[r'/favicon\.ico', r'/static/.*', r'.*\.css', 
		 r'/softprop/nchk/.*',
		 '/softprop/login', '/softprop/validate_login', 
	]
my_beforeware= fh.Beforeware(my_before_function, skip=my_skip)

#3. Creating FastAPI App
app = fh.FastHTML(
	hdrs=my_headers, 
	before=my_beforeware,
	secret_key=xmother.new_id(25), 
	exception_handlers=exception_handlers)

4. La función xmsession.validate para ver si el usuario tiene permiso

En el módulo xmsession se tiene esta función que :
  • Se crea una ruta de redirección para el caso de error que es "/softprop/login" para que ingrese las credenciales al sistema.
  • El primer error que lnaza es no pasar-le un tab_id
  • Si el tab_id no está presente en las cookies que duran 3600 segundos lanza error
  • Compueba que el tab_id esté en la sessión guardada en la BD 
  • Si hay error lo devuelve a la pantalla de login


 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
def validate(tab_id:str, req)->tuple:
	redirection='/softprop/login'
	my_error=""

	# 1. Si es la primera vegada, no tenim tab_id i envia un script per a arreplegar-lo
	if tab_id is None: 
		my_error="No s'ha inclòs el tab_id en el request"
			
	#2. Check if the tab_id is in the cookies
	#   Cookies that has finished the max_age=3600 seconds are not present in the session
	if len(my_error)==0:
		cookies = req.cookies
		if tab_id not in cookies.keys(): 
			my_error="El tab_id no està present en el cookies, pot estar caducat"

	#3. Verifiquem que l'usuari existisca i estiga actiu
	if len(my_error)==0:
		with xmdb.session_local() as db:
			user, my_error=test_session(req, db, tab_id)
	
			if len(user.strip())==0 and len(my_error.strip())==0:
				my_error="Usuari no trobat"
			
			# 4. Guardem la petició de l'usuari per a fer una auditoria
			else:
				user_request=RequestStore(
					description=str(req.url)+'|'+datetime.now(timezone.utc).strftime('%Y%m%d%H%M%S%f'),
					user=user,
					tab_id=tab_id,
					url = str(req.url),
					method=req.method,
					user_agent=get_user_agent(req),
					user_ip=get_user_ip(req),
					is_nginx=get_is_nginx(req)
				)
				user_request=db.merge(user_request)
				db.flush()		
				db.commit()

	# Si hi ha un error redireccionem altra vegada al login
	if len(my_error.strip())>0:		
		a=fh.RedirectResponse(redirection, status_code=303)
		print('enviem a  /softprop/login perque: ' + my_error)	
		return a, my_error  

	return '',''








WEBPROPv2 (I). Arranque de la aplicacion (mnu_main.py)

 1. Arranque de la aplicación

Vemos el módulo que arranca la aplicación. El arranque se realiza con la sentencia uvicorn.run().

Este módulo:

  • Arranca el servidor web unicorv
  • Le passa los parametros necesarios al servidor para arrancar con https:
    • nombre del servidor o ip, 
    • puerto, 
    • certificado (certifcdo + clave)
  • Crea las tablas en base al modelo objeto relacional
  • Utiliza el módulo menu_fh para configurar el objeto app de FastHTML 

El módulo se llama mnu_main.py

 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
#!/home/eduard/MyPython/11.softprop-01/venv_softprop/bin/python3

#1. Imports
import os
from sqlalchemy import Table
#------Imprescindible para poder importar de otras carpetas (de basicutils)
import sys
from pathlib import Path
path_root1 = Path(__file__).parents[1] # Damos un salto de directorio hacia arriba-> ximoutilsmod
sys.path.append(str(path_root1))
path_root0 = Path(__file__).parents[0] # El mismo directorio
sys.path.append(str(path_root0))
# ------Fin imprescindible

#--------------------------------------------------------
# NOTA: No eliminar las siguientes dependencias marcadas en gris 
#       pues, sinó falla el programa
#       De menus: routes01mnu, routes02form, routes03grid, app
#       De basicutils: xmdb, xmfunction
#       De menus.mnu_fh: fh
#       De models.xmallmodels: model_classes
#--------------------------------------------------------
from menus import routes00comp, routes01mnu, routes02form, routes03grid # No eliminar !!!
from basicutils import xmdb, xmfunction

# Ahora importamos las funciones de FastHtml pero mejoradas con nuevos componentes
from menus.mnu_fh import fh, app                         # No eliminar app !!!

#--- Definición de tablas de la Base de datos postgres
from models.xmallmodels import model_classes


# Create database tables
xmdb.Base.metadata.create_all(bind=xmdb.engine)

# Execute the web server, but now not using fh.serve but uvicorm.run instead
#fh.serve()
if __name__ == "__main__":
	import uvicorn
	cert_path="/home/eduard/MyPython/11.softprop-01/static/certs/wildcard.tavernes.es."
	uvicorn.run(app, host="edu.tavernes.es", port=5001,	
	#uvicorn.run(app, host="192.168.10.5", port=5001,	
		ssl_keyfile =cert_path+"key", 
		ssl_certfile=cert_path+"crt"
	)

A destacar:

Línea 1: El shebang que indica que entorno virtual ejecutar el programa

Lineas 4-5: Importación de librería del entorno virtual

Líneas 7-12: Truco para poder importar nuestras librerias (permite reconocer las rutas en 2 niveles de subcarpetas

Linea 27: Hemos delegado la configuración de la app de FastHTML al módulo mnu_fh donde se definen las rutas estáticas, las rutas que no vamos a revisar la seguridad ...

Lineas 30 y 34: Importamos las clases que originaran tablas(línea 30)  y las creamos (linea 34) 

Línea 39: importamos el servidor web "uvicorn"

linea 40: Definimos la ubicación del certificado de servidor junto con la raiz del nombre de los ficheros de clave y certifcado

Línea 41: Ejecutamos el servidor web, le indicamos la app que hemos complementado, la URL del servidor web (o IP) el puerto y la rutas del certificado y clave asociada. 


En una entrada mas adelante vermos como se crean servicios para arrancar a uvicorn y que no se quede colgado al no permitir que un usuario ejecute la aplicación si hay otro usuario que está ejecutando un proceso largo.