# SPDX-License-Identifier: GPL-2.0-only
# This file is part of Scapy
# See https://scapy.net/ for more information
# Copyright (C) Gabriel Potter

"""
Wireshark extcap API utils
https://www.wireshark.org/docs/wsdg_html_chunked/ChCaptureExtcap.html
"""

import collections
import functools
import pathlib
import re
import subprocess

from scapy.config import conf
from scapy.consts import WINDOWS
from scapy.data import MTU
from scapy.error import warning
from scapy.interfaces import (
    network_name,
    resolve_iface,
    InterfaceProvider,
    NetworkInterface,
)
from scapy.packet import Packet
from scapy.supersocket import SuperSocket
from scapy.utils import PcapReader, _create_fifo, _open_fifo

# Typing
from typing import (
    cast,
    Any,
    Dict,
    List,
    NoReturn,
    Optional,
    Tuple,
    Type,
    Union,
)


def _extcap_call(prog: str,
                 args: List[str],
                 format: Dict[str, List[str]],
                 ) -> Dict[str, List[Tuple[str, ...]]]:
    """
    Function used to call a program using the extcap format,
    then parse the results
    """
    p = subprocess.Popen(
        [prog] + args,
        # On Windows, we must be in the Wireshark/ folder.
        cwd=pathlib.Path(prog).parent.parent,
        stdout=subprocess.PIPE, stderr=subprocess.PIPE,
        text=True
    )
    data, err = p.communicate()
    if p.returncode != 0:
        raise OSError("%s returned with error code %s: %s" % (prog, p.returncode, err))
    res = collections.defaultdict(list)
    for ifa in data.split("\n"):
        ifa = ifa.strip()
        for keyword, values in format.items():
            if not ifa.startswith(keyword):
                continue

            def _match(val: str, ifa: str) -> str:
                m = re.search(r"{%s=([^}]*)}" % val, ifa)
                if m:
                    return m.group(1)
                return ""
            res[keyword].append(
                tuple(
                    [_match(val, ifa) for val in values]
                )
            )
            break
    return cast(Dict[str, List[Tuple[str, ...]]], res)


class _ExtcapNetworkInterface(NetworkInterface):
    """
    Extcap NetworkInterface
    """

    def get_extcap_config(self) -> Dict[str, Tuple[str, ...]]:
        """
        Return a list of available configuration options on an extcap interface
        """
        return _extcap_call(
            self.provider.cmdprog,  # type: ignore
            ["--extcap-interface", self.network_name, "--extcap-config"],
            {
                "arg": ["number", "call", "display", "default", "required"],
                "value": ["arg", "value", "display", "default"],
            },
        )

    def get_extcap_cmd(self, **kwarg: Dict[str, str]) -> List[str]:
        """
        Return the extcap command line options
        """
        cmds = []
        for x in self.get_extcap_config()["arg"]:
            key = x[1].strip("-").replace("-", "_")
            if key in kwarg:
                # Apply argument
                cmds += [x[1], str(kwarg[key])]
            else:
                # Apply default
                if x[4] == "true":  # required
                    raise ValueError(
                        "Missing required argument: '%s' on iface %s." % (
                            key,
                            self.network_name,
                        )
                    )
                elif not x[3] or x[3] == "false":  # no default (or false)
                    continue
                if x[3] == "true":
                    cmds += [x[1]]
                else:
                    cmds += [x[1], x[3]]
        return cmds


class _ExtcapSocket(SuperSocket):
    """
    Read packets at layer 2 using an extcap command
    """

    nonblocking_socket = True

    @staticmethod
    def select(sockets: List[SuperSocket],
               remain: Optional[float] = None) -> List[SuperSocket]:
        return sockets

    def __init__(self, *_: Any, **kwarg: Any) -> None:
        cmdprog = kwarg.pop("cmdprog")
        iface = kwarg.pop("iface", None)
        if iface is None:
            raise NameError("Must select an interface for a extcap socket !")
        iface = resolve_iface(iface)
        if not isinstance(iface, _ExtcapNetworkInterface):
            raise ValueError("Interface should be an _ExtcapNetworkInterface")
        args = iface.get_extcap_cmd(**kwarg)
        iface = network_name(iface)
        self.outs = None  # extcap sockets can't write
        # open fifo
        fifo, fd = _create_fifo()
        args = ["--extcap-interface", iface, "--capture", "--fifo", fifo] + args
        self.proc = subprocess.Popen(
            [cmdprog] + args,
        )
        self.fd = _open_fifo(fd)
        self.reader = PcapReader(self.fd)  # type: ignore
        self.ins = self.reader  # type: ignore

    def recv(self, x: int = MTU, **kwargs: Any) -> Packet:
        return self.reader.recv(x, **kwargs)

    def close(self) -> None:
        self.proc.kill()
        self.proc.wait(timeout=2)
        SuperSocket.close(self)
        self.fd.close()


class _ExtcapInterfaceProvider(InterfaceProvider):
    """
    Interface provider made to hook on a extcap binary
    """

    headers = ("Index", "Name", "Address")
    header_sort = 1

    def __init__(self, *args: Any, **kwargs: Any) -> None:
        self.cmdprog = kwargs.pop("cmdprog")
        super(_ExtcapInterfaceProvider, self).__init__(*args, **kwargs)

    def load(self) -> Dict[str, NetworkInterface]:
        data: Dict[str, NetworkInterface] = {}
        try:
            interfaces = _extcap_call(
                self.cmdprog,
                ["--extcap-interfaces"],
                {"interface": ["value", "display"]},
            )["interface"]
        except OSError as ex:
            warning(
                "extcap %s failed to load: %s",
                self.name,
                str(ex).strip().split("\n")[-1]
            )
            return {}
        for netw_name, name in interfaces:
            _index = re.search(r".*(\d+)", name)
            if _index:
                index = int(_index.group(1)) + 100
            else:
                index = 100
            if_data = {
                "name": name,
                "network_name": netw_name,
                "description": name,
                "index": index,
            }
            data[netw_name] = _ExtcapNetworkInterface(self, if_data)
        return data

    def _l2listen(self, _: Any) -> Type[SuperSocket]:
        return functools.partial(_ExtcapSocket, cmdprog=self.cmdprog)  # type: ignore

    def _l3socket(self, *_: Any) -> NoReturn:
        raise ValueError("Only sniffing is available for an extcap provider !")

    _l2socket = _l3socket  # type: ignore

    def _is_valid(self, dev: NetworkInterface) -> bool:
        return True

    def _format(self,
                dev: NetworkInterface,
                **kwargs: Any
                ) -> Tuple[Union[str, List[str]], ...]:
        """Returns a tuple of the elements used by show()"""
        return (str(dev.index), dev.name, dev.network_name)


def load_extcap() -> None:
    """
    Load extcap folder from wireshark and populate Scapy's providers.

    Additional interfaces should appear in conf.ifaces.
    """
    if WINDOWS:
        pattern = re.compile(r"^[^.]+(?:\.bat|\.exe)?$")
    else:
        pattern = re.compile(r"^[^.]+(?:\.sh)?$")
    for fld in conf.prog.extcap_folders:
        root = pathlib.Path(fld)
        for _cmdprog in root.glob("*"):
            if not _cmdprog.is_file() or not pattern.match(_cmdprog.name):
                continue
            cmdprog = str((root / _cmdprog).absolute())
            # success
            provname = pathlib.Path(cmdprog).name.rsplit(".", 1)[0]

            class _prov(_ExtcapInterfaceProvider):
                name = provname

            conf.ifaces.register_provider(
                functools.partial(_prov, cmdprog=cmdprog)  # type: ignore
            )
