From ecaa57fcb11fda5f1ae080cce1befc9ba3bfd725 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81?= Date: Sat, 6 Dec 2025 19:55:04 +0100 Subject: [PATCH] first commit --- ReadMe.md | 1 + __pycache__/client.cpython-313.pyc | Bin 0 -> 68212 bytes client.py | 1041 ++++++++++++++++++++++++++++ main.py | 54 ++ 4 files changed, 1096 insertions(+) create mode 100644 ReadMe.md create mode 100644 __pycache__/client.cpython-313.pyc create mode 100644 client.py create mode 100644 main.py 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 0000000000000000000000000000000000000000..7ec74d486a879d887606b52b6d5743943086e919 GIT binary patch literal 68212 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 T8&*X8V2ieXoBD$SCGP(ZKgJf0 literal 0 HcmV?d00001 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