commit ecaa57fcb11fda5f1ae080cce1befc9ba3bfd725 Author: Алекс Date: Sat Dec 6 19:55:04 2025 +0100 first commit diff --git a/ReadMe.md b/ReadMe.md new file mode 100644 index 0000000..41c01bf --- /dev/null +++ b/ReadMe.md @@ -0,0 +1 @@ +https://youtu.be/uonU5m0xER8 \ No newline at end of file diff --git a/__pycache__/client.cpython-313.pyc b/__pycache__/client.cpython-313.pyc new file mode 100644 index 0000000..7ec74d4 Binary files /dev/null and b/__pycache__/client.cpython-313.pyc differ diff --git a/client.py b/client.py new file mode 100644 index 0000000..32db9fc --- /dev/null +++ b/client.py @@ -0,0 +1,1041 @@ +import tkinter as tk +from tkinter import ttk, messagebox, simpledialog, filedialog +import threading +import time +import datetime +import subprocess +import webbrowser +import shutil +import socket +import queue +import hashlib +import json +import sys +import platform + +try: + import pygame + pygame.mixer.init() +except ModuleNotFoundError: + pygame = None + +try: + from cryptography.hazmat.primitives import hashes as crypto_hashes, serialization + from cryptography.hazmat.primitives.asymmetric import rsa, padding as rsa_padding + from cryptography.hazmat.backends import default_backend +except ModuleNotFoundError: + rsa = None + + +def start_client(): + root = tk.Tk() + root.title("Cliente - Panel PSP (mockup)") + root.geometry("1200x800") + + # Cola de eventos para comunicación hilo->GUI + event_queue = queue.Queue() + + # Grid principal: top bar, main content, status + root.columnconfigure(0, weight=0, minsize=240) + root.columnconfigure(1, weight=1) + root.columnconfigure(2, weight=0, minsize=320) + root.rowconfigure(0, weight=0) # barra superior fija + root.rowconfigure(1, weight=1) # contenido + root.rowconfigure(2, weight=0) # barra estado + + # Barra superior T1..T5 + Configuración + top_bar = tk.Frame(root, bg='#ffffff', height=40) + top_bar.grid(row=0, column=0, columnspan=3, sticky='ew') + top_bar.grid_propagate(False) + + categorias = [ + ('T1. Procesos', '#1e90ff'), + ('T2.Threads', '#c71585'), + ('T3. Sockets', '#ff4500'), + ('T4. Servicios', '#228b22'), + ('T5. Seguridad', '#daa520'), + ('Configuración', '#d3d3d3') + ] + + def seleccionar_categoria(nombre): + info_label.config(text=f"Categoría seleccionada: {nombre}") + # Creamos después info_label; el callback se ejecuta luego y tendrá acceso. + + for i, (texto, color) in enumerate(categorias): + lbl = tk.Label(top_bar, text=texto, bg=color, fg='white', font=('Helvetica', 11, 'bold'), padx=10, pady=6, cursor='hand2') + lbl.pack(side='left', padx=(8 if i==0 else 4, 4), pady=4) + lbl.bind('', lambda e, n=texto: seleccionar_categoria(n)) + + # Frames principales + left = tk.Frame(root, bg="#f8f8f8") + center = tk.Frame(root, bg="#ffffff") + right = tk.Frame(root, bg="#ffffff") + + left.grid(row=1, column=0, sticky="nsw", padx=6, pady=(6,6)) + center.grid(row=1, column=1, sticky="nsew", padx=6, pady=(6,6)) + right.grid(row=1, column=2, sticky="nse", padx=6, pady=(6,6)) + + left.grid_propagate(False) + right.grid_propagate(False) + + # LEFT: acciones y listas + def section(parent, title): + f = tk.LabelFrame(parent, text=title, padx=6, pady=6) + f.pack(fill="x", pady=8, padx=8) + return f + + s_actions = section(left, "") + + # --- funcionalidad adicional --- + def open_vscode(): + exe = shutil.which('code') or shutil.which('code-insiders') or shutil.which('code-oss') + if exe: + try: + subprocess.Popen([exe]) + info_label.config(text="Abriendo Visual Studio Code...") + except Exception as e: + messagebox.showerror("Error", f"No se pudo abrir VS Code: {e}") + else: + webbrowser.open('https://code.visualstudio.com/') + info_label.config(text="VS Code no encontrado: abriendo web como alternativa") + + def open_browser(): + url = simpledialog.askstring('Abrir URL', 'Ingresa la URL a abrir:') + if url and url.strip(): + url = url.strip() + if not url.startswith(('http://', 'https://')): + url = 'https://' + url + try: + webbrowser.open(url) + event_queue.put(('status', f'Abierto navegador: {url}')) + except Exception as e: + event_queue.put(('status', f'Error: {e}')) + + def buscar_google(): + url = 'https://publicapis.dev/' + def worker(): + event_queue.put(('status', f'Scraping APIs desde {url}...')) + try: + import requests + from bs4 import BeautifulSoup + response = requests.get(url, timeout=10, headers={'User-Agent': 'Mozilla/5.0'}) + soup = BeautifulSoup(response.content, 'html.parser') + + # Extraer APIs con sus URLs + apis = [] + + # Buscar todos los links que contengan URLs de APIs + links = soup.find_all('a', href=True) + for link in links: + href = link.get('href', '').strip() + texto = link.get_text().strip() + + # Filtrar links válidos + if (href and (href.startswith('http://') or href.startswith('https://')) and + len(texto) > 2 and len(texto) < 100 and + texto not in [t[0] for t in apis]): # Evitar duplicados por nombre + apis.append((texto, href)) + + # Si no encuentra suficientes, buscar en divs + if len(apis) < 10: + api_elements = soup.find_all(['div', 'section', 'article']) + for elem in api_elements[:50]: + elem_link = elem.find('a', href=True) + if elem_link: + href = elem_link.get('href', '').strip() + texto = elem.get_text().strip()[:80] + if (href and (href.startswith('http://') or href.startswith('https://')) and + len(texto) > 2 and + (texto, href) not in apis): + apis.append((texto, href)) + + # Construir resultado - TODAS las APIs con URLs + resultado = f'\n{"="*70}\n' + resultado += f'URL: {url}\n' + resultado += f'{"="*70}\n\n' + resultado += f'🔗 APIs ENCONTRADAS ({len(apis)})\n' + resultado += f'{"-"*70}\n' + if apis: + for nombre, api_url in apis: + resultado += f' • {nombre}\n' + resultado += f' URL: {api_url}\n\n' + else: + resultado += ' [No se encontraron]\n' + resultado += f'{"="*70}\n' + + event_queue.put(('scrape_result', resultado)) + event_queue.put(('status', f'✓ APIs encontradas: {len(apis)}')) + except Exception as e: + event_queue.put(('scrape_result', f'ERROR AL PROCESAR:\n{str(e)}')) + event_queue.put(('status', f'✗ Error: {e}')) + threading.Thread(target=worker, daemon=True, name='APIsScraper').start() + + + # Alarma + def configurar_alarma(): + minutos = simpledialog.askinteger('Alarma', 'Minutos hasta alarma', minvalue=1, maxvalue=720) + if not minutos: + return + def worker(): + target = time.time() + minutos*60 + while True: + restante = int(target - time.time()) + if restante <= 0: + event_queue.put(('alarm', '¡Alarma!')) + break + event_queue.put(('alarm_progress', restante)) + time.sleep(1) + threading.Thread(target=worker, daemon=True).start() + tk.Button(s_actions, text='Programar Alarma', bg='#ffe0ff', width=24, command=configurar_alarma).pack(pady=6) + tk.Button(s_actions, text="Navegar (URL)", bg="#dff0d8", width=24, command=open_browser).pack(pady=6) + tk.Button(s_actions, text="Buscar API Google", bg="#dff0d8", width=24, command=buscar_google).pack(pady=6) + + # Launch external command with parameters + def launch_command(): + cmd = simpledialog.askstring("Lanzar comando", "Introduce el comando a ejecutar (ej: firefox https://google.com):") + if not cmd: + return + try: + # split by shell to allow parameters; run via shell for convenience + subprocess.Popen(cmd, shell=True) + info_label.config(text=f"Lanzado: {cmd}") + except Exception as e: + messagebox.showerror("Error", f"No se pudo ejecutar el comando: {e}") + + tk.Button(s_actions, text="Lanzar aplicación (con parámetros)", bg="#e8f4ff", width=30, command=launch_command).pack(pady=6) + + # Execute PowerShell script (.ps1) if pwsh/powershell available + def run_powershell_script(): + path = filedialog.askopenfilename(title="Selecciona script PowerShell (.ps1)", filetypes=[("PowerShell", "*.ps1" )]) + if not path: + return + exe = shutil.which('pwsh') or shutil.which('powershell') + if not exe: + messagebox.showwarning("pwsh no encontrado", "No se encontró 'pwsh' ni 'powershell' en PATH. En Linux puedes instalar PowerShell Core (pwsh) o ejecutar el script en Windows.") + return + try: + subprocess.Popen([exe, '-File', path]) + info_label.config(text=f"Ejecutando script PowerShell: {path}") + except Exception as e: + messagebox.showerror("Error", f"Fallo al ejecutar el script: {e}") + + tk.Button(s_actions, text="Ejecutar .ps1 (PowerShell)", bg="#ffeedd", width=30, command=run_powershell_script).pack(pady=6) + + s_apps = section(left, "Aplicaciones") + tk.Button(s_apps, text="Visual Code", bg="#e6f7ff", width=24, command=open_vscode).pack(pady=4) + tk.Button(s_apps, text="App2", bg="#e6f7ff", width=24).pack(pady=4) + tk.Button(s_apps, text="App3", bg="#e6f7ff", width=24).pack(pady=4) + + # Resource monitor and editor + def open_text_editor(): + ed = tk.Toplevel(root) + ed.title("Editor - Notepad simple") + ed.geometry("700x500") + text = tk.Text(ed) + text.pack(fill='both', expand=True) + + def save_file(): + p = filedialog.asksaveasfilename(defaultextension='.txt') + if p: + with open(p, 'w', encoding='utf-8') as f: + f.write(text.get('1.0', 'end')) + info_label.config(text=f"Fichero guardado: {p}") + + def open_file(): + p = filedialog.askopenfilename() + if p: + with open(p, 'r', encoding='utf-8', errors='ignore') as f: + text.delete('1.0', 'end') + text.insert('1.0', f.read()) + info_label.config(text=f"Fichero abierto: {p}") + + btns = tk.Frame(ed) + btns.pack(fill='x') + tk.Button(btns, text='Abrir', command=open_file).pack(side='left') + tk.Button(btns, text='Guardar', command=save_file).pack(side='left') + + tk.Button(s_apps, text="Editor texto", bg="#f0e6ff", width=24, command=open_text_editor).pack(pady=6) + # Hash archivo + def hash_archivo(): + path = filedialog.askopenfilename(title='Selecciona archivo para hash') + if not path: + return + def worker(): + event_queue.put(('status', f'Calculando SHA256 de {path}')) + try: + h = hashlib.sha256() + with open(path, 'rb') as f: + for chunk in iter(lambda: f.read(8192), b''): + h.update(chunk) + event_queue.put(('hash', f'{path}\nSHA256: {h.hexdigest()}')) + event_queue.put(('status', 'Hash completado')) + except Exception as e: + event_queue.put(('status', f'Error hash: {e}')) + threading.Thread(target=worker, daemon=True).start() + tk.Button(s_apps, text='Hash archivo', bg='#e6ffe6', width=24, command=hash_archivo).pack(pady=4) + + def open_resource_monitor(): + # Monitor gráfico sin depender de Pillow: Canvas puro. + try: + import psutil + except ModuleNotFoundError: + messagebox.showerror("Dependencia falta", "Instala 'psutil' para monitor de recursos: pip install psutil") + return + + win = tk.Toplevel(root) + win.title('Monitor recursos (Canvas)') + width, height = 600, 360 + cv = tk.Canvas(win, width=width, height=height, bg='white') + cv.pack(fill='both', expand=True) + + # Series + cpu_data = [] + mem_data = [] + thr_data = [] + maxlen = 120 + + def draw_axes(): + cv.delete('axis') + cv.create_rectangle(50,20,width-20,height-20, outline='#444', tags='axis') + for i in range(6): + y = 20 + (height-40)*i/5 + cv.create_line(50,y,width-20,y, fill='#eee', tags='axis') + cv.create_text(10,20, text='100%', anchor='nw', tags='axis') + cv.create_text(10,height-40, text='0%', anchor='nw', tags='axis') + cv.create_text(width-120,10, text='CPU( rojo ) MEM( verde ) HILOS( azul )', anchor='nw', tags='axis') + + def scale_y(val, series_max=100): + # map 0..series_max to canvas space + return 20 + (height-40)*(1 - val/series_max) + + def draw_series(): + cv.delete('series') + # Determine dynamic max for threads + thr_max = max(thr_data) if thr_data else 1 + def draw_line(data, color, yscale_max): + if len(data) < 2: + return + step_x = (width-70)/ (maxlen-1) + pts = [] + start_index = max(0, len(data)-maxlen) + for idx, val in enumerate(data[start_index:]): + x = 50 + step_x*idx + y = scale_y(val, yscale_max) + pts.append((x,y)) + for i in range(len(pts)-1): + cv.create_line(pts[i][0], pts[i][1], pts[i+1][0], pts[i+1][1], fill=color, width=2, tags='series') + draw_line(cpu_data, 'red', 100) + draw_line(mem_data, 'green', 100) + draw_line(thr_data, 'blue', thr_max) + if thr_data: + cv.create_text(width-180,height-25, text=f'Threads max: {thr_max}', anchor='nw', tags='series', fill='blue') + + def update(): + try: + cpu = psutil.cpu_percent(interval=None) + mem = psutil.virtual_memory().percent + thr = sum(p.num_threads() for p in psutil.process_iter()) + cpu_data.append(cpu); mem_data.append(mem); thr_data.append(thr) + cpu_data[:] = cpu_data[-maxlen:] + mem_data[:] = mem_data[-maxlen:] + thr_data[:] = thr_data[-maxlen:] + draw_axes(); draw_series() + cv.create_text(60, height-25, text=f'CPU {cpu:.1f}% MEM {mem:.1f}% HILOS {thr}', anchor='nw', fill='#222', tags='series') + except tk.TclError: + return + win.after(1000, update) + draw_axes(); update() + + tk.Button(left, text="Monitor recursos (gráficas)", bg="#fff2cc", width=30, command=open_resource_monitor).pack(pady=6) + + s_batch = section(left, "Procesos batch") + def realizar_backup(): + # OS-aware backup: Windows uses PowerShell Compress-Archive; Linux uses tar.gz + src = filedialog.askdirectory(title='Directorio origen a respaldar') + if not src: + return + dest = filedialog.asksaveasfilename(title='Archivo destino backup', defaultextension='.zip' if platform.system()=='Windows' else '.tar.gz') + if not dest: + return + def worker(): + event_queue.put(('status', f'Iniciando backup de {src} -> {dest}')) + if platform.system()=='Windows': + exe = shutil.which('powershell') or shutil.which('pwsh') + if not exe: + event_queue.put(('status','PowerShell no encontrado')) + return + # Compress-Archive -Path src -DestinationPath dest + cmd = [exe, '-Command', f"Compress-Archive -Path '{src}' -DestinationPath '{dest}' -Force"] + try: + subprocess.run(cmd, timeout=600) + event_queue.put(('status','Backup completado (Windows)')) + except Exception as e: + event_queue.put(('status', f'Error backup: {e}')) + else: + # Linux/Unix: tar -czf dest -C parent basename + import os + parent = os.path.dirname(src) + base = os.path.basename(src) + cmd = ['tar','-czf',dest,'-C',parent,base] + try: + subprocess.run(cmd, timeout=600) + event_queue.put(('status','Backup completado (tar.gz)')) + except Exception as e: + event_queue.put(('status', f'Error backup: {e}')) + threading.Thread(target=worker, daemon=True).start() + tk.Button(s_batch, text="Copias de seguridad", bg="#fff0d6", width=24, command=realizar_backup).pack(pady=6) + + # CENTER: Notebook grande + panel inferior + center.rowconfigure(0, weight=1) + center.rowconfigure(1, weight=0) + center.columnconfigure(0, weight=1) + + notebook = ttk.Notebook(center) + notebook.grid(row=0, column=0, sticky="nsew", padx=6, pady=6) + + # store text widgets so we can update a specific tab programmatically + tab_texts = {} + tab_names = ["Resultados", "Navegador", "Correos", "Tareas", "Alarmas", "Enlaces", "Servicios", "Seguridad"] + for name in tab_names: + f = ttk.Frame(notebook) + notebook.add(f, text=name) + + # Añadir botones ANTES del área de texto para que sean visibles + if name == 'Navegador': + tk.Label(f, text='Herramientas de navegación:', font=('Arial', 10, 'bold')).pack(anchor='w', padx=6, pady=4) + nav_frame = tk.Frame(f) + nav_frame.pack(anchor='w', padx=6, pady=4) + + def abrir_url_navegador(): + url = simpledialog.askstring('Abrir URL', 'Ingresa la URL a abrir:') + if url and url.strip(): + url = url.strip() + if not url.startswith(('http://', 'https://')): + url = 'https://' + url + try: + webbrowser.open(url) + event_queue.put(('status', f'Abriendo: {url}')) + except Exception as e: + event_queue.put(('status', f'Error: {e}')) + + def iniciar_scraping(): + url = simpledialog.askstring('Web Scraping', 'Ingresa la URL a escanear:') + if url and url.strip(): + url = url.strip() + if not url.startswith(('http://', 'https://')): + url = 'https://' + url + def worker(): + event_queue.put(('status', f'Scraping: {url}...')) + try: + import requests, re + from bs4 import BeautifulSoup + response = requests.get(url, timeout=10, headers={'User-Agent': 'Mozilla/5.0'}) + soup = BeautifulSoup(response.content, 'html.parser') + + # Extraer emails + emails = sorted(list(set(re.findall(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', response.text)))) + + # Extraer teléfonos + telefonos = sorted(list(set(re.findall(r'(?:\+?\d{1,3}[-.]?)?\(?\d{2,4}\)?[-.]?\d{2,4}[-.]?\d{2,4}|\d{9,}', response.text)))) + telefonos = [t for t in telefonos if len(t) >= 9] + + # Extraer nombres/títulos + nombres = set() + for elem in soup.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'b', 'strong', 'a']): + texto = elem.get_text().strip() + if 2 <= len(texto) <= 50: + nombres.add(texto) + nombres = sorted(list(nombres))[:30] + + # Construir resultado con mejor formato + resultado = f'\n{"="*70}\n' + resultado += f'URL: {url}\n' + resultado += f'{"="*70}\n\n' + + # Sección de Emails + resultado += f'📧 EMAILS ({len(emails)})\n' + resultado += f'{"-"*70}\n' + if emails: + for e in emails[:15]: + resultado += f' {e}\n' + if len(emails) > 15: + resultado += f'\n ... y {len(emails)-15} más\n' + else: + resultado += ' [No se encontraron]\n' + resultado += '\n' + + # Sección de Teléfonos + resultado += f'📞 TELÉFONOS ({len(telefonos)})\n' + resultado += f'{"-"*70}\n' + if telefonos: + for t in telefonos[:15]: + resultado += f' {t}\n' + if len(telefonos) > 15: + resultado += f'\n ... y {len(telefonos)-15} más\n' + else: + resultado += ' [No se encontraron]\n' + resultado += '\n' + + # Sección de Títulos/Nombres + resultado += f'👥 TÍTULOS/NOMBRES ({len(nombres)})\n' + resultado += f'{"-"*70}\n' + if nombres: + for n in nombres[:20]: + resultado += f' {n}\n' + if len(nombres) > 20: + resultado += f'\n ... y {len(nombres)-20} más\n' + else: + resultado += ' [No se encontraron]\n' + resultado += f'\n{"="*70}\n' + + event_queue.put(('scrape_result', resultado)) + event_queue.put(('status', f'✓ Completado: {len(emails)} emails, {len(telefonos)} telefonos')) + except Exception as e: + event_queue.put(('scrape_result', f'ERROR AL PROCESAR:\n{str(e)}')) + event_queue.put(('status', f'✗ Error: {e}')) + threading.Thread(target=worker, daemon=True, name='Scraper').start() + + tk.Button(nav_frame, text='🌐 Abrir URL', bg='#87CEEB', font=('Arial', 10, 'bold'), command=abrir_url_navegador).pack(side='left', padx=2) + tk.Button(nav_frame, text='🔍 Extraer Datos', bg='#FFB6C1', font=('Arial', 10, 'bold'), command=iniciar_scraping).pack(side='left', padx=2) + + if name == 'Tareas': + btn_frame = tk.Frame(f) + btn_frame.pack(anchor='w', padx=6, pady=4) + tk.Button(btn_frame, text='🏁 Iniciar Carrera de Camellos', bg='#90EE90', font=('Arial', 10, 'bold'), + command=lambda: iniciar_carrera(tab_texts['Tareas'])).pack(side='left', padx=2) + + if name == 'Servicios': + srv_frame = tk.Frame(f) + srv_frame.pack(anchor='w', padx=6, pady=4) + tk.Button(srv_frame, text='POP3 listar', command=lambda: servicio_pop3()).pack(side='left', padx=2) + tk.Button(srv_frame, text='SMTP enviar', command=lambda: servicio_smtp()).pack(side='left', padx=2) + tk.Button(srv_frame, text='FTP listar', command=lambda: servicio_ftp()).pack(side='left', padx=2) + tk.Button(srv_frame, text='HTTP GET', command=lambda: consumir_api('https://httpbin.org/get')).pack(side='left', padx=2) + + if name == 'Seguridad': + sec_frame = tk.Frame(f) + sec_frame.pack(anchor='w', padx=6, pady=4) + tk.Button(sec_frame, text='Generar RSA', command=lambda: generar_rsa()).pack(side='left', padx=2) + tk.Button(sec_frame, text='AES Cifrar', command=lambda: aes_cifrar()).pack(side='left', padx=2) + + # Área de texto después de los botones + txt = tk.Text(f) + txt.insert("1.0", f"{name} - área de contenido\n\n") + txt.pack(fill="both", expand=True, padx=6, pady=6) + tab_texts[name] = txt + + info = tk.Frame(center, bg="#f7fff0", height=120) + info.grid(row=1, column=0, sticky="ew", padx=6, pady=(0,6)) + info.grid_propagate(False) + info_label = tk.Label(info, text="Panel para notas informativas y mensajes sobre la ejecución de los hilos.", bg="#f7fff0", anchor="w") + info_label.pack(fill='both', expand=True, padx=8, pady=8) + + # RIGHT: Chat y lista de alumnos + chat_box = tk.LabelFrame(right, text="Chat", padx=6, pady=6) + chat_box.pack(fill="x", padx=8, pady=(8,4)) + tk.Label(chat_box, text="Mensaje").pack(anchor="w") + msg = tk.Text(chat_box, height=6) + msg.pack(fill="x", pady=4) + tk.Button(chat_box, text="enviar", bg="#cfe8cf").pack(pady=(0,6)) + + students = tk.LabelFrame(right, text="Alumnos", padx=6, pady=6) + students.pack(fill="both", expand=True, padx=8, pady=(4,8)) + for i in range(1, 4): + s = tk.Frame(students) + s.pack(fill="x", pady=6) + tk.Label(s, text=f"Alumno {i}", font=("Helvetica", 13, "bold")).pack(anchor="w") + tk.Label(s, text="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod.", wraplength=280, justify="left").pack(anchor="w") + + # Reproductor música + music_state = {'path': None, 'thread': None, 'playing': False, 'stopping': False} + def seleccionar_musica(): + p = filedialog.askopenfilename(title='Seleccionar audio', filetypes=[('Audio','*.wav *.mp3 *.ogg'), ('Todos','*.*')]) + if p: + music_state['path'] = p + info_label.config(text=f'Audio: {p}') + def reproducir_musica(): + if pygame is None: + messagebox.showwarning('Audio', 'Instala pygame: pip install pygame') + return + path = music_state.get('path') + if not path: + seleccionar_musica(); path = music_state.get('path') + if not path: + return + if pygame.mixer.music.get_busy(): + event_queue.put(('status', 'Ya reproduciendo')) + return + music_state['stopping'] = False + def worker(): + event_queue.put(('status', f'Reproduciendo {path}')) + try: + pygame.mixer.music.load(path) + pygame.mixer.music.play() + music_state['playing'] = True + # Esperar mientras reproduce y no se detiene + while pygame.mixer.music.get_busy() and not music_state['stopping']: + time.sleep(0.1) + if not music_state['stopping']: + event_queue.put(('status', 'Audio terminado')) + music_state['playing'] = False + except Exception as e: + event_queue.put(('status', f'Error audio: {e}')) + music_state['playing'] = False + t = threading.Thread(target=worker, daemon=True); music_state['thread']=t; t.start() + def detener_musica(): + try: + if pygame and pygame.mixer.music.get_busy(): + music_state['stopping'] = True + pygame.mixer.music.stop() + event_queue.put(('status', 'Audio detenido')) + else: + event_queue.put(('status', 'No hay audio reproduciéndose')) + except Exception as e: + event_queue.put(('status', f'Error al detener: {e}')) + music_box = tk.LabelFrame(right, text='Reproductor música', padx=4, pady=4) + music_box.pack(fill='x', padx=8, pady=(0,8)) + tk.Button(music_box, text='Seleccionar', command=seleccionar_musica).pack(side='left', padx=2) + tk.Button(music_box, text='Play', command=reproducir_musica).pack(side='left', padx=2) + tk.Button(music_box, text='Stop', command=detener_musica).pack(side='left', padx=2) + + # Chat cliente + chat_client = {'sock': None} + def conectar_chat(): + if chat_client['sock']: + messagebox.showinfo('Chat','Ya conectado') + return + host = simpledialog.askstring('Host','Host chat', initialvalue='127.0.0.1') + port = simpledialog.askinteger('Puerto','Puerto', initialvalue=3333) + if not host or not port: + return + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.connect((host, port)) + chat_client['sock']=s + event_queue.put(('status', f'Chat conectado {host}:{port}')) + def receptor(): + try: + while True: + data = s.recv(1024) + if not data: + break + event_queue.put(('chat', data.decode(errors='ignore'))) + except Exception as e: + event_queue.put(('status', f'Error chat: {e}')) + finally: + s.close(); chat_client['sock']=None + event_queue.put(('status','Chat desconectado')) + threading.Thread(target=receptor, daemon=True).start() + except Exception as e: + messagebox.showerror('Chat', f'Error conexión: {e}') + def enviar_chat(): + texto = msg.get('1.0','end').strip() + if not texto: + return + s = chat_client.get('sock') + if not s: + messagebox.showwarning('Chat','No conectado') + return + try: + s.send(texto.encode()) + msg.delete('1.0','end') + except Exception as e: + event_queue.put(('status', f'Error envío: {e}')) + tk.Button(chat_box, text='Conectar', bg='#ddeeff', command=conectar_chat).pack(pady=(0,4)) + tk.Button(chat_box, text='Enviar mensaje', bg='#cfe8cf', command=enviar_chat).pack(pady=(0,6)) + + # Sección Sockets (TCP/UDP servers) añadida al panel izquierdo + s_sockets = section(left, 'Sockets Locales') + tcp_state = {'thread': None, 'stop': False, 'sock': None} + udp_state = {'thread': None, 'stop': False, 'sock': None} + + def start_tcp_server(): + if tcp_state['thread'] and tcp_state['thread'].is_alive(): + info_label.config(text='TCP Server ya iniciado') + return + port = simpledialog.askinteger('TCP Server','Puerto', initialvalue=5555) + if not port: + return + def worker(): + event_queue.put(('status', f'TCP Server escuchando {port}')) + import socket as s + srv = s.socket(s.AF_INET, s.SOCK_STREAM) + srv.setsockopt(s.SOL_SOCKET, s.SO_REUSEADDR,1) + srv.bind(('0.0.0.0', port)) + srv.listen(5) + tcp_state['sock']=srv + while not tcp_state['stop']: + try: + srv.settimeout(1.0) + try: + c, addr = srv.accept() + except s.timeout: + continue + event_queue.put(('status', f'Nueva conexión TCP {addr}')) + threading.Thread(target=lambda: manejar_tcp_cliente(c, addr), daemon=True).start() + except Exception as e: + event_queue.put(('status', f'Error server TCP: {e}')) + break + srv.close(); tcp_state['sock']=None; tcp_state['stop']=False + event_queue.put(('status','TCP Server detenido')) + tcp_state['stop']=False + t = threading.Thread(target=worker, daemon=True); tcp_state['thread']=t; t.start() + + def manejar_tcp_cliente(c, addr): + try: + while True: + data = c.recv(1024) + if not data: + break + event_queue.put(('status', f'TCP {addr} -> {data[:40]!r}')) + c.send(b'ACK') + except Exception as e: + event_queue.put(('status', f'Error cliente TCP {addr}: {e}')) + finally: + c.close() + + def stop_tcp_server(): + tcp_state['stop']=True + + def start_udp_server(): + if udp_state['thread'] and udp_state['thread'].is_alive(): + info_label.config(text='UDP Server ya iniciado') + return + port = simpledialog.askinteger('UDP Server','Puerto', initialvalue=5556) + if not port: + return + def worker(): + event_queue.put(('status', f'UDP Server escuchando {port}')) + import socket as s + srv = s.socket(s.AF_INET, s.SOCK_DGRAM) + srv.bind(('0.0.0.0', port)) + udp_state['sock']=srv + srv.settimeout(1.0) + while not udp_state['stop']: + try: + try: + data, addr = srv.recvfrom(1024) + except s.timeout: + continue + event_queue.put(('status', f'UDP {addr} -> {data[:40]!r}')) + srv.sendto(b'ACK', addr) + except Exception as e: + event_queue.put(('status', f'Error server UDP: {e}')) + break + srv.close(); udp_state['sock']=None; udp_state['stop']=False + event_queue.put(('status','UDP Server detenido')) + udp_state['stop']=False + t = threading.Thread(target=worker, daemon=True); udp_state['thread']=t; t.start() + + def stop_udp_server(): + udp_state['stop']=True + + tk.Button(s_sockets, text='Start TCP', bg='#e0ffe0', command=start_tcp_server).pack(pady=2, fill='x') + tk.Button(s_sockets, text='Stop TCP', bg='#ffe0e0', command=stop_tcp_server).pack(pady=2, fill='x') + tk.Button(s_sockets, text='Start UDP', bg='#e0ffe0', command=start_udp_server).pack(pady=2, fill='x') + tk.Button(s_sockets, text='Stop UDP', bg='#ffe0e0', command=stop_udp_server).pack(pady=2, fill='x') + + # Carrera camellos con sincronización mejorada + race_state = {'running': False, 'winner': None, 'lock': threading.Lock(), 'condition': threading.Condition()} + + def iniciar_carrera(text_widget): + # Verificar si ya hay una carrera en curso + with race_state['lock']: + if race_state['running']: + event_queue.put(('status', 'Ya hay una carrera en curso')) + return + race_state['running'] = True + race_state['winner'] = None + + corredores = 5 + posiciones = [0] * corredores + meta = 50 + ganador_declarado = threading.Event() + + def corredor(i): + import random + nombre = f"Camello {i+1}" + try: + while not ganador_declarado.is_set(): + # Sincronización: solo un corredor avanza a la vez + with race_state['lock']: + # Verificar si alguien ya ganó + if ganador_declarado.is_set(): + break + + # Avanzar + avance = random.randint(1, 3) + posiciones[i] += avance + + # Actualizar visualización + event_queue.put(('race_update', (i, posiciones[i], meta))) + + # Verificar si alcanzó la meta + if posiciones[i] >= meta and not ganador_declarado.is_set(): + ganador_declarado.set() + race_state['winner'] = i + event_queue.put(('race_end', i)) + break + + # Esperar un poco antes del siguiente avance + time.sleep(random.uniform(0.1, 0.2)) + except Exception as e: + event_queue.put(('status', f'Error en {nombre}: {e}')) + finally: + # Asegurar que liberamos recursos + with race_state['lock']: + pass + + # Iniciar todos los corredores + event_queue.put(('race_start', corredores)) + for i in range(corredores): + threading.Thread(target=corredor, args=(i,), daemon=True, name=f'Corredor-{i+1}').start() + + # Thread que monitorea finalización + def monitor_finalizacion(): + ganador_declarado.wait(timeout=30) # Timeout de seguridad + time.sleep(0.5) # Esperar a que se procesen últimos eventos + with race_state['lock']: + race_state['running'] = False + if race_state['winner'] is None: + event_queue.put(('status', 'Carrera terminada (timeout)')) + + threading.Thread(target=monitor_finalizacion, daemon=True, name='Monitor-Carrera').start() + + # Servicios placeholders + def servicio_pop3(): + event_queue.put(('status','POP3 placeholder (implementación futura)')) + def servicio_smtp(): + event_queue.put(('status','SMTP placeholder (implementación futura)')) + def servicio_ftp(): + event_queue.put(('status','FTP placeholder (implementación futura)')) + + # Seguridad avanzada + def generar_rsa(): + def worker(): + if rsa is None: + event_queue.put(('status','Instala cryptography para RSA')) + return + event_queue.put(('status','Generando claves RSA...')) + try: + key = rsa.generate_private_key(public_exponent=65537, key_size=2048, backend=default_backend()) + pub = key.public_key().public_bytes(serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo) + event_queue.put(('hash', f'Llave pública RSA:\n{pub.decode()}')) + event_queue.put(('status','RSA generado')) + except Exception as e: + event_queue.put(('status', f'Error RSA: {e}')) + threading.Thread(target=worker, daemon=True).start() + + def aes_cifrar(): + texto = simpledialog.askstring('AES','Texto a cifrar') + if not texto: + return + def worker(): + try: + from cryptography.fernet import Fernet + except ModuleNotFoundError: + event_queue.put(('status','Instala cryptography para AES/Fernet')) + return + key = Fernet.generate_key(); f = Fernet(key) + ct = f.encrypt(texto.encode()) + event_queue.put(('hash', f'AES(Fernet)\nKey: {key.decode()}\nCT: {ct.decode()}')) + event_queue.put(('status','Texto cifrado')) + threading.Thread(target=worker, daemon=True).start() + + # STATUS BAR + status = tk.Frame(root, bd=1, relief="sunken") + status.grid(row=2, column=0, columnspan=3, sticky="ew") + status.columnconfigure(0, weight=1) + status.columnconfigure(1, weight=1) + status.columnconfigure(2, weight=1) + status.columnconfigure(3, weight=1) + status.columnconfigure(4, weight=1) + + lbl_mail = tk.Label(status, text="Correos sin leer", anchor="w", padx=6) + lbl_temp = tk.Label(status, text="Temperatura local", anchor="w") + lbl_net = tk.Label(status, text="Net 0 KB/s in / 0 KB/s out", anchor="w") + lbl_dt = tk.Label(status, text="Fecha Día y Hora", anchor="e") + lbl_alarm = tk.Label(status, text='Alarma: --', anchor='w') + + lbl_mail.grid(row=0, column=0, sticky="w") + lbl_temp.grid(row=0, column=1, sticky="w") + lbl_net.grid(row=0, column=2, sticky="w") + lbl_dt.grid(row=0, column=3, sticky="e", padx=6) + lbl_alarm.grid(row=0, column=4, sticky='w') + + # Hilo para actualizar la fecha/hora + def updater(lbl): + try: + while True: + now = datetime.datetime.now() + lbl_text = f"{now.strftime('%A')}, {now.strftime('%Y-%m-%d %H:%M:%S')}" + lbl.after(0, lbl.config, {"text": lbl_text}) + time.sleep(1) + except tk.TclError: + return + + th = threading.Thread(target=updater, args=(lbl_dt,), daemon=True) + th.start() + + # Network I/O monitor (kb/s) + def net_io_runner(lbl): + try: + import psutil + except ModuleNotFoundError: + lbl.after(0, lbl.config, {"text": "Instala psutil para monitor red (pip install psutil)"}) + return + prev = psutil.net_io_counters() + prev_time = time.time() + try: + while True: + time.sleep(1) + cur = psutil.net_io_counters() + now = time.time() + dt = now - prev_time if now - prev_time > 0 else 1 + sent_b = cur.bytes_sent - prev.bytes_sent + recv_b = cur.bytes_recv - prev.bytes_recv + sent_k = sent_b / 1024.0 / dt + recv_k = recv_b / 1024.0 / dt + prev = cur + prev_time = now + lbl_text = f"Net {recv_k:.1f} KB/s in / {sent_k:.1f} KB/s out" + lbl.after(0, lbl.config, {"text": lbl_text}) + except tk.TclError: + return + + threading.Thread(target=net_io_runner, args=(lbl_net,), daemon=True).start() + + # Consumir API REST con detalles mejorados + def consumir_api(url='https://datatracker.ietf.org/doc/html/rfc6749'): + def worker(): + event_queue.put(('status', f'GET {url}')) + try: + import requests + from bs4 import BeautifulSoup + r = requests.get(url, timeout=10) + + # Construir respuesta detallada + output = [] + output.append("="*80) + output.append(f"URL: {url}") + output.append(f"Status: {r.status_code} {r.reason}") + output.append(f"Content-Type: {r.headers.get('Content-Type', 'N/A')}") + output.append(f"Content-Length: {r.headers.get('Content-Length', 'N/A')}") + output.append(f"Server: {r.headers.get('Server', 'N/A')}") + output.append("="*80) + output.append("\nHEADERS:") + for k, v in list(r.headers.items())[:10]: + output.append(f" {k}: {v}") + output.append("\n" + "="*80) + output.append("RESPONSE BODY:\n") + + # Intentar formatear según tipo de contenido + content_type = r.headers.get('Content-Type', '').lower() + + if 'application/json' in content_type: + try: + data = r.json() + if isinstance(data, list): + output.append(f"Array with {len(data)} elements:\n") + output.append(json.dumps(data[:5], indent=2)) # Primeros 5 elementos + if len(data) > 5: + output.append(f"\n... and {len(data) - 5} more items") + else: + output.append(json.dumps(data, indent=2)[:3000]) + except Exception as e: + output.append(f"JSON parse error: {e}\n{r.text[:2000]}") + + elif 'text/html' in content_type: + try: + soup = BeautifulSoup(r.text, 'html.parser') + # Extraer título + title = soup.find('title') + if title: + output.append(f"Title: {title.get_text()}\n") + # Extraer primeros párrafos + paragraphs = soup.find_all('p')[:5] + for p in paragraphs: + text = p.get_text().strip() + if text: + output.append(f"{text}\n") + output.append(f"\n[HTML document with {len(soup.find_all())} tags]") + except Exception: + output.append(r.text[:2000]) + + else: + # Texto plano u otro + output.append(r.text[:3000]) + + texto = '\n'.join(output) + event_queue.put(('api', texto)) + event_queue.put(('status', f'✓ Respuesta {r.status_code} - {len(r.content)} bytes')) + except Exception as e: + event_queue.put(('api', f'ERROR: {str(e)}')) + event_queue.put(('status', f'✗ Error API: {e}')) + threading.Thread(target=worker, daemon=True).start() + + # Añadir botón API en pestaña Navegador después de crear pestañas (modificar contenido) + + # Pump de cola + def pump_queue(): + try: + while True: + tipo, payload = event_queue.get_nowait() + if tipo == 'status': + info_label.config(text=payload) + elif tipo == 'scrape': + tab_texts['Resultados'].insert('end', payload + '\n---\n') + elif tipo == 'hash': + tab_texts['Resultados'].insert('end', payload + '\n') + elif tipo == 'chat': + tab_texts['Resultados'].insert('end', f'[CHAT] {payload}\n') + elif tipo == 'alarm_progress': + lbl_alarm.config(text=f'Alarma: {payload}s') + elif tipo == 'alarm': + lbl_alarm.config(text='Alarma disparada') + messagebox.showinfo('Alarma', '¡Tiempo cumplido!') + elif tipo == 'api': + tab_texts['Navegador'].insert('end', payload + '\n') + elif tipo == 'race_start': + num_corredores = payload + tab_texts['Tareas'].delete('1.0', 'end') + # Crear tags con colores para cada camello + colores = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8'] + for i in range(num_corredores): + tab_texts['Tareas'].tag_config(f'camello_{i}', foreground=colores[i % len(colores)], font=('Courier', 10, 'bold')) + tab_texts['Tareas'].insert('end', f'🏁 CARRERA INICIADA - {num_corredores} camellos 🏁\n', 'title') + tab_texts['Tareas'].insert('end', '='*60 + '\n\n') + for i in range(num_corredores): + tab_texts['Tareas'].insert('end', f'Camello {i+1}: [░'*40 + f'] 0/{num_corredores*10}\n', f'camello_{i}') + elif tipo == 'race_update': + idx, pos, meta = payload + progreso = int((pos / meta) * 40) + barra = '█' * progreso + '░' * (40 - progreso) + line_num = idx + 4 + try: + inicio = tab_texts['Tareas'].index(f'{line_num}.0') + fin = tab_texts['Tareas'].index(f'{line_num}.end') + tab_texts['Tareas'].delete(inicio, fin) + nueva_linea = f'Camello {idx+1}: [{barra}] {pos}/{meta}' + tab_texts['Tareas'].insert(inicio, nueva_linea, f'camello_{idx}') + except: + pass + elif tipo == 'race_end': + g = payload + tab_texts['Tareas'].insert('end', '\n' + '='*60 + '\n') + tab_texts['Tareas'].insert('end', f'🏆 ¡GANADOR: CAMELLO {g+1}! 🏆\n') + tab_texts['Tareas'].insert('end', '='*60 + '\n') + event_queue.put(('status', f'Carrera finalizada - Ganó Camello {g+1}')) + elif tipo == 'scrape_result': + tab_texts['Navegador'].delete('1.0', 'end') + tab_texts['Navegador'].insert('end', payload) + except queue.Empty: + pass + root.after(200, pump_queue) + root.after(200, pump_queue) + + root.mainloop() + + +if __name__ == '__main__': + start_client() \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..b407215 --- /dev/null +++ b/main.py @@ -0,0 +1,54 @@ +import socket +import threading + +# Configuraci n del servidor +HOST = '0.0.0.0' # Escucha en todas las interfaces de red +PORT = 3333 # Puerto de escucha +clients = [] # Lista para almacenar los clientes conectados + +# Funci n para retransmitir mensajes a todos los clientes +def broadcast(message, client_socket): + for client in clients: + if client != client_socket: # Evitar enviar el mensaje al remitente + try: + client.send(message) + except: + # Si falla el env o, eliminar el cliente + clients.remove(client) + +# Función para manejar la comunicación con un cliente +def handle_client(client_socket, client_address): + print(f"[NUEVO CLIENTE] {client_address} conectado.") + while True: + try: + # Recibir mensaje del cliente + message = client_socket.recv(1024) + if not message: + break # Si no hay mensaje, el cliente cerr la conexi n + print(f"[{client_address}] {message.decode('utf-8')}") + # Retransmitir el mensaje a los dem s clientes + broadcast(message, client_socket) + except: + print(f"[DESCONECTADO] {client_address} se ha desconectado.") + clients.remove(client_socket) + client_socket.close() + break + +# Funci n principal para iniciar el servidor +def start_server(): + server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # IPv4, TCP + server.bind((HOST, PORT)) + server.listen(5) # M ximo 5 conexiones en cola + print(f"[INICIO] Servidor escuchando en {HOST}:{PORT}") + + while True: + client_socket, client_address = server.accept() # Aceptar nueva conexión + clients.append(client_socket) + print(f"[CONECTADO] Nueva conexi n desde {client_address}") + # Iniciar un hilo para manejar el cliente + client_thread = threading.Thread(target=handle_client, args=(client_socket, client_address)) + client_thread.start() + +# Iniciar el servidor +if __name__ == "__main__": + start_server() \ No newline at end of file