class Ip6PacketParser(PacketParserInterface):
    def __init__(self, config: ConfigurationData):
        self.config = config
        self.ip_utils = IpAddrUtils()

    def extract_data(self, packet: IP6) -> Munch:
        data = Munch()
        try:
            data.src_ip, data.dst_ip = self.extract_src_dest_ip(packet)
            data.ip_proto = packet.p
            data.ip_payload_size = len(packet.data)
            if packet.all_extension_headers:
                data.ip6_nxt_hdr = self.config.FieldDelimiter.join([
                    str(header.nxt) for header in packet.all_extension_headers
                ])

        except BaseException as ex:
            logging.warning('Unable to extract IP6 from `%s`. Error: `%s`',
                            type(packet), ex)
            raise ex

        return data

    def extract_src_dest_ip(self, ip_packet: IP) -> Tuple:
        src_ip = self.ip_utils.inet_to_str(ip_packet.src)
        dst_ip = self.ip_utils.inet_to_str(ip_packet.dst)

        if self.config.use_numeric_values is True:
            return self.ip_utils.ip_to_int(src_ip), self.ip_utils.ip_to_int(
                dst_ip)

        return src_ip, dst_ip
class ArpPacketParser(PacketParserInterface):
    def __init__(self, config: ConfigurationData):
        self.config = config
        self.ip_utils = IpAddrUtils()
        self.mac_utils = MacAddressUtils()

    def extract_data(self, packet: ARP) -> Munch:
        data = Munch()
        try:
            data.arp_request_src = packet.op
            data.arp_src_mac = self.mac_utils.convert_hexadecimal_mac_to_readable_mac(
                packet.sha)
            data.arp_src_ip = self.ip_utils.inet_to_str(packet.spa)
            data.arp_dst_mac = self.mac_utils.convert_hexadecimal_mac_to_readable_mac(
                packet.tha)
            data.arp_dst_ip = self.ip_utils.inet_to_str(packet.tpa)

            if self.config.use_numeric_values is True:
                data.arp_src_mac = self.mac_utils.mac_to_int(data.arp_src_mac)
                data.arp_dst_mac = self.mac_utils.mac_to_int(data.arp_dst_mac)
                data.arc_src_ip = self.ip_utils.ip_to_int(data.arp_src_ip)
                data.arc_dst_ip = self.ip_utils.ip_to_int(data.arp_src_ip)

        except BaseException as ex:
            logging.warning('Unable to extract ARP from `%s`. Error: `%s`',
                            type(packet), ex)
            raise ex

        return data
class IgmpPacketParser(PacketParserInterface):
    # Use Scipy: https://github.com/secdev/scapy/blob/master/scapy/contrib/igmp.py
    def __init__(
        self,
        config: ConfigurationData,
    ):
        self.config = config
        self.ip_addr_utils = IpAddrUtils()

    @staticmethod
    def load_igmp_packet_from_ip_packet(ip_packet: IP) -> IGMP:
        try:
            return IGMP(ip_packet.data)

        except BaseException as ex:
            logging.warning(
                'Can not extract IGMP packet data from IP Packet. Error: `%s`',
                ex)
            raise ex

    def extract_data(self, packet: IGMP) -> Munch:
        data = Munch()
        if packet is None:
            return data

        data.igmp_type = packet.type
        data.igmp_addr = self.ip_addr_utils.inet_to_str(packet.group)
        if self.config.use_numeric_values is True:
            data.igmp_addr = self.ip_addr_utils.ip_to_int(data.igmp_addr)

        return data
class NtpPacketParser(PacketParserInterface):
    def __init__(self, config: ConfigurationData):
        self.config = config
        self.ip_utils = IpAddrUtils()

    @staticmethod
    def load_ntp_packet_from_ip_packet(ip_packet: IP) -> Optional[NTP]:
        try:
            udp_packet = UDP(ip_packet.data)
            return NtpPacketParser.load_ntp_packet_from_udp_packet(udp_packet)

        except BaseException as ex:
            logging.warning(
                'Can not extract NTP packet from UDP packet. Error: `%s`', ex)
            raise ex

    @staticmethod
    def load_ntp_packet_from_udp_packet(udp_packet: UDP) -> Optional[NTP]:
        try:
            return NTP(udp_packet.data)

        except dpkt.dpkt.NeedData:
            logging.warning(
                'Not enough data to extract NTP packet from UDP packet')

        except BaseException as ex:
            logging.warning(
                'Can not extract NTP packet from UDP packet. Error: `%s`', ex)
            raise ex

    def extract_data(self, packet: NTP) -> Munch:
        data = Munch()
        try:
            data.ntp_mode = packet.mode
            data.ntp_interval = packet.interval
            data.ntp_stratum = packet.stratum
            data.ntp_reference_id = self.resolve_ntp_reference(packet)

        except BaseException as ex:
            logging.warning('Unable to extract NTP from `%s`. Error: `%s`',
                            type(packet), ex)
            raise ex

        return data

    def resolve_ntp_reference(self, packet: NTP) -> Union[int, str]:
        reference_id = self.ip_utils.inet_to_str(packet.id)
        if reference_id is None:
            # Could not parse NTP REFID, probably it is a string
            return packet.id

        if reference_id == '0.0.0.0':
            # REFID is NULL but dpkt considers it as b'\x00\x00\x00\x00'
            return ''

        if self.config.use_numeric_values is True:
            return self.ip_utils.ip_to_int(reference_id)

        return reference_id
class IpPacketParser(PacketParserInterface):
    def __init__(self,
                 config: ConfigurationData,
                 static_data: StaticData = None):
        self.config = config
        self.ip_utils = IpAddrUtils()
        self.static_data = static_data or StaticData()

    @staticmethod
    def load_ip_packet_from_ethernet_frame(
            packet_data: bytes) -> Union[IP, IP6]:
        if isinstance(packet_data, (IP, IP6)):
            return packet_data

        # Packet data is bytes because it is a fragmented packet.
        try:
            return IP(packet_data)

        except dpkt.dpkt.UnpackError:
            return IP6(
                packet_data)  # When IPv6 packet is encapsulated in IPv4 packet

        except BaseException as ex:
            logging.error(
                'Can not parse Ethernet frame as IPv4 or IPv6 packet. Error: `%s`',
                ex)
            raise ex

    def extract_data(self, packet: IP) -> Munch:
        data = Munch()
        try:
            data.src_ip, data.dst_ip = self.extract_src_dest_ip(packet)
            data.ip_proto = self.get_ip_proto_name(packet.p)
            data.ip_payload_size = len(packet.data)
            data.ip_ttl = packet.ttl
            data.ip_tos = packet.tos
            data.ip_opts = self.parse_ip_options(packet.opts) or ''
            data.ip_do_not_fragment = bool(packet.off & dpkt.ip.IP_DF)
            data.ip_more_fragment = bool(packet.off & dpkt.ip.IP_MF)

            if self.config.use_numeric_values:
                data.ip_do_not_fragment = 1 if data.ip_do_not_fragment is True else 0
                data.ip_more_fragment = 1 if data.ip_more_fragment is True else 0

        except BaseException as ex:
            logging.warning('Unable to extract IP4 from `%s`. Error: `%s`',
                            type(packet), ex)
            raise ex

        return data

    # pylint: disable=duplicate-code
    def extract_src_dest_ip(self, ip_packet: IP) -> Tuple:
        src_ip = self.ip_utils.inet_to_str(ip_packet.src)
        dst_ip = self.ip_utils.inet_to_str(ip_packet.dst)

        if self.config.use_numeric_values is True:
            return self.ip_utils.ip_to_int(src_ip), self.ip_utils.ip_to_int(
                dst_ip)

        return src_ip, dst_ip

    def parse_ip_options(self, ip_options: bytes) -> Union[int, str]:
        # Split 4 bytes \x94\x04\x00\x00 to single bytes [94, 04, 00, 00]
        options = binascii.hexlify(ip_options).decode('utf-8')
        if not options:
            return ''

        options = [options[i:i + 2] for i in range(0, len(options), 2)]
        # first byte gives information of IP options
        if self.config.use_numeric_values is True:
            return hex_to_integer(options[0])

        hex_option = '0x' + options[0]

        return self.static_data.ip_options_data.get(
            hex_option, {}).get('abbrv') or hex_option

    def get_ip_proto_name(self, proto_num: int) -> Union[int, str]:
        if self.config.use_numeric_values is True:
            return proto_num

        try:
            proto_key = str(proto_num)
        except ValueError:
            return proto_num

        proto_name = ''
        if proto_key in self.static_data.ip_protocol_data:
            proto_name = self.static_data.ip_protocol_data.get(
                proto_key, {}).get('keyword', '')

        return proto_name or proto_key