Modulo de comunicacion

En 1997 me recibí de programador (Época de Pascal / Cobol 68 en la escuela, en la vida real otros lenguajes mas de la época) me dedique a programar moy poco tiempo hasta allá por el 2001, luego de eso tuve una actividad mas comercial venta de hardware mantenimiento de empresas seguridad informática casi todo por scripting nada muy desarrollado. Siempre me debí a mi mismo realizar un sistema de gestión que realmente cumpla todas mis necesidades y la de la gente.

Bueno hoy en día despues de retomar con un breve curso de Python a mano y empecé su desarrollo.

¿Por qué decido arrancar por el modulo de comunicación?
Sencillo casi todos los enlatados que vi de ejecución local en cada PC cometen los siguientes errores

  1. No lo podes usar fuera de la LAN, si por una VPN pero no a una velocidad que valga la pena utilizar
  2. Tienen la DB expuesta en la LAN (por lo general dejan el servicio de SQL escuchando las consultas en toda la LAN
  3. Siempre dejan su archivo de configuración para acceso a la DB dentro de in INI en el directorio de trabajo

Bueno eso son los principales retos a enfrentar en una Aplicacion de Escritorio a la hora de trabajar tanto localmente como remotamente

Voy a empezar por explicar los temas que quiero que abarque el modulo de conexion

  1. establecer un tunnel-SSH a el server donde este la DB
  2. Comunicar a el motor de la base de datos para correr las consultas como localhost dentro del servidor y no tener la base de datos abierta al mundo
  3. dicho servicio SSH solo corre en la lan de la empresa, asi que el que este por fuera deberá contar con acceso de VPN para poder levantar los 2 puntos anteriores, con esto creo que dejo bastante rebuscado la comunicación con la DB
  4. Implementación con criptografía con una clave privada dentro del codigo fuente para que genere de entrada los INI correspondientes ya encriptados
  5. Dentro del modulo tener un indicador si la conexion a el motor de base de datos esta operacional y el ssh activo, poder reiniciarlo configurarlo y demas desde dicho modulo, que seria solo una parte del sistema real.

Bueno a continuación voy a pasar los códigos que voy desarrollando a ver a quien le interesa participar

1 me gusta

Aca les dejo el modulo Alpha 0.1 que esta muy lejos de ser lo que quiero pero cumple las funciones basicas de establecer las conexiones

import tkinter as tk
from tkinter import ttk
import sshtunnel
import mysql.connector
import configparser


class ConfiguracionConexion:
    def __init__(self, root):
        self.root = root
        self.root.title("Configuración de Conexión")
        self.root.geometry("600x400")

        self.estado_conexion = tk.StringVar()

        self.frame_estado = ttk.LabelFrame(self.root, text="Estado de la Conexión")
        self.frame_estado.pack(padx=20, pady=20, fill=tk.BOTH, expand=True)

        self.frame_ssh = ttk.Frame(self.frame_estado)
        self.frame_ssh.grid(row=0, column=0, padx=20, pady=10, sticky="nsew")

        self.frame_mysql = ttk.Frame(self.frame_estado)
        self.frame_mysql.grid(row=0, column=1, padx=20, pady=10, sticky="nsew")

        self.iniciar_ssh_btn = ttk.Button(self.frame_ssh, text="Iniciar SSH", command=self.iniciar_ssh)
        self.parar_ssh_btn = ttk.Button(self.frame_ssh, text="Parar SSH", command=self.parar_ssh)
        self.configurar_ssh_btn = ttk.Button(self.frame_ssh, text="Configurar SSH",
                                             command=self.abrir_ventana_configuracion_ssh)

        self.iniciar_mysql_btn = ttk.Button(self.frame_mysql, text="Iniciar MySQL", command=self.iniciar_mysql)
        self.parar_mysql_btn = ttk.Button(self.frame_mysql, text="Parar MySQL", command=self.parar_mysql)
        self.configurar_mysql_btn = ttk.Button(self.frame_mysql, text="Configurar MySQL",
                                               command=self.abrir_ventana_configuracion_mysql)

        self.iniciar_ssh_btn.grid(row=0, column=0, padx=10, pady=10)
        self.parar_ssh_btn.grid(row=1, column=0, padx=10, pady=10)
        self.configurar_ssh_btn.grid(row=2, column=0, padx=10, pady=10)

        self.iniciar_mysql_btn.grid(row=0, column=0, padx=10, pady=10)
        self.parar_mysql_btn.grid(row=1, column=0, padx=10, pady=10)
        self.configurar_mysql_btn.grid(row=2, column=0, padx=10, pady=10)

        self.estado_lbl = ttk.Label(self.frame_estado, textvariable=self.estado_conexion)
        self.estado_lbl.grid(row=3, column=0, columnspan=2, padx=10, pady=10)

        # Inicialmente, el estado está en "Parado"
        self.estado_conexion.set("Parado")

        self.ssh_tunnel = None
        self.mysql_connection = None

        # Variables para almacenar configuración SSH y MySQL
        self.config_ssh = {
            'Host': tk.StringVar(),
            'Puerto': tk.StringVar(),
            'Usuario': tk.StringVar(),
            'Password': tk.StringVar()
        }

        self.config_mysql = {
            'Host': tk.StringVar(),
            'Puerto': tk.StringVar(),
            'Usuario': tk.StringVar(),
            'Password': tk.StringVar(),
            'BaseDatos': tk.StringVar()
        }

        # Cargar configuración inicial desde los archivos INI
        self.cargar_configuracion('tunel-ssh.ini', self.config_ssh)
        self.cargar_configuracion('db.ini', self.config_mysql)

    def abrir_ventana_configuracion_ssh(self):
        self.abrir_ventana_configuracion('tunel-ssh.ini', self.config_ssh, "Configuración SSH")

    def abrir_ventana_configuracion_mysql(self):
        self.abrir_ventana_configuracion('db.ini', self.config_mysql, "Configuración MySQL")

    def abrir_ventana_configuracion(self, archivo_ini, configuracion, titulo):
        ventana_configuracion = tk.Toplevel(self.root)
        ventana_configuracion.title(titulo)
        ventana_configuracion.geometry("400x300")

        ttk.Label(ventana_configuracion, text="Configuración").pack(pady=10)
        self.crear_campos(ventana_configuracion, configuracion)

        guardar_btn = ttk.Button(ventana_configuracion, text="Guardar Configuración",
                                 command=lambda: self.guardar_configuracion(archivo_ini))
        guardar_btn.pack(pady=10)

    def crear_campos(self, ventana, configuracion):
        ttk.Label(ventana, text="Host:").pack(pady=5)
        host_entry = ttk.Entry(ventana, textvariable=configuracion['Host'])
        host_entry.pack(pady=5)

        ttk.Label(ventana, text="Puerto:").pack(pady=5)
        puerto_entry = ttk.Entry(ventana, textvariable=configuracion['Puerto'])
        puerto_entry.pack(pady=5)

        ttk.Label(ventana, text="Usuario:").pack(pady=5)
        usuario_entry = ttk.Entry(ventana, textvariable=configuracion['Usuario'])
        usuario_entry.pack(pady=5)

        ttk.Label(ventana, text="Password:").pack(pady=5)
        password_entry = ttk.Entry(ventana, show="*", textvariable=configuracion['Password'])
        password_entry.pack(pady=5)

        if 'BaseDatos' in configuracion:
            ttk.Label(ventana, text="Base de Datos:").pack(pady=5)
            base_datos_combo = ttk.Combobox(ventana, textvariable=configuracion['BaseDatos'])
            base_datos_combo.pack(pady=5)

            # Agregar aquí la lógica para cargar las bases de datos disponibles en el servidor MySQL

    def iniciar_ssh(self):
        try:
            self.ssh_tunnel = sshtunnel.SSHTunnelForwarder(
                (self.config_ssh['Host'].get(), int(self.config_ssh['Puerto'].get())),
                ssh_username=self.config_ssh['Usuario'].get(),
                ssh_password=self.config_ssh['Password'].get(),
                remote_bind_address=(self.config_mysql['Host'].get(), int(self.config_mysql['Puerto'].get()))
            )
            self.ssh_tunnel.start()
            self.estado_conexion.set("Ok")
        except Exception as e:
            self.estado_conexion.set("Error")
            print(f"Error al iniciar la conexión SSH: {e}")

    def parar_ssh(self):
        try:
            if self.ssh_tunnel:
                self.ssh_tunnel.stop()
                self.estado_conexion.set("Parado")
        except Exception as e:
            print(f"Error al detener la conexión SSH: {e}")

    def iniciar_mysql(self):
        try:
            self.mysql_connection = mysql.connector.connect(
                user=self.config_mysql['Usuario'].get(),
                password=self.config_mysql['Password'].get(),
                host=self.config_mysql['Host'].get(),
                port=int(self.config_mysql['Puerto'].get()),
                database=self.config_mysql['BaseDatos'].get()
            )
            if self.mysql_connection.is_connected():
                self.estado_conexion.set("Ok")
            else:
                self.estado_conexion.set("Error")
        except Exception as e:
            self.estado_conexion.set("Error")
            print(f"Error al iniciar la conexión MySQL: {e}")

    def parar_mysql(self):
        try:
            if self.mysql_connection:
                self.mysql_connection.close()
                self.estado_conexion.set("Parado")
        except Exception as e:
            print(f"Error al detener la conexión MySQL: {e}")

    def cargar_configuracion(self, archivo_ini, configuracion):
        config = configparser.ConfigParser()
        config.read(archivo_ini)

        for key in configuracion:
            if key in config['DEFAULT']:
                configuracion[key].set(config['DEFAULT'][key])

    def guardar_configuracion(self, archivo_ini):
        config = configparser.ConfigParser()

        config['DEFAULT'] = {
            'Host': self.config_ssh['Host'].get(),
            'Puerto': self.config_ssh['Puerto'].get(),
            'Usuario': self.config_ssh['Usuario'].get(),
            'Password': self.config_ssh['Password'].get()
        }

        config['MySQL'] = {
            'Host': self.config_mysql['Host'].get(),
            'Puerto': self.config_mysql['Puerto'].get(),
            'Usuario': self.config_mysql['Usuario'].get(),
            'Password': self.config_mysql['Password'].get(),
            'BaseDatos': self.config_mysql['BaseDatos'].get()
        }

        with open(archivo_ini, 'w') as configfile:
            config.write(configfile)

        print(f"Configuraciones guardadas en {archivo_ini}")


if __name__ == "__main__":
    root = tk.Tk()
    app = ConfiguracionConexion(root)
    root.mainloop()

Como para que vallan echando una mirada

Partiendo de este codigo debería agregar

  1. En los módulos configuración SSH y configuración MYSQL antes de colocar el tilde al comprobar los datos ingresados y dar un OK verificar si la información ingresada corresponde al tipo de variable declarada, si no colocar un tilde de cruz y deshabilitar el botón guardar hasta tanto no se corrija dicha información. Ninguna cuadro de dialogo puede quedar sin completar.

Al pulsar guardar configuración si todas las condiciones se cumplen guardar y luego cerrar el cuadro de dialogo

  1. En los modulos Configuracion SSH y Consiguracion MYSQL, solicitar 2 datos por Linea uno al lado del otro.
  1. Tanto como en el modulo configurar SSH y Configurar Mysql la ventana debe autorezaicearse para que todo lo que debe mostrar se vea correctamente, y las variables deben verse 2 por linea 1 al lado del otro (Ejemplo Host … Puerto …) cada vez que un cuadro de dialogo es completado y salta al siguente mostrar un tilde de grabado a su lado
  1. Las variable de Host puede ser alfanumérica, la variable puerto es solo numérica la variable de passwords es alfanumérica , la variable para Base de datos es alfanumérica
  1. Las opciones de Iniciar apagar y configurar mysql deben ser visibles, pero solo si el estado de la conexion SSH esta en OK

En lo visual remplazar el OK visual en pantalla de la conexion por un circulo verde, si no un circulo rojo que indique que esta apagado

  1. El cuadro de configurar conexion tambien se debe autoajustar, las opciones Iniciar parar y Configurar de Mysql no debe permitir operar si el servicio SSH no esta activo, reemplazar la confirmación OK por un Led Verde de funcional, Rojo para Parado
  1. Los INI deben estar encriptado por separado

Les dejo el ultimo codigo en el que estoy trabajando que es la parte de encriptacion para poder empezar a tachar algo de la lista, vengo trabajando en esto desde las 13hs siendo las 1830, creo que entrare en pausa, estoy llegando a un error que no estaria encontrando

import tkinter as tk
from tkinter import ttk
import sshtunnel
import mysql.connector
import configparser
from cryptography.fernet import Fernet
import os

class ConfiguracionConexion:
    def __init__(self, root):
        self.root = root
        self.root.title("Configuración de Conexión")
        self.root.geometry("800x600")

        self.estado_conexion = tk.StringVar()

        self.frame_estado = ttk.LabelFrame(self.root, text="Estado de la Conexión")
        self.frame_estado.pack(padx=20, pady=20, fill=tk.BOTH, expand=True)

        self.frame_ssh = ttk.Frame(self.frame_estado)
        self.frame_ssh.grid(row=0, column=0, padx=20, pady=10, sticky="nsew")

        self.frame_mysql = ttk.Frame(self.frame_estado)
        self.frame_mysql.grid(row=0, column=1, padx=20, pady=10, sticky="nsew")

        self.iniciar_ssh_btn = ttk.Button(self.frame_ssh, text="Iniciar SSH", command=self.iniciar_ssh)
        self.parar_ssh_btn = ttk.Button(self.frame_ssh, text="Parar SSH", command=self.parar_ssh)
        self.configurar_ssh_btn = ttk.Button(self.frame_ssh, text="Configurar SSH",
                                             command=self.abrir_ventana_configuracion_ssh)

        self.iniciar_mysql_btn = ttk.Button(self.frame_mysql, text="Iniciar MySQL", command=self.iniciar_mysql)
        self.parar_mysql_btn = ttk.Button(self.frame_mysql, text="Parar MySQL", command=self.parar_mysql)
        self.configurar_mysql_btn = ttk.Button(self.frame_mysql, text="Configurar MySQL",
                                               command=self.abrir_ventana_configuracion_mysql)

        self.iniciar_ssh_btn.grid(row=0, column=0, padx=10, pady=10)
        self.parar_ssh_btn.grid(row=1, column=0, padx=10, pady=10)
        self.configurar_ssh_btn.grid(row=2, column=0, padx=10, pady=10)

        self.iniciar_mysql_btn.grid(row=0, column=0, padx=10, pady=10)
        self.parar_mysql_btn.grid(row=1, column=0, padx=10, pady=10)
        self.configurar_mysql_btn.grid(row=2, column=0, padx=10, pady=10)

        self.estado_lbl = ttk.Label(self.frame_estado, textvariable=self.estado_conexion)
        self.estado_lbl.grid(row=3, column=0, columnspan=2, padx=10, pady=10)

        # Inicialmente, el estado está en "Parado"
        self.estado_conexion.set("Parado")

        self.ssh_tunnel = None
        self.mysql_connection = None

        # Variables para almacenar configuración SSH y MySQL
        self.config_ssh = {
            'Host': tk.StringVar(),
            'Puerto': tk.StringVar(),
            'Usuario': tk.StringVar(),
            'Password': tk.StringVar()
        }

        self.config_mysql = {
            'Host': tk.StringVar(),
            'Puerto': tk.StringVar(),
            'Usuario': tk.StringVar(),
            'Password': tk.StringVar(),
            'BaseDatos': tk.StringVar()
        }

        # Cargar configuración inicial desde los archivos INI
        self.cargar_configuracion('tunel-ssh.ini', self.config_ssh)
        self.cargar_configuracion('db.ini', self.config_mysql)

        clave_privada = b'prw0ZDOjOpJqmv2gv4oc7O9YSqcBvzKXb3ZSCTvvbss='

        # Cargar configuración cifrada SSH y MySQL
        self.cargar_configuracion_cifrada('tunel-ssh.ini', self.config_ssh, clave_privada)
        self.cargar_configuracion_cifrada('db.ini', self.config_mysql, clave_privada)

    def abrir_ventana_configuracion_ssh(self):
        self.abrir_ventana_configuracion('tunel-ssh.ini', self.config_ssh, "Configuración SSH")

    def abrir_ventana_configuracion_mysql(self):
        self.abrir_ventana_configuracion('db.ini', self.config_mysql, "Configuración MySQL")

    def abrir_ventana_configuracion(self, archivo_ini, configuracion, titulo):
        ventana_configuracion = tk.Toplevel(self.root)
        ventana_configuracion.title(titulo)
        ventana_configuracion.geometry("400x300")

        ttk.Label(ventana_configuracion, text="Configuración").pack(pady=10)
        self.crear_campos(ventana_configuracion, configuracion)

        guardar_btn = ttk.Button(ventana_configuracion, text="Guardar Configuración",
                                 command=lambda: self.guardar_configuracion(archivo_ini))
        guardar_btn.pack(pady=10)

    def crear_campos(self, ventana, configuracion):
        ttk.Label(ventana, text="Host:").pack(pady=5)
        host_entry = ttk.Entry(ventana, textvariable=configuracion['Host'])
        host_entry.pack(pady=5)

        ttk.Label(ventana, text="Puerto:").pack(pady=5)
        puerto_entry = ttk.Entry(ventana, textvariable=configuracion['Puerto'])
        puerto_entry.pack(pady=5)

        ttk.Label(ventana, text="Usuario:").pack(pady=5)
        usuario_entry = ttk.Entry(ventana, textvariable=configuracion['Usuario'])
        usuario_entry.pack(pady=5)

        ttk.Label(ventana, text="Password:").pack(pady=5)
        password_entry = ttk.Entry(ventana, show="*", textvariable=configuracion['Password'])
        password_entry.pack(pady=5)

        if 'BaseDatos' in configuracion:
            ttk.Label(ventana, text="Base de Datos:").pack(pady=5)
            base_datos_combo = ttk.Combobox(ventana, textvariable=configuracion['BaseDatos'])
            base_datos_combo.pack(pady=5)

            # Agregar aquí la lógica para cargar las bases de datos disponibles en el servidor MySQL

    def iniciar_ssh(self):
        try:
            self.ssh_tunnel = sshtunnel.SSHTunnelForwarder(
                (self.config_ssh['Host'].get(), int(self.config_ssh['Puerto'].get())),
                ssh_username=self.config_ssh['Usuario'].get(),
                ssh_password=self.config_ssh['Password'].get(),
                remote_bind_address=(self.config_mysql['Host'].get(), int(self.config_mysql['Puerto'].get()))
            )
            self.ssh_tunnel.start()
            self.estado_conexion.set("Ok")
        except Exception as e:
            self.estado_conexion.set("Error")
            print(f"Error al iniciar la conexión SSH: {e}")

    def parar_ssh(self):
        try:
            if self.ssh_tunnel:
                self.ssh_tunnel.stop()
                self.estado_conexion.set("Parado")
        except Exception as e:
            print(f"Error al detener la conexión SSH: {e}")

    def iniciar_mysql(self):
        try:
            self.mysql_connection = mysql.connector.connect(
                user=self.config_mysql['Usuario'].get(),
                password=self.config_mysql['Password'].get(),
                host=self.config_mysql['Host'].get(),
                port=int(self.config_mysql['Puerto'].get()),
                database=self.config_mysql['BaseDatos'].get()
            )
            if self.mysql_connection.is_connected():
                self.estado_conexion.set("Ok")
            else:
                self.estado_conexion.set("Error")
        except Exception as e:
            self.estado_conexion.set("Error")
            print(f"Error al iniciar la conexión MySQL: {e}")

    def parar_mysql(self):
        try:
            if self.mysql_connection:
                self.mysql_connection.close()
                self.estado_conexion.set("Parado")
        except Exception as e:
            print(f"Error al detener la conexión MySQL: {e}")

    def cargar_configuracion(self, archivo_ini, configuracion):
        config = configparser.ConfigParser()
        config.read(archivo_ini)

        for key in configuracion:
            if key in config['DEFAULT']:
                configuracion[key].set(config['DEFAULT'][key])

    def cargar_configuracion_cifrada(self, archivo_ini, configuracion, clave_privada):
        try:
            with open(archivo_ini, 'rb') as archivo:
                contenido_cifrado = archivo.read()

            fernet = Fernet(clave_privada)
            contenido_descifrado = fernet.decrypt(contenido_cifrado).decode('utf-8')
            config = configparser.ConfigParser()
            config.read_string(contenido_descifrado)

            for key in configuracion:
                if key in config['DEFAULT']:
                    configuracion[key].set(config['DEFAULT'][key])
        except FileNotFoundError:
            # Si el archivo no existe, inicialízalo cifrado
            self.guardar_configuracion_cifrada(archivo_ini, configuracion, clave_privada)

    def guardar_configuracion(self, archivo_ini):
        config = configparser.ConfigParser()

        config['DEFAULT'] = {
            'Host': self.config_ssh['Host'].get(),
            'Puerto': self.config_ssh['Puerto'].get(),
            'Usuario': self.config_ssh['Usuario'].get(),
            'Password': self.config_ssh['Password'].get()
        }

        config['MySQL'] = {
            'Host': self.config_mysql['Host'].get(),
            'Puerto': self.config_mysql['Puerto'].get(),
            'Usuario': self.config_mysql['Usuario'].get(),
            'Password': self.config_mysql['Password'].get(),
            'BaseDatos': self.config_mysql['BaseDatos'].get()
        }

        with open(archivo_ini, 'w') as configfile:
            config.write(configfile)

        print(f"Configuraciones guardadas en {archivo_ini}")

    def guardar_configuracion_cifrada(self, archivo_ini, configuracion, clave_privada):
        pass


if __name__ == "__main__":
    root = tk.Tk()
    app = ConfiguracionConexion(root)
    root.mainloop()

Les dejo la ultima revision de mi codigo de comunicaciones, el cual ya avance bastante

"""Modulo de comunicacion Version Alpha 0.2
Requerimientos Complidos
En los módulos configuración SSH y configuración MYSQL antes de colocar el tilde al comprobar los datos ingresados y dar un OK verificar si la información ingresada corresponde al tipo de variable declarada, si no colocar un tilde de cruz y deshabilitar el botón guardar hasta tanto no se corrija dicha información. Ninguna cuadro de dialogo puede quedar sin completar.
Al pulsar guardar configuración si todas las condiciones se cumplen guardar y luego cerrar el cuadro de dialogo
Los INI deben estar encriptado por separado


Requerimientos por Cumplir
En los modulos Configuracion SSH y Consiguracion MYSQL, solicitar 2 datos por Linea uno al lado del otro.

Tanto como en el modulo configurar SSH y Configurar Mysql la ventana debe autorezaicearse para que todo lo que debe mostrar se vea correctamente, y las variables deben verse 2 por linea 1 al lado del otro (Ejemplo Host … Puerto …) cada vez que un cuadro de dialogo es completado y salta al siguente mostrar un tilde de grabado a su lado

Las variable de Host puede ser alfanumérica, la variable puerto es solo numérica la variable de passwords es alfanumérica , la variable para Base de datos es alfanumérica

Las opciones de Iniciar apagar y configurar mysql deben ser visibles, pero solo si el estado de la conexion SSH esta en OK

En lo visual remplazar el OK visual en pantalla de la conexion por un circulo verde, si no un circulo rojo que indique que esta apagado

El cuadro de configurar conexion tambien se debe autoajustar, las opciones Iniciar parar y Configurar de Mysql no debe permitir operar si el servicio SSH no esta activo, reemplazar la confirmación OK por un Led Verde de funcional, Rojo para Parado

"""

# Importación de módulos necesarios
import tkinter as tk
from tkinter import ttk
import sshtunnel
import mysql.connector
import configparser
from cryptography.fernet import Fernet
import os
import re

# Definición de la clase principal de la aplicación
class ConfiguracionConexion:
    def __init__(self, root):
        # Inicialización de la ventana principal
        self.root = root
        self.root.title("Configuración de Conexión")
        self.root.geometry("800x600")

        # Variable para mostrar el estado de la conexión
        self.estado_conexion = tk.StringVar()

        # Creación de un marco para el estado de la conexión
        self.frame_estado = ttk.LabelFrame(self.root, text="Estado de la Conexión")
        self.frame_estado.pack(padx=20, pady=20, fill=tk.BOTH, expand=True)

        # Creación de marcos para SSH y MySQL dentro del marco de estado
        self.frame_ssh = ttk.Frame(self.frame_estado)
        self.frame_ssh.grid(row=0, column=0, padx=20, pady=10, sticky="nsew")

        self.frame_mysql = ttk.Frame(self.frame_estado)
        self.frame_mysql.grid(row=0, column=1, padx=20, pady=10, sticky="nsew")

        # Creación de botones para SSH
        self.iniciar_ssh_btn = ttk.Button(self.frame_ssh, text="Iniciar SSH", command=self.iniciar_ssh)
        self.parar_ssh_btn = ttk.Button(self.frame_ssh, text="Parar SSH", command=self.parar_ssh)
        self.configurar_ssh_btn = ttk.Button(self.frame_ssh, text="Configurar SSH",
                                             command=self.abrir_ventana_configuracion_ssh)

        # Creación de botones para MySQL
        self.iniciar_mysql_btn = ttk.Button(self.frame_mysql, text="Iniciar MySQL", command=self.iniciar_mysql)
        self.parar_mysql_btn = ttk.Button(self.frame_mysql, text="Parar MySQL", command=self.parar_mysql)
        self.configurar_mysql_btn = ttk.Button(self.frame_mysql, text="Configurar MySQL",
                                               command=self.abrir_ventana_configuracion_mysql)

        # Ubicación de botones en los marcos
        self.iniciar_ssh_btn.grid(row=0, column=0, padx=10, pady=10)
        self.parar_ssh_btn.grid(row=1, column=0, padx=10, pady=10)
        self.configurar_ssh_btn.grid(row=2, column=0, padx=10, pady=10)

        self.iniciar_mysql_btn.grid(row=0, column=0, padx=10, pady=10)
        self.parar_mysql_btn.grid(row=1, column=0, padx=10, pady=10)
        self.configurar_mysql_btn.grid(row=2, column=0, padx=10, pady=10)

        # Etiqueta para mostrar el estado de la conexión
        self.estado_lbl = ttk.Label(self.frame_estado, textvariable=self.estado_conexion)
        self.estado_lbl.grid(row=3, column=0, columnspan=2, padx=10, pady=10)

        # Inicialmente, el estado está en "Parado"
        self.estado_conexion.set("Parado")

        # Variables para almacenar configuración SSH y MySQL
        self.config_ssh = {
            'Host': tk.StringVar(),
            'Puerto': tk.StringVar(),
            'Usuario': tk.StringVar(),
            'Password': tk.StringVar()
        }

        self.config_mysql = {
            'Host': tk.StringVar(),
            'Puerto': tk.StringVar(),
            'Usuario': tk.StringVar(),
            'Password': tk.StringVar(),
            'BaseDatos': tk.StringVar()
        }

        # Clave privada para cifrar/decifrar configuraciones
        clave_privada = b'prw0ZDOjOpJqmv2gv4oc7O9YSqcBvzKXb3ZSCTvvbss='

        # Cargar configuración inicial desde los archivos INI cifrados
        self.cargar_configuracion('tunel-ssh.ini', self.config_ssh, clave_privada)
        self.cargar_configuracion('db.ini', self.config_mysql, clave_privada)

    # Método para abrir la ventana de configuración SSH
    def abrir_ventana_configuracion_ssh(self):
        archivo_ini = 'tunel-ssh.ini'
        self.abrir_ventana_configuracion(archivo_ini, self.config_ssh, "Configuración SSH")

    # Método para abrir la ventana de configuración MySQL
    def abrir_ventana_configuracion_mysql(self):
        archivo_ini = 'db.ini'
        self.abrir_ventana_configuracion(archivo_ini, self.config_mysql, "Configuración MySQL")

    # Método para abrir una ventana de configuración genérica
    def abrir_ventana_configuracion(self, archivo_ini, configuracion, titulo):
        ventana_configuracion = tk.Toplevel(self.root)
        ventana_configuracion.title(titulo)
        ventana_configuracion.geometry("600x600")

        self.etiquetas_validacion = {}

        ttk.Label(ventana_configuracion, text="Configuración").pack(pady=10)
        self.crear_campos(ventana_configuracion, configuracion)

        self.guardar_btn = ttk.Button(ventana_configuracion, text="Guardar Configuración",
                             command=lambda: self.guardar_configuracion(archivo_ini, configuracion, ventana_configuracion))
        self.guardar_btn.pack(pady=10)
        self.guardar_btn.state(['disabled'])  # Inicialmente, el botón está deshabilitado

    # Método para crear campos de entrada en la ventana de configuración
    def crear_campos(self, ventana, configuracion):
        def validar_alfanumerico(entry_text):
            return bool(re.match(r'^[a-zA-Z0-9\s\.,]*$', entry_text))

        def validar_ip(entry_text):
            return bool(re.match(r'^\d{1,3}(\.\d{1,3}){3}$', entry_text))

        def validar_entero(entry_text):
            return bool(re.match(r'^\d+$', entry_text))

        def validar_password(entry_text):
            return True

        def validar_usuario(entry_text):
            return bool(re.match(r'^[a-zA-Z0-9]{6,}$', entry_text))

        etiquetas = {
            'Host': "Host:",
            'Puerto': "Puerto:",
            'Usuario': "Usuario:",
            'Password': "Password:",
            'BaseDatos': "Base de Datos:"
        }

        for key in configuracion:
            ttk.Label(ventana, text=etiquetas[key]).pack(pady=5)
            entry = ttk.Entry(ventana, textvariable=configuracion[key])
            entry.pack(pady=5)

            etiqueta_validacion = ttk.Label(ventana, text="")
            etiqueta_validacion.pack(pady=5)
            self.etiquetas_validacion[key] = etiqueta_validacion

            validar_func = None
            if key == 'Host':
                validar_func = validar_alfanumerico
            elif key == 'Puerto':
                validar_func = validar_entero
            elif key == 'Usuario':
                validar_func = validar_usuario
            elif key == 'Password':
                validar_func = validar_password
            elif key == 'BaseDatos':
                validar_func = validar_alfanumerico

            if validar_func:
                entry.bind('<FocusOut>', lambda event, key=key: self.validar_campo(event, key, validar_func))

    # Método para validar campos de entrada
    def validar_campo(self, event, key, validar_func):
        valor = self.config_ssh[key].get() if key in self.config_ssh else self.config_mysql[key].get()
        etiqueta_validacion = self.etiquetas_validacion[key]
        if validar_func(valor):
            etiqueta_validacion.config(text="✔️", foreground="green")
        else:
            etiqueta_validacion.config(text="❌", foreground="red")

        campos_validos_ssh = all(self.etiquetas_validacion[key]["text"] == "✔️" for key in self.config_ssh)
        if campos_validos_ssh:
            self.guardar_btn.state(['!disabled'])
        else:
            self.guardar_btn.state(['disabled'])
        campos_validos_mysql = all(self.etiquetas_validacion[key]["text"] == "✔️" for key in self.config_mysql)
        if campos_validos_mysql:
            self.guardar_btn.state(['!disabled'])
        else:
            self.guardar_btn.state(['disabled'])

    # Método para iniciar la conexión SSH
    def iniciar_ssh(self):
        try:
            self.ssh_tunnel = sshtunnel.SSHTunnelForwarder(
                (self.config_ssh['Host'].get(), int(self.config_ssh['Puerto'].get())),
                ssh_username=self.config_ssh['Usuario'].get(),
                ssh_password=self.config_ssh['Password'].get(),
                remote_bind_address=(self.config_mysql['Host'].get(), int(self.config_mysql['Puerto'].get()))
            )
            self.ssh_tunnel.start()
            self.estado_conexion.set("Ok")
        except Exception as e:
            self.estado_conexion.set("Error")
            print(f"Error al iniciar la conexión SSH: {e}")

    # Método para detener la conexión SSH
    def parar_ssh(self):
        try:
            if self.ssh_tunnel:
                self.ssh_tunnel.stop()
                self.estado_conexion.set("Parado")
        except Exception as e:
            print(f"Error al detener la conexión SSH: {e}")

    # Método para iniciar la conexión MySQL
    def iniciar_mysql(self):
        try:
            self.mysql_connection = mysql.connector.connect(
                user=self.config_mysql['Usuario'].get(),
                password=self.config_mysql['Password'].get(),
                host=self.config_mysql['Host'].get(),
                port=int(self.config_mysql['Puerto'].get()),
                database=self.config_mysql['BaseDatos'].get()
            )
            if self.mysql_connection.is_connected():
                self.estado_conexion.set("Ok")
            else:
                self.estado_conexion.set("Error")
        except Exception as e:
            self.estado_conexion.set("Error")
            print(f"Error al iniciar la conexión MySQL: {e}")

    # Método para detener la conexión MySQL
    def parar_mysql(self):
        try:
            if self.mysql_connection:
                self.mysql_connection.close()
                self.estado_conexion.set("Parado")
        except Exception as e:
            print(f"Error al detener la conexión MySQL: {e}")

    # Método para cargar la configuración desde archivos INI cifrados
    def cargar_configuracion(self, archivo_ini, configuracion, clave_privada):
        try:
            with open(archivo_ini, 'rb') as archivo:
                contenido_cifrado = archivo.read()

            fernet = Fernet(clave_privada)
            contenido_descifrado = fernet.decrypt(contenido_cifrado).decode('utf-8')
            config = configparser.ConfigParser()
            config.read_string(contenido_descifrado)

            for key in configuracion:
                if key in config['DEFAULT']:
                    configuracion[key].set(config['DEFAULT'][key])
        except FileNotFoundError:
            # Si el archivo no existe, inicialízalo cifrado
            self.guardar_configuracion_cifrada(archivo_ini, configuracion, clave_privada)

    # Método para guardar la configuración en archivos INI
    def guardar_configuracion(self, archivo_ini, configuracion, ventana):
        config = configparser.ConfigParser()

        config['DEFAULT'] = {
            'Host': configuracion['Host'].get(),
            'Puerto': configuracion['Puerto'].get(),
            'Usuario': configuracion['Usuario'].get(),
            'Password': configuracion['Password'].get()
        }

        with open(archivo_ini, 'w') as configfile:
            config.write(configfile)

        print(f"Configuraciones guardadas en {archivo_ini}")

        clave_privada = b'prw0ZDOjOpJqmv2gv4oc7O9YSqcBvzKXb3ZSCTvvbss='
        self.guardar_configuracion_cifrada(archivo_ini, configuracion, clave_privada)

        ventana.destroy()

    # Método para guardar la configuración cifrada en archivos INI
    def guardar_configuracion_cifrada(self, archivo_ini, configuracion, clave_privada):
        config = configparser.ConfigParser()

        config['DEFAULT'] = {
            'Host': configuracion['Host'].get(),
            'Puerto': configuracion['Puerto'].get(),
            'Usuario': configuracion['Usuario'].get(),
            'Password': configuracion['Password'].get()
        }

        archivo_temporal = archivo_ini + '.temp'

        with open(archivo_temporal, 'w') as configfile:
            config.write(configfile)

        try:
            with open(archivo_temporal, 'rb') as archivo:
                contenido = archivo.read()

            fernet = Fernet(clave_privada)
            contenido_cifrado = fernet.encrypt(contenido)

            with open(archivo_ini, 'wb') as archivo:
                archivo.write(contenido_cifrado)

            print(f"Configuraciones guardadas y cifradas en {archivo_ini}")

        except Exception as e:
            print(f"Error al cifrar y guardar configuraciones: {e}")

        os.remove(archivo_temporal)

        self.root.focus_set()

# Comprobación para ejecutar el programa solo si este es el archivo principal
if __name__ == "__main__":
    root = tk.Tk()
    app = ConfiguracionConexion(root)
    root.mainloop()