#!/usr/bin/env python3
"""
Balse — lector de capturas de tráfico (.pcap)
Proyecto educativo de Datapoint

Lee un archivo de captura de tráfico en formato .pcap (el formato que
usa Wireshark) y muestra cada paquete de forma legible: de dónde viene,
hacia dónde va, y qué protocolo usa. Está escrito a mano, sin librerías
externas (ni scapy ni dpkt), para que puedas leer el código completo y
entender exactamente cómo está armado un paquete de red por dentro.

Este script NO captura tráfico en vivo — de eso también se encarga
Wireshark, y requiere permisos especiales del sistema operativo. Este
lee un archivo .pcap que ya existe (por ejemplo, sample-traffic.pcap,
incluido junto a este script).

Cómo usarlo:
    python3 pcap_reader.py sample-traffic.pcap

Requiere: Python 3.7 o más nuevo. No necesita instalar nada más.
"""

import argparse
import os
import socket
import struct
import sys

PROTOCOLOS_IP = {1: "ICMP", 6: "TCP", 17: "UDP"}


def leer_pcap(ruta: str) -> None:
    with open(ruta, "rb") as f:
        cabecera_global = f.read(24)
        if len(cabecera_global) < 24:
            print("Archivo demasiado corto para ser un .pcap válido.")
            sys.exit(1)

        magic = struct.unpack("I", cabecera_global[0:4])[0]
        if magic == 0xA1B2C3D4:
            endian = "<"  # little-endian
        elif magic == 0xD4C3B2A1:
            endian = ">"  # big-endian
        else:
            print("Este archivo no parece ser un .pcap válido (número mágico incorrecto).")
            sys.exit(1)

        numero_paquete = 0
        while True:
            cabecera_paquete = f.read(16)
            if len(cabecera_paquete) < 16:
                break  # fin del archivo

            ts_sec, ts_usec, incl_len, orig_len = struct.unpack(
                endian + "IIII", cabecera_paquete
            )
            datos = f.read(incl_len)
            numero_paquete += 1
            describir_paquete(numero_paquete, ts_sec, datos)

        print(f"\nTotal: {numero_paquete} paquetes leídos de {ruta}")


def describir_paquete(numero: int, ts_sec: int, datos: bytes) -> None:
    resumen = f"#{numero:<3} "

    if len(datos) < 14:
        print(resumen + "(paquete demasiado corto para tener cabecera Ethernet)")
        return

    # Cabecera Ethernet: 6 bytes MAC destino, 6 bytes MAC origen, 2 bytes ethertype.
    ethertype = struct.unpack("!H", datos[12:14])[0]

    if ethertype != 0x0800:  # solo interpretamos IPv4 en esta versión educativa
        print(resumen + f"Ethernet (ethertype 0x{ethertype:04x}, no es IPv4, no se detalla)")
        return

    ip_datos = datos[14:]
    if len(ip_datos) < 20:
        print(resumen + "IPv4 (cabecera incompleta)")
        return

    version_ihl = ip_datos[0]
    ihl = (version_ihl & 0x0F) * 4  # longitud de la cabecera IP en bytes
    protocolo = ip_datos[9]
    ip_origen = socket.inet_ntoa(ip_datos[12:16])
    ip_destino = socket.inet_ntoa(ip_datos[16:20])
    nombre_protocolo = PROTOCOLOS_IP.get(protocolo, f"protocolo {protocolo}")

    detalle_puertos = ""
    payload = ip_datos[ihl:]
    if protocolo in (6, 17) and len(payload) >= 4:  # TCP o UDP: leer puertos
        puerto_origen, puerto_destino = struct.unpack("!HH", payload[0:4])
        detalle_puertos = f", puerto {puerto_origen} → {puerto_destino}"
    elif protocolo == 1 and len(payload) >= 2:  # ICMP: leer tipo
        tipo_icmp = payload[0]
        etiquetas = {8: "echo request (ping)", 0: "echo reply (respuesta de ping)"}
        detalle_puertos = f", tipo {etiquetas.get(tipo_icmp, tipo_icmp)}"

    print(
        resumen
        + f"[t={ts_sec}] {ip_origen} → {ip_destino}  {nombre_protocolo}{detalle_puertos}"
    )


def pausar_al_salir() -> None:
    """Evita que la ventana se cierre de golpe si el script se abrió con doble clic."""
    try:
        input("\nPresiona Enter para cerrar...")
    except (EOFError, KeyboardInterrupt):
        pass


def main() -> None:
    parser = argparse.ArgumentParser(
        description="Lector educativo de archivos .pcap — Balse / Datapoint"
    )
    parser.add_argument(
        "archivo", nargs="?", default=None, help="Ruta al archivo .pcap a leer"
    )
    args = parser.parse_args()

    archivo = args.archivo
    if not archivo:
        print("Balse — lector de capturas .pcap\n")
        sugerido = "sample-traffic.pcap"
        existe_sugerido = os.path.exists(sugerido)
        prompt = (
            f"¿Qué archivo .pcap quieres leer? (Enter para usar {sugerido}): "
            if existe_sugerido
            else "¿Qué archivo .pcap quieres leer?: "
        )
        respuesta = input(prompt).strip()
        archivo = respuesta if respuesta else (sugerido if existe_sugerido else None)
        if not archivo:
            print("No escribiste nada, saliendo.")
            pausar_al_salir()
            return

    try:
        leer_pcap(archivo)
    except FileNotFoundError:
        print(f"No se encontró el archivo '{archivo}'.")
        pausar_al_salir()
        return

    print(
        "\nTip: en la práctica, un IP de origen que se repite mandando muchos\n"
        "paquetes al mismo puerto en poco tiempo puede ser una señal de escaneo\n"
        "de puertos — así es como se detecta, no por magia."
    )
    pausar_al_salir()


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("\nInterrumpido por el usuario.")
        pausar_al_salir()
