import struct
import socket
import os
from collections import defaultdict

# Ethernet header length
ETH_HEADER_LENGTH = 14

# PCAP Packet Header format
PCAP_PACKET_HEADER_FORMAT = '@IIII'

# Hardcoded folder path for PCAP files
PCAP_FOLDER_PATH = "C:\\Users\\tj471\\PycharmProjects\\Lab_Ass_4\\Lab3-pcap-2"  # Update this path to your PCAP files location


def parse_pcap_file(pcap_file, target_ip):
    """Process PCAP file and return lists of TCP and UDP packets directed to the target IP."""
    tcp_packets = []
    udp_packets = []

    with open(pcap_file, 'rb') as file:
        # Skip global header
        file.read(24)

        while True:
            packet_header = file.read(16)
            if len(packet_header) < 16:
                break  # End of file

            timestamp_sec, timestamp_usec, included_len, _ = struct.unpack(PCAP_PACKET_HEADER_FORMAT, packet_header)
            packet_data = file.read(included_len)
            if len(packet_data) < included_len:
                break  # Handle cases where the packet may be truncated
            ethernet_protocol = struct.unpack('!H', packet_data[12:14])[0]

            if ethernet_protocol == 0x0800:  # IPv4
                ip_header = packet_data[ETH_HEADER_LENGTH:20 + ETH_HEADER_LENGTH]
                ip_fields = struct.unpack('!BBHHHBBH4s4s', ip_header)
                protocol_type = ip_fields[6]
                dest_ip = socket.inet_ntoa(ip_fields[9])

                if dest_ip == target_ip:
                    source_ip = socket.inet_ntoa(ip_fields[8])
                    timestamp = timestamp_sec + (timestamp_usec / 1_000_000)

                    if protocol_type == 6:  # TCP
                        src_port, dest_port = struct.unpack('!HH', packet_data[ETH_HEADER_LENGTH + 20:ETH_HEADER_LENGTH + 24])
                        tcp_packets.append((timestamp, source_ip, dest_ip, src_port, dest_port))
                    elif protocol_type == 17:  # UDP
                        src_port, dest_port = struct.unpack('!HH', packet_data[ETH_HEADER_LENGTH + 20:ETH_HEADER_LENGTH + 24])
                        udp_packets.append((timestamp, source_ip, dest_ip, src_port, dest_port))

    # Separate sorting
    tcp_packets_time_sorted = sorted(tcp_packets, key=lambda x: x[0])   # Sort by timestamp
    tcp_packets_port_sorted = sorted(tcp_packets, key=lambda x: x[4])   # Sort by dest port
    udp_packets_time_sorted = sorted(udp_packets, key=lambda x: x[0])   # Sort by timestamp
    udp_packets_port_sorted = sorted(udp_packets, key=lambda x: x[4])   # Sort by dest port

    return tcp_packets_time_sorted, tcp_packets_port_sorted, udp_packets_time_sorted, udp_packets_port_sorted


def find_time_clusters(packets, width, packet_threshold):
    """Identify clusters for probing (by time)."""
    clusters = []
    current_cluster = []

    for packet in packets:
        timestamp = packet[0]
        if not current_cluster:
            current_cluster.append(packet)
        else:
            last_timestamp = current_cluster[-1][0]
            if (timestamp - last_timestamp <= width):
                current_cluster.append(packet)
            else:
                if len(current_cluster) >= packet_threshold:
                    clusters.append(current_cluster)
                current_cluster = [packet]

    if len(current_cluster) >= packet_threshold:
        clusters.append(current_cluster)

    return clusters


def find_port_clusters(packets, width, packet_threshold):
    """Identify clusters for scanning (by port)."""
    clusters = []
    current_cluster = []

    for packet in packets:
        dest_port = packet[4]
        if not current_cluster:
            current_cluster.append(packet)
        else:
            last_port = current_cluster[-1][4]
            if (abs(dest_port - last_port) <= width):
                current_cluster.append(packet)
            else:
                if len(current_cluster) >= packet_threshold:
                    clusters.append(current_cluster)
                current_cluster = [packet]

    if len(current_cluster) >= packet_threshold:
        clusters.append(current_cluster)

    return clusters


def output_results(clusters, protocol, cluster_type):
    """Output the results of the analysis."""
    results = defaultdict(lambda: defaultdict(int))

    for cluster in clusters:
        for packet in cluster:
            source_ip = packet[1]
            dest_port = packet[4]
            results[source_ip][dest_port] += 1

    # Print formatted results
    print(f"\nReports of {cluster_type.capitalize()} with {protocol}:")

    if not results:
        print("No Reports Found")  # No clusters found
    else:
        for src_ip, ports in results.items():
            for port, attempts in ports.items():
                print(f" > {protocol} from {src_ip} to Port {port} - Total Attempts: {attempts}")


def main():
    # Gather user inputs
    target_ip = input("Enter the target IP address (-t): ")
    probing_width = int(input("Enter the width for probing in seconds (-l): "))
    min_probes = int(input("Enter the minimum number of packets for probing (-m): "))
    scanning_width = int(input("Enter the width for scanning in port numbers (-n): "))
    min_scans = int(input("Enter the minimum number of packets for scanning (-p): "))

    # Process each PCAP file in the designated folder
    pcap_files = [f for f in os.listdir(PCAP_FOLDER_PATH) if f.endswith('.pcap')]

    for pcap_file in pcap_files:
        full_path = os.path.join(PCAP_FOLDER_PATH, pcap_file)
        print(f"Processing {full_path}...")

        # Parse the pcap file
        tcp_time_sorted, tcp_port_sorted, udp_time_sorted, udp_port_sorted = parse_pcap_file(full_path, target_ip)

        # Find TCP and UDP probes (by time)
        tcp_probes = find_time_clusters(tcp_time_sorted, probing_width, min_probes)
        udp_probes = find_time_clusters(udp_time_sorted, probing_width, min_probes)

        # Find TCP and UDP scans (by port)
        tcp_scans = find_port_clusters(tcp_port_sorted, scanning_width, min_scans)
        udp_scans = find_port_clusters(udp_port_sorted, scanning_width, min_scans)

        # Output in the desired order: TCP Probing, UDP Probing, TCP Scanning, UDP Scanning
        output_results(tcp_probes, 'TCP', "probing")
        output_results(udp_probes, 'UDP', "probing")
        output_results(tcp_scans, 'TCP', "scanning")
        output_results(udp_scans, 'UDP', "scanning")

        print("\nAnalysis complete.")


if __name__ == "__main__":
    main()