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'


def parse_pcap_file(pcap_file):
    """Process PCAP file and return sorted lists of TCP and UDP packets."""
    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)
            ethernet_header = packet_data[:ETH_HEADER_LENGTH]
            ethernet_fields = struct.unpack('!6s6sH', ethernet_header)
            ethernet_protocol = socket.ntohs(ethernet_fields[2])

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

                timestamp = timestamp_sec + (timestamp_usec / 1_000_000)

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

    return tcp_packets, udp_packets


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

    # Sort packets by timestamp
    packets.sort(key=lambda x: x[0])

    for packet in packets:
        timestamp, source_ip, dest_ip, src_port, dest_port = packet
        if not current_cluster:  # Starting a new cluster
            current_cluster.append(packet)
        else:
            # Check if the current packet is within the time width
            last_packet_timestamp = current_cluster[-1][0]
            if (timestamp - last_packet_timestamp <= time_width):
                current_cluster.append(packet)
            else:
                # Save the current cluster if it meets the threshold
                if len(current_cluster) >= packet_threshold:
                    clusters.append(current_cluster)
                # Start a new cluster
                current_cluster = [packet]

    # Final check for the last cluster
    if len(current_cluster) >= packet_threshold:
        clusters.append(current_cluster)

    return clusters


def find_clusters_by_port(packets, port_width, packet_threshold):
    """Identify clusters for scanning (by port number)."""
    clusters = []
    current_cluster = []

    # Sort packets by destination port
    packets.sort(key=lambda x: x[4])

    for packet in packets:
        timestamp, source_ip, dest_ip, src_port, dest_port = packet
        if not current_cluster:  # Starting a new cluster
            current_cluster.append(packet)
        else:
            # Check if the current packet is within the port width
            last_packet_dest_port = current_cluster[-1][4]
            if (abs(dest_port - last_packet_dest_port) <= port_width):
                current_cluster.append(packet)
            else:
                # Save the current cluster if it meets the threshold
                if len(current_cluster) >= packet_threshold:
                    clusters.append(current_cluster)
                # Start a new cluster
                current_cluster = [packet]

    # Final check for the last cluster
    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(int)

    for cluster in clusters:
        if protocol == 'TCP':
            for packet in cluster:
                source_ip = packet[1]
                dest_ip = packet[2]
                port = packet[4]  # TCP scanning uses the destination port
                results[(source_ip, port)] += 1
        elif protocol == 'UDP':
            for packet in cluster:
                source_ip = packet[1]
                results[source_ip] += 1

    # Output results
    if cluster_type == "time":
        print(f"\nReports of Probing with {protocol}:")
    elif cluster_type == "port":
        print(f"\nReports of Scanning with {protocol}:")

    for key, attempts in results.items():
        if isinstance(key, tuple):
            # This is the (src_ip, port) format, typically for TCP
            src_ip, port = key
            print(f" > {protocol} from {src_ip} to Port {port} - Total Attempts: {attempts}")
        else:
            # This is just the src_ip format, typically for UDP
            src_ip = key
            print(f" > {protocol} from {src_ip} - 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 in a 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 in a scanning (-p): "))

    # 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
    pcap_files = [f for f in os.listdir(PCAP_FOLDER_PATH) if f.endswith('.pcap')]

    # Process each PCAP file in the folder
    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_packets, udp_packets = parse_pcap_file(full_path)

        # Find clusters for probing (by time) and scanning (by port)
        tcp_time_clusters = find_clusters_by_time(tcp_packets, probing_width, min_probes)
        udp_port_clusters = find_clusters_by_port(udp_packets, scanning_width, min_scans)

        # Output results for TCP probing and UDP scanning
        output_results(tcp_time_clusters, 'TCP', "time")
        output_results(udp_port_clusters, 'UDP', "port")

    print("\nAnalysis complete.")


if __name__ == "__main__":
    main()