# -*- coding: utf-8 -*-
r"""Le pacotes do jogo a partir de uma captura de REDE (.pcapng), sem tocar no processo.

    python tools/analise/ler_pcap.py captura.pcapng            resumo
    python tools/analise/ler_pcap.py captura.pcapng --porta 10012 --op 0401

Mesma interface do `ler_hook`, para as analises que ja existem servirem tal e qual:

    import ler_pcap
    for t, op, corpo in ler_pcap.pacotes("captura.pcapng", "RECV", porta=10012):
        ...

PARA QUE SERVE. O nosso injector nao entra em clients protegidos -- Themida, XIGNCODE,
XTrap. Essas proteccoes defendem a MEMORIA do processo; nao conseguem esconder os bytes que
saem pela placa de rede. O `pktmon`, que vem no Windows, grava ao nivel do adaptador:

    pktmon start --capture --pkt-size 0 -f captura.etl
    (jogar)
    pktmon stop
    pktmon etl2pcap captura.etl -o captura.pcapng

FICA MELHOR DO QUE O HOOK NUM PONTO. O `ler_hook` tem de coser as sobras por socket a mao,
porque o hook carimba a chamada ao socket e um pacote grande atravessa varias. Aqui o TCP
traz numeros de sequencia, portanto a remontagem e exacta e ordenada, mesmo com pacotes
fora de ordem ou repetidos.

DIRECCAO. Sem saber quem e o servidor, "RECV" e "SEND" nao existem. Toma-se a PORTA do
servidor como referencia: o que vem dela e RECV (chegou ao jogador), o que vai para ela e
SEND. A porta adivinha-se sozinha se nao for dada -- escolhe-se a mais falada abaixo de
49152, que e onde as portas efemeras comecam.

O QUE ISTO NAO RESOLVE: se o protocolo for cifrado, saem bytes cifrados. Reconhece-se
depressa -- se os opcodes conhecidos nao aparecerem e o comprimento declarado nos primeiros
quatro bytes nunca fizer sentido, e porque ha cifra por cima.
"""
import argparse
import collections
import os
import struct
import sys

BLOCO_SECAO = 0x0A0D0D0A
BLOCO_INTERFACE = 1
BLOCO_PACOTE = 6
BLOCO_PACOTE_SIMPLES = 3

LINK_ETHERNET = 1
LINK_RAW = 101
LINK_IPV4 = 228
LINK_NULO = 0


def _blocos_pcapng(dados):
    """(tipo, corpo, endianness) de cada bloco. Trata os dois sentidos de byte."""
    i, ordem = 0, "<"
    n = len(dados)
    while i + 12 <= n:
        tipo = struct.unpack_from(ordem + "I", dados, i)[0]
        if tipo == BLOCO_SECAO:
            # o campo magico diz a ordem dos bytes desta seccao
            magico = struct.unpack_from("<I", dados, i + 8)[0]
            ordem = "<" if magico == 0x1A2B3C4D else ">"
            tipo = struct.unpack_from(ordem + "I", dados, i)[0]
        tam = struct.unpack_from(ordem + "I", dados, i + 4)[0]
        if tam < 12 or i + tam > n:
            break
        yield tipo, dados[i + 8:i + tam - 4], ordem
        i += tam


def _tira_camada_de_ligacao(pkt, link):
    if link == LINK_ETHERNET:
        if len(pkt) < 14:
            return None
        tipo = struct.unpack_from(">H", pkt, 12)[0]
        desl = 14
        while tipo in (0x8100, 0x88A8) and len(pkt) >= desl + 4:   # VLAN
            tipo = struct.unpack_from(">H", pkt, desl + 2)[0]
            desl += 4
        return pkt[desl:] if tipo == 0x0800 else None
    if link in (LINK_RAW, LINK_IPV4):
        return pkt
    if link == LINK_NULO:
        return pkt[4:] if len(pkt) > 4 else None
    return pkt


def _tcp(pkt):
    """(origem, destino, seq, carga) de um datagrama IPv4/TCP, ou None."""
    if len(pkt) < 20 or (pkt[0] >> 4) != 4:
        return None
    ihl = (pkt[0] & 0x0F) * 4
    if pkt[9] != 6 or len(pkt) < ihl + 20:      # 6 = TCP
        return None
    ip_o = ".".join(str(b) for b in pkt[12:16])
    ip_d = ".".join(str(b) for b in pkt[16:20])
    t = pkt[ihl:]
    porta_o, porta_d = struct.unpack_from(">HH", t, 0)
    seq = struct.unpack_from(">I", t, 4)[0]
    desl = (t[12] >> 4) * 4
    total = struct.unpack_from(">H", pkt, 2)[0]
    fim = total - ihl if 0 < total - ihl <= len(t) else len(t)
    carga = t[desl:fim]
    return (ip_o, porta_o), (ip_d, porta_d), seq, carga


def fluxos(caminho, porta=0):
    """{(origem, destino): bytes remontados}, e a porta do servidor usada."""
    dados = open(caminho, "rb").read()
    links = []
    pedacos = collections.defaultdict(dict)     # chave -> {seq: carga}
    instantes = {}
    for tipo, corpo, ordem in _blocos_pcapng(dados):
        if tipo == BLOCO_INTERFACE:
            links.append(struct.unpack_from(ordem + "H", corpo, 0)[0])
            continue
        if tipo == BLOCO_PACOTE:
            iface = struct.unpack_from(ordem + "I", corpo, 0)[0]
            alto, baixo = struct.unpack_from(ordem + "II", corpo, 4)
            carimbo = ((alto << 32) | baixo) / 1e6
            cap = struct.unpack_from(ordem + "I", corpo, 12)[0]
            bruto = corpo[20:20 + cap]
        elif tipo == BLOCO_PACOTE_SIMPLES:
            iface, carimbo = 0, 0.0
            bruto = corpo[4:]
        else:
            continue
        link = links[iface] if iface < len(links) else LINK_ETHERNET
        ip = _tira_camada_de_ligacao(bruto, link)
        if not ip:
            continue
        r = _tcp(ip)
        if not r:
            continue
        orig, dest, seq, carga = r
        if not carga:
            continue
        chave = (orig, dest)
        # DEDUPLICA POR SEQUENCIA. Retransmissoes trazem os mesmos bytes outra vez; sem isto
        # o fluxo remontado ficava com pedacos repetidos e o separador desalinhava.
        pedacos[chave].setdefault(seq, carga)
        instantes.setdefault(chave, carimbo)

    if not porta:
        # A PORTA ESCOLHE-SE PELO ENQUADRAMENTO, nao pela quantidade de bytes.
        #
        # A primeira versao pegava na porta mais falada abaixo de 49152 e escolheu a 33389 --
        # o RDP da maquina, que numa captura de rede fala muito mais do que o jogo. Aqui
        # tenta-se separar o fluxo de cada candidata em pacotes e mede-se quanto do fluxo foi
        # consumido por pacotes bem formados. Lixo nao encaixa; o protocolo do jogo encaixa
        # quase a cem por cento.
        candidatas = collections.Counter()
        for (o, d), partes in pedacos.items():
            n = sum(len(x) for x in partes.values())
            for ip_p in (o, d):
                if ip_p[1] < 49152:
                    candidatas[ip_p[1]] += n
        melhor, melhor_nota = 0, 0.0
        for cand, _n in candidatas.most_common(12):
            usados = total = quadros = 0
            for (o, d), partes in pedacos.items():
                if cand not in (o[1], d[1]):
                    continue
                buf = b"".join(partes[s] for s in sorted(partes))
                total += len(buf)
                i = 0
                while i + 4 <= len(buf):
                    tam = int.from_bytes(buf[i:i + 4], "big")
                    # O TECTO E APERTADO DE PROPOSITO. Com 1 MB, dados aleatorios tambem
                    # "encaixam": quatro bytes quaisquer formam um tamanho plausivel e o
                    # separador engole tudo. Com 64 KB isso deixa de acontecer, e foi o que
                    # fez o RDP da maquina (porta 33389) deixar de ganhar a votacao.
                    if 0 < tam <= 65536 and i + 4 + tam <= len(buf):
                        usados += 4 + tam
                        quadros += 1
                        i += 4 + tam
                    else:
                        i += 1
            nota = usados / total if total else 0.0
            if quadros >= 8 and nota > melhor_nota:
                melhor, melhor_nota = cand, nota
        porta = melhor

    saida = {}
    for chave, partes in pedacos.items():
        if porta and porta not in (chave[0][1], chave[1][1]):
            continue
        buf = bytearray()
        for seq in sorted(partes):
            buf += partes[seq]
        saida[chave] = (bytes(buf), instantes.get(chave, 0.0))
    return saida, porta


def pacotes(caminho, direcao="RECV", porta=0):
    """(segundos, opcode_2bytes, corpo_sem_prefixo), como o ler_hook.

    A direccao sai da porta do servidor: o que vem dela chegou ao jogador (RECV).
    """
    fl, porta = fluxos(caminho, porta)
    for (orig, dest), (buf, t0) in sorted(fl.items(), key=lambda x: x[1][1]):
        d = "RECV" if orig[1] == porta else "SEND"
        if direcao and d != direcao:
            continue
        i, n = 0, len(buf)
        while i + 4 <= n:
            tam = int.from_bytes(buf[i:i + 4], "big")
            if tam <= 0 or tam > 1 << 20:
                i += 1                      # dessincronizado: anda um byte e tenta de novo
                continue
            if i + 4 + tam > n:
                break
            corpo = buf[i + 4:i + 4 + tam]
            if len(corpo) >= 2:
                yield t0, corpo[:2], corpo
            i += 4 + tam


def main() -> int:
    ap = argparse.ArgumentParser(description="Le pacotes do jogo de um .pcapng.")
    ap.add_argument("captura")
    ap.add_argument("--porta", type=int, default=0, help="porta do servidor (adivinha se 0)")
    ap.add_argument("--op", default="", help="mostrar so este opcode, ex: 0401")
    ap.add_argument("--limite", type=int, default=25)
    args = ap.parse_args()

    if not os.path.exists(args.captura):
        print("nao achei %s" % args.captura)
        return 1
    fl, porta = fluxos(args.captura, args.porta)
    print("porta do servidor: %d" % porta)
    print("fluxos com dados: %d" % len(fl))
    for (o, d), (buf, t) in sorted(fl.items(), key=lambda x: -len(x[1][0]))[:6]:
        print("   %s:%d -> %s:%d   %d bytes" % (o[0], o[1], d[0], d[1], len(buf)))
    print()
    for direcao in ("RECV", "SEND"):
        c = collections.Counter()
        for _t, op, _corpo in pacotes(args.captura, direcao, porta):
            c[op] += 1
        total = sum(c.values())
        print("%s: %d pacotes, %d opcodes distintos" % (direcao, total, len(c)))
        if total:
            print("   %s" % " ".join("%02x%02x:%d" % (o[0], o[1], n)
                                     for o, n in c.most_common(14)))
    if args.op:
        alvo = bytes.fromhex(args.op)
        print()
        print("=== pacotes %s" % args.op)
        n = 0
        for _t, op, corpo in pacotes(args.captura, "", porta):
            if op == alvo:
                print("   (%4d) %s" % (len(corpo),
                                       " ".join("%02x" % b for b in corpo[:40])))
                n += 1
                if n >= args.limite:
                    break
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
