jueves, 25 de septiembre de 2025

WEBPROPv2 (VIII). Instalación en un servidor con nginx (II). Instalar el servicio de consulta al LDAP

 

1. Introducción

Tenemos que crear un programa para que verifique las credenciales de usuario y contraseña sobre el servidor LDAP

También debemos ponerlo en un servidor http para recibir dichos parámetros y devolver la autorización

2. Programa de autenticación LDAP (xmldap.py)

Veamos el código fuente:

La primera línea es de (Shebang #!) que indica el ejecutor del programa. Si en una ventana de comandos ejecutamos xmldap.py, ya sabe que python lo deber ejecutar, que es el de la ruta indicada en el shebang, que es la del entorno virtual que tiene todos los módulos necesarios instalados. (Pero en este caso no lo vamos a ejecutar con el shebang, sino el fichero python del siguiente apartado)

El módulo que nos da toda la gracia de acceso a LDAP es ldap3 que hay que instalarlo con "pip install ldap3" desde el entorno virtual

La función getProps se encarga de leer el fichero de propiedades para aceder al fichero de parámetros del servidor LDAP

La función autheticateByLogin se encarga de acceder al servidor LDAP y autenticar el usuario con su contraseña.

Las demás funciones son para obtener información del sistem LDAP. 

Veamos el código de xmldap.py

#!/home/informatica/eduApps/softprop1/venv_softprop/bin/python3

import pprint
from ldap3 import Server, Connection, ALL, SUBTREE, ALL_ATTRIBUTES ,SAFE_SYNC, NTLM, ALL_ATTRIBUTES
import os, sys
from typing import Tuple
#------Imprescindible para poder importar de otras carpetas (de basicutils)
from softprop.basicutils import xmcrypto, xmfile
from softprop import xmproject_folder
# ------Fin imprescindible


def get_props() -> dict:
	'''Get the properties from the encrypted file'''
	logs=[]
	logs.append('env='+str(os.environ))
	xmfile.add_lines_to_file('/home/eduard/kk.log', logs)
	conf_file=xmproject_folder.get_project_static_conf_params_encr_folder() + os.sep +'ldap.encrypted.yml'
	config= xmcrypto.get_properties_from_file(conf_file)
	return config['ldapWindows']

def autheticate_by_login(username: str, password: str)->bool:
	'''Autheticate by login name and password'''
	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
	
def get_conn()-> Tuple[Connection, dict, str]:
	'''Get the connection, config and base DN
	Returns:
		'''
	conn=None; config=None; base_d_n=None
	try: 
		config=get_props()
		base_d_n=config['dn'][config['dn'].find("DC="):]
		server = Server(config['serverName'], get_info=ALL)
		conn = Connection(server, user=config['dn'], password=config['password'], auto_bind=True)
		return conn, config, base_d_n
	except Exception as e:
		print(f"LDAP error2 : {e}", file=sys.stderr)
		if 'conn' in locals():
			conn.unbind()
		return conn, config, base_d_n
	
'''Get all login names from LDAP'''
def get_all_logins()->list:
	entries=None
	try:
		conn, config, base_d_n=get_conn()
		conn.search(base_d_n, '(&(objectclass=person) (sAMAccountName=*))',
			# COMMENT attributes=['sAMAccountName','cn','mail','ou','userAccountControl','accountStatus',
			# COMMENT 'nsAccountLock','loginDisabled', 'shadowExpire','pwdAccountLockedTime'])
			attributes=ALL_ATTRIBUTES)
		entries=conn.entries
		# Close the connection
		conn.unbind()
		#with open('/home/eduard/kk/ldap.txt', 'w') as f:
		with open(os.path.join('home','eduard','kk','ldap.txt', 'w')) as f:
			for e in entries:
				f.write(f"{str(e).replace("\n", ";")}\n")
		# COMMENT: return [str(e) for e in entries]
		return entries
		
	except Exception as e:
		print(f"LDAP error3 : {e}", file=sys.stderr)
		if 'conn' in locals():
			conn.unbind()
		return []

'''Get all DNs from LDAP'''
def get_all_d_ns()->list:
	try:
		conn, config, base_d_n=get_conn()
		conn.search(base_d_n, '(&(objectclass=user) (sAMAccountName=*))',attributes=[])
		#conn.search(baseDN, '(&(objectclass=person) (sAMAccountName=*))',attributes=['DN'])
		entries=conn.entries
		#pprint.pprint(entries)
		print(str(entries[0]))
		# Close the connection
		conn.unbind()
		return [str(e)[:str(e).find(" - STATUS:")].replace('DN: ','')for e in entries]
		
		
	except Exception as e:
		print(f"LDAP error4 : {e}", file=sys.stderr)
		if 'conn' in locals():
			conn.unbind()
		return []

'''Get all entries from LDAP'''
def get_all_entries()->list:
	try:
		conn, _, base_d_n=get_conn()
		conn.search(search_base=base_d_n, 
			search_filter="(objectClass=*)",  # Search for all entries,attributes=[])
			search_scope=SUBTREE, 
			attributes=None)  # Retrieve these attributes

			# Print the results
		for entry in conn.entries:
			print(entry)

		# Close the connection
		conn.unbind()

	except Exception as e:
		print(f"Error: {e}")
		# Close the connection
		conn.unbind()


def exists_user(username:str)->bool:
	try:
		conn, _, base_d_n=get_conn()
		# Build the search filter (for example, using uid or sAMAccountName)
		user_attribute="sAMAccountName"	
		search_filter = f'(&({user_attribute}={username})(objectClass=person))'
		
		conn.search(search_base=base_d_n, search_filter=search_filter, search_scope=SUBTREE, attributes=['sAMAccountName','cn','mail','ou'])
		
		if conn.entries:
			print('*****************************************************')
			print (str(conn.entries))
			return True
		else:
			return False
	except Exception as e:
		print("Search failed:", e)
		return False

def get_user_info(conn, base_d_n, username:str):
	if not conn:
		conn, _, base_d_n=get_conn()
	
	user_attribute="sAMAccountName"	
	search_filter = f'(&({user_attribute}={username})(objectClass=person))'
	# COMMENT:conn.search(search_base=base_d_n, search_filter=search_filter, search_scope=SUBTREE, attributes=['sAMAccountName','cn','mail','ou'])	
	conn.search(search_base=base_d_n, search_filter=search_filter, search_scope=SUBTREE, attributes=ALL_ATTRIBUTES)
	if conn.entries: return conn.entries[0]
	return None
			
if __name__ == "__main__":
	if len(sys.argv) != 3:
		print("Usage: authenticate.py <username> <password>")
		sys.exit(1)

	username = sys.argv[1]
	password = sys.argv[2]
	#print(f"username: {username}, password: {password}")

	#print(get_all_d_ns())
	print ("==========================================================")
	print()
	print (get_all_logins())
	print ("==========================================================")
	print()
	# COMMENT get_all_entries()
	# COMMENT print(autheticate_by_login(username, password))
	# COMMENT print(autheticateByLogin(username, password+'a'))
	# COMMENT print(autheticateByLogin(username+'a', password))
	

3. Programa para obtener el usuario del certificado y com`provar si está enb la base de datos.(xmusers_procs.py)

Veamos el código fuente, del que solo nos interesda la última función:

from datetime import timezone
from sqlalchemy import select, and_
import asyncio
import sys

from softprop.models.xmusermodels import UserStore, UserAbst, UserStoreHst #,xmdb
from softprop.basicutils import xmdbv4, xmldap, xmother
from softprop.procedures import xmalbaopc_procs


def get_user(alba_dict:dict | None= None, ldap_entry=None)->UserAbst:
	'''Set the user from Sediapualba or LDAP'''
		
	# Check if the user is active in LDAP
	is_active=False
	if ldap_entry is not None:
		if 'userAccountControl' in ldap_entry:
			uac = int(ldap_entry.userAccountControl.value)
			is_active = not (uac & 2)
		# OpenLDAP-style check
		elif 'accountStatus' in ldap_entry :
			is_active = xmother.get_attr(ldap_entry,'accountStatus','').lower() == 'active'
		elif 'nsAccountLock' in ldap_entry:
			is_active = xmother.get_attr(ldap_entry,'nsAccountLock','').lower() != 'true'
		elif 'loginDisabled' in ldap_entry:
			is_active = xmother.get_attr(ldap_entry,'loginDisabled','').lower() != 'true'
		elif 'shadowExpire' in ldap_entry:
			from datetime import datetime, timedelta
			# 'shadowExpire' is typically days since Jan 1, 1970
			expire_days = int(ldap_entry.shadowExpire.value)
			if expire_days == -1:
				is_active = True
			else:
				is_active = (datetime.now(timezone.utc) < datetime(1970,1,1) + timedelta(days=expire_days))
		elif 'pwdAccountLockedTime' in ldap_entry:
			is_active = False
		else:
			# Default fallback
			is_active = True  # or False, depending on your default stance

	myuser=UserAbst(description=xmother.get_attr_complex(alba_dict,'Descripcion',ldap_entry,'cn',''),
		id_alba=alba_dict.get('Id',-1),
		nif=alba_dict.get('Nif',''),
		nom=alba_dict.get('Nombre',''),
		cognom1 = alba_dict.get('Apellido1',''),
		cognom2 = alba_dict.get('Apellido2',''),
		puesto = xmother.get_attr_complex(alba_dict,'Puesto',ldap_entry,'sAMAccountName',''), 
		activo = alba_dict.get('Activo','False').lower()=='true',
		#--LDAP
		ldap_user=xmother.get_attr(ldap_entry,'sAMAccountName',''),
		ldap_cn=xmother.get_attr(ldap_entry,'cn', ''),
		ldap_mail=xmother.get_attr(ldap_entry,'mail',''),
		ldap_activo=is_active,
		ldap_user_control=xmother.get_attr(ldap_entry,'userAccountControl',''),
		#--TOTP

	)	
	return myuser

def is_dif_user(myuser:UserAbst, mynewuser:UserAbst)->bool:
	'''Check if the user is different from the new user'''
	return any([
        myuser.description != mynewuser.description,
        myuser.id_alba != mynewuser.id_alba,
        myuser.nif != mynewuser.nif,
        myuser.nom != mynewuser.nom,
        myuser.cognom1 != mynewuser.cognom1,
        myuser.cognom2 != mynewuser.cognom2,
        myuser.puesto != mynewuser.puesto,
        myuser.activo != mynewuser.activo,
    ])

async def save_alba_users():
    """Save Sediapualba's users to the database"""
    d_users = {}
    d_nodes = {}
    conn = None
    try:
        conn, _, base_d_n = xmldap.get_conn()
        async with xmdbv4.get_async_session(xmdbv4.my_main_db) as db_main:
            xmalbaopc_procs.alba_get_all_users_nifs(d_nodes=d_nodes, d_users=d_users)

            for key, value in d_users.items():
                operation = "update"
                id_user = value.get("Puesto", "")
                if key == "73912286S":
                    id_user = "eduard"
                ldap_entry = xmldap.get_user_info(conn, base_d_n, id_user)

                result = await db_main.execute(select(UserStore).filter_by(nif=key))
                myuser = result.scalars().first()
                if not myuser:
                    myuser = UserStore()
                    operation = "insert"

                mynewuser = get_user(value, ldap_entry)
                has_changes = is_dif_user(myuser, mynewuser)

                if operation == "insert" or (operation == "update" and has_changes):
                    auser = await xmdbv4.merge_to_main_and_histo_async_newtrans(xmdbv4.my_main_db, mynewuser, UserStore, UserStoreHst)
                    print(getattr(auser, "description", "No description!!!"))

            #await db_main.commit()
            
    except Exception as e:
        print(f"Error getting users from Sediapualba: {e}")
    finally:
        if conn:
            conn.unbind()
    

async def save_ldap_users():
    """Save LDAP's users to the database"""
    try:
        entries = xmldap.get_all_logins()
        async with xmdbv4.get_async_session(xmdbv4.my_main_db) as db_main:
            for ldap_entry in entries:
                dn = getattr(ldap_entry, "entry_dn", "")
                print(dn)
                if "USUARIS" in dn.upper() or "USERS" in dn.upper():
                    result = await db_main.execute(
                        select(UserStore).filter_by(ldap_user=ldap_entry.sAMAccountName.value)
                    )
                    myuser = result.scalars().first()
                    if not myuser:
                        mynewuser = get_user({}, ldap_entry)
                        auser = await xmdbv4.merge_to_main_and_histo_async_newtrans(xmdbv4.my_main_db, mynewuser, UserStore, UserStoreHst)
                        print(getattr(auser, "description", "No description!!!"))
            await db_main.commit()
    except Exception as e:
        print(f"Error getting users from LDAP: {e}")
			
async def save_alba_ldap_users():
    """Save Sediapualba's users and LDAP's users to the database"""
    await save_alba_users()
    await save_ldap_users()
    return "OK", None

async def get_user_by_certificate_dn(cert_dn:str)->UserAbst | None:
	'''Get the user in the database by the DNI in the certificate DN'''
	nif=xmother.get_substring_between(cert_dn,'serialNumber=',',').strip()
	if nif=='': nif=xmother.get_substring_between(cert_dn,'- DNI ',',').strip()	

	# Search in the database
	async with xmdbv4.get_async_session(xmdbv4.my_main_db) as db_main:
		result = await db_main.execute(
			select(UserStore).where(and_(UserStore.nif == nif, UserStore.ldap_activo == True))
		)
		return result.scalars().first()


if __name__ == "__main__":
	asyncio.run(save_alba_ldap_users())
		

4. Programa de utilidades varias.(xmother.py)

Veamos el código fuente, del que solo nos interesda  función:

import base64
import datetime
import os
import random
import secrets
import string
from typing import Any
from pathlib import Path

#---General Parameters
cookie_max_age=3600 # seconds

def new_id(size=10):
    ''' Crea un id de 10 letras aleatorias en mayúscula'''
    return ''.join(random.choices(string.ascii_uppercase + string.digits, k=size))


def return_as_list(my_dict:dict)->list:
	if my_dict is None or len(my_dict)==0: return []
	if not isinstance(my_dict, dict): return [my_dict]
	first_key = next(iter(my_dict))
	first_value = my_dict[first_key]
	if first_value is None: return []
	if isinstance(first_value, list): return first_value
	if isinstance(first_value, dict): return [first_value]
      
def new_return(output_dict:dict| Any, params:list|str, as_list:bool=True):
	my_obj=None
	my_error=None
	if not isinstance(output_dict, dict):
		if as_list: output_dict=[output_dict]	
		return output_dict, my_error
	else:
		if output_dict.get('Error',None)!=None:
			my_error=output_dict['Error']
			if as_list: my_obj=[my_obj]	
			return my_obj,my_error
	try:
		if isinstance(params,str):	my_obj=output_dict[params]
		else: my_obj=[output_dict[param] for param in params]	
	except Exception as e:
		my_error=str(e)
	if as_list: my_obj=[my_obj]	
	return my_obj, my_error	  

def json_str2json_bytearray(mystr:str)->str:
	# Hem de lleva les 2 primeres posicions (b') i l'última (')
	my_bytes=mystr[2:-1].encode() #Truco, sinó no va
	return my_bytes

def arrange_base64(mystr:str)->str:
	'''Replaces any of the chars of "/+=" with "X" of a baswe64 string '''
	toreplace="/+="
	for mychar in toreplace: mystr=mystr.replace(mychar,"X")
	return mystr

def get_old_random_string(mylength: int=32)->str:
	'''get a random string and removes "/+=" '''
	my_random_string = base64.b64encode(os.urandom(mylength)).decode('utf-8')  #
	return arrange_base64(my_random_string)

def get_random_string(mylength: int=32)->str:
	'''get a random string and removes "/+=" '''
	chars = string.ascii_letters + string.digits
	return ''.join(secrets.choice(chars) for _ in range(mylength))

def get_last_folder(mypath:str)->str:
	'''Returns the last folder of a path'''
	path_obj = Path(mypath).resolve()  # Convert to absolute path

	if path_obj.is_dir():
		return path_obj.name  # Return the folder itself if it's a directory

	return path_obj.parent.name  # Otherwise, return the parent folder of the file

def get_tipo_doc_eni(doc_name:str)->str:
	#TD99: Otros (expedient eni)
	if ('es_l' in doc_name.lower() and 'exp_index' in doc_name.lower()): return 'TD02'
	#TD02: Acuerdo de pleno
	elif 'acta' in doc_name.lower() and 'sessi' in doc_name.lower(): return 'TD02'
	elif ('es_l' in doc_name.lower() and 'actes_ple' in doc_name.lower()): return 'TD02'
	#TD12: Diligencia
	elif 'dil' in doc_name.lower() and 'actes' in doc_name.lower(): return 'TD12'
	#TD01: Decreto
	elif 'decr' in doc_name.lower() or 'resol' in doc_name.lower(): return 'TD01'
	else: return 'TD99'
	

def get_estado_elaboracion_eni(doc_name:str)->str:
	if doc_name.endswith('.pdf'): return 'EE01'
	elif doc_name.endswith('.xml'): 
		if "_DOC_" in doc_name: return 'EE02'
		elif "_EXP_" in doc_name: return 'EE01'
	return 'EE04'

def get_attr(obj, attr:str, default_value=None):
	'''Get the attribute of an object'''
	if obj is not None and len(attr)>0: 
		if isinstance(obj, dict): 
			if attr in obj: return obj[attr]
		elif hasattr(obj, attr): return getattr(obj, attr).value
	return default_value	
		
def get_attr_complex(obj, attr:str, obj1=None, attr1:str='', default_value=None)->str:
	'''Get nested alternative attributes form objects'''
	myvalue= get_attr(obj, attr)
	if not myvalue: myvalue= get_attr(obj1, attr1, default_value)
	return myvalue

def get_substring_between(mystring: str, start: str, end: str) -> str:
	"""Get the substring between two strings."""
	try:
		print('string='+str(mystring))
		print('start='+start)
		print('end='+end)
		start_index = mystring.index(start) + len(start)
		end_index = mystring.index(end, start_index)
		if end != -1: return mystring[start_index:end_index]
		else: return mystring[start:]  # no end found
	except ValueError:
		return ""
	
def get_totp_html(fh, has_totp:bool, image_path:str, is_error:bool=False)->str:
	'''Get the TOTP HTML code'''
	myimage=''
	myimage_label=''	
	myerror=''
	# 3. Si no tiene TOTP mostramos el código QR para que el Authenticator
	if not has_totp:
		myimage=fh.Img(src=image_path, alt="TOTP QR Code", cls="img-fluid")
		myimage_label=fh.Label("Escanea el QR code con Authenticator", cls="form-label")
	if is_error:
		myerror=fh.Div("Error en el 2º Factor de autenticación", cls="alert alert-danger")
	return fh.Div(
		myerror,
		myimage_label,
		myimage,
		fh.Label("Introduzca el código TOTP del Authenticator", cls="form-label"),
		fh.Input(type="text", name="totp_digits", id="totp_digits", cls="form-control"),
		fh.Label(""),
	)

def get_yyyymmddhhmmss()->str:
	'''Get the current date and time in the format YYYYMMDDHHMMSS'''
	now_= datetime.datetime.now()
	return now_.strftime("%Y%m%d%H%M%S")+str(now_.microsecond)[:6] # Get the first 6 digits of the microseconds
"""
def get_project_path():
  '''
  Gets the path of the current Python project.

  Returns:
    str: The absolute path to the project directory.
  '''
  #NO VA: path1=os.path.dirname(os.path.abspath(__file__))
  #NO VA: path2=sys.path[0]
  #NO VA: path3=Path(__file__).parent.parent.absolute()
  path4=os.getcwdb().decode('utf-8')
  return path4

"""

if __name__ == "__main__":
	a=get_substring_between('C=ES,O=EDIFICIO_LOCALIDAD,OU=CERTIFICADO ELECTRONICO DE PRUEBA,SN=MARTINEZ MARTINEZ,GN=MARTINETE,serialNumber=12345678A,CN=MARTINETE MARTINEZ MARTINEZ - DNI 12345678A', 'serialNumber=', ',')
	print (a)


5. Programa python en fastapi para que ejecute un servidor uvicorn que reciba los parámetros y autentique (xmopenresty.py)

Cabe destacar :

  • La línea del shebang para que se ejecute desde el entorno virtual correcto
  • Si no le indcamos el PROJECT_ROOT, n podemos ejecutar con shebang!!
  • La línea que llama a FastAPI (app = FastAPI())
  • Las clases AuthRequest y AuthResponse que encapsulan la entrada y salida de datos
  • La ruta POST /auth que es la que se encarga de contestar las peticiones de autenticación
  • El arranque del servidor uvicorn en l IP 127.0.0.1 y el puerto 5000

Veamos el código de xmopenresty.py:

#!/home/informatica/eduApps/softprop/venv_softprop/bin/python3

import sys
import os

# --- Ensure project root is in sys.path when run directly ---
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
if PROJECT_ROOT not in sys.path:
    sys.path.insert(0, PROJECT_ROOT)

print("PROJECT_ROOT =", PROJECT_ROOT)
from softprop.basicutils import xmldap
from softprop.procedures import xmusers_procs
from softprop import xmproject_folder  # <-- Add this import

# ------Fin imprescindible


app = FastAPI()

class AuthRequest(BaseModel):
    username: str
    password: str
    # cert :str
    cert_verify : str
    cert_dn : str  

class AuthResponse(BaseModel):
    authenticated: bool



def authenticate_db(cert_dn:str, username: str) -> bool:
	''' Authenticate the user using the certificate DN and username.
		This function checks if the user exists in the database with the DNI of the certificate DN.
		And tests if the user is the same as the one in the DB with the DNI
		Args:
			cert_dn (str): The DN of the certificate.
			username (str): The username to authenticate.
		Returns:
			bool: True if the user is authenticated, False otherwise.
	'''
	myuser=xmusers_procs.get_user_by_certificate_dn(cert_dn)
	if myuser and myuser.ldap_user == username:
		print("authenticate_db: User authenticated successfully.")
		return True
	
	print("authenticate_db: NOT User authenticated.")
	return False
				


@app.post("/auth", response_model=AuthResponse)
async def auth(request: AuthRequest):
	''' This function is only used from openresty (nginx)
		Verify that the cert is OK and that the user and password exists in LDAP
		Args:
			request (AuthRequest): The authentication request containing username, password, and certificate information.
		Returns:
			AuthResponse: The authentication response indicating whether the user is authenticated or not.
	'''
	print(request)
	client_verify = 'SUCCESS' in request.cert_verify.upper() 
	is_authenticated_ldap= xmldap.autheticate_by_login(request.username, request.password)  
	is_authenticated_db= authenticate_db(request.cert_dn, request.username)
	is_valid_usr_pwd = client_verify and is_authenticated_ldap and is_authenticated_db
	        
	return AuthResponse(authenticated=is_valid_usr_pwd)


if __name__ == "__main__":
	cert_path=os.path.join(xmproject_folder.get_project_static_folder(), 'certs','wildcard.tavernes.es.')
    	uvicorn.run(app, host="proves.tavernes.es", port=5000,	
		ssl_keyfile =cert_path+"key", 
		ssl_certfile=cert_path+"crt")

Para ejecutarlo, hay que ir a un ventana de comandos y introducir

cd /home/informatica/eduApps
./softprop/authentication/ xmopenresty.py
# ------También se puede ejecutar sin el ./
#  softprop/authentication/ xmopenresty.py 

Pues como está el shebang de la primera línea, ya sabe que entorno vurtual de python utilizará para su ejecución


WEBPROPv2 (VII). Instalación en un servidor con nginx (II). Instalar python, dependencias y programas.

 1. Instalar python y entorno virtual

Ejecutamos

sudo apt update
sudo apt install python3 python3-venv python3-pip -y

Creamos las carpetas y cremos el entorno virtual:

cd ~
# -- 1. Creamos carpetas
mkdir eduApps
cd eduApps
mkdir softprop

# -- 2. Cambianos permisos
sudo chown $USER:$USER softprop
cd softprop

# -- 3. Cremos el entorno virtual en softprop
python3 -m venv venv_softprop

2. Instalar dependencias

En el ordenador de desarrollo vamos a ver que dependencias tenemos:

pip freeze > requirements.txt

Copiamos este fichero a la carpeta softprop del servidor (por ssh o filezilla)

Ejecutamos

# -- 1. Activamos el entorno virtual
source venv_softprop/bin/activate

# -- 2. Instalamos las dependencias
pip install -r requirements.txt

3. Copiar programas python

Mediante el filezilla dándole la IP del servidor ay el protovolo "SFTP - SSH File Transfer Protocol", copiamos todos los programas, siendo ahora la carpeta del proyecto "softprop"



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 elpuerto 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 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.conf.softprop

           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