# -*- test-case-name: twisted.protocols.haproxy.test.test_v2parser -*-

# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.

"""
IProxyParser implementation for version two of the PROXY protocol.
"""
from __future__ import annotations

import binascii
import struct
from typing import Callable, Literal

from zope.interface import implementer

from constantly import ValueConstant, Values

from twisted.internet import address
from twisted.python import compat
from . import _info, _interfaces
from ._exceptions import (
    InvalidNetworkProtocol,
    InvalidProxyHeader,
    MissingAddressData,
    convertError,
)


class NetFamily(Values):
    """
    Values for the 'family' field.
    """

    UNSPEC = ValueConstant(0x00)
    INET = ValueConstant(0x10)
    INET6 = ValueConstant(0x20)
    UNIX = ValueConstant(0x30)


class NetProtocol(Values):
    """
    Values for 'protocol' field.
    """

    UNSPEC = ValueConstant(0)
    STREAM = ValueConstant(1)
    DGRAM = ValueConstant(2)


_HIGH = 0b11110000
_LOW = 0b00001111
_LOCALCOMMAND = "LOCAL"
_PROXYCOMMAND = "PROXY"


@implementer(_interfaces.IProxyParser)
class V2Parser:
    """
    PROXY protocol version two header parser.

    Version two of the PROXY protocol is a binary format.
    """

    PREFIX = b"\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A"
    VERSIONS = [32]
    COMMANDS = {0: _LOCALCOMMAND, 1: _PROXYCOMMAND}
    ADDRESSFORMATS = {
        # TCP4
        17: "!4s4s2H",
        18: "!4s4s2H",
        # TCP6
        33: "!16s16s2H",
        34: "!16s16s2H",
        # UNIX
        49: "!108s108s",
        50: "!108s108s",
    }

    def __init__(self) -> None:
        self.buffer = b""

    def feed(self, data: bytes) -> tuple[_info.ProxyInfo, bytes] | tuple[None, None]:
        """
        Consume a chunk of data and attempt to parse it.

        @param data: A bytestring.
        @type data: bytes

        @return: A two-tuple containing, in order, a L{_interfaces.IProxyInfo}
            and any bytes fed to the parser that followed the end of the
            header.  Both of these values are None until a complete header is
            parsed.

        @raises InvalidProxyHeader: If the bytes fed to the parser create an
            invalid PROXY header.
        """
        self.buffer += data
        if len(self.buffer) < 16:
            raise InvalidProxyHeader()

        size = struct.unpack("!H", self.buffer[14:16])[0] + 16
        if len(self.buffer) < size:
            return (None, None)

        header, remaining = self.buffer[:size], self.buffer[size:]
        self.buffer = b""
        info = self.parse(header)
        return (info, remaining)

    @staticmethod
    def _bytesToIPv4(bytestring: bytes) -> bytes:
        """
        Convert packed 32-bit IPv4 address bytes into a dotted-quad ASCII bytes
        representation of that address.

        @param bytestring: 4 octets representing an IPv4 address.
        @type bytestring: L{bytes}

        @return: a dotted-quad notation IPv4 address.
        @rtype: L{bytes}
        """
        return b".".join(
            ("%i" % (ord(b),)).encode("ascii") for b in compat.iterbytes(bytestring)
        )

    @staticmethod
    def _bytesToIPv6(bytestring: bytes) -> bytes:
        """
        Convert packed 128-bit IPv6 address bytes into a colon-separated ASCII
        bytes representation of that address.

        @param bytestring: 16 octets representing an IPv6 address.
        @type bytestring: L{bytes}

        @return: a dotted-quad notation IPv6 address.
        @rtype: L{bytes}
        """
        hexString = binascii.b2a_hex(bytestring)
        return b":".join(
            (f"{int(hexString[b : b + 4], 16):x}").encode("ascii")
            for b in range(0, 32, 4)
        )

    @classmethod
    def parse(cls, line: bytes) -> _info.ProxyInfo:
        """
        Parse a bytestring as a full PROXY protocol header.

        @param line: A bytestring that represents a valid HAProxy PROXY
            protocol version 2 header.
        @type line: bytes

        @return: A L{_interfaces.IProxyInfo} containing the
            parsed data.

        @raises InvalidProxyHeader: If the bytestring does not represent a
            valid PROXY header.
        """
        prefix = line[:12]
        addrInfo = None
        with convertError(IndexError, InvalidProxyHeader):
            # Use single value slices to ensure bytestring values are returned
            # instead of int in PY3.
            versionCommand = ord(line[12:13])
            familyProto = ord(line[13:14])

        if prefix != cls.PREFIX:
            raise InvalidProxyHeader()

        version, command = versionCommand & _HIGH, versionCommand & _LOW
        if version not in cls.VERSIONS or command not in cls.COMMANDS:
            raise InvalidProxyHeader()

        if cls.COMMANDS[command] == _LOCALCOMMAND:
            return _info.ProxyInfo(line, None, None)

        family, netproto = familyProto & _HIGH, familyProto & _LOW
        with convertError(ValueError, InvalidNetworkProtocol):
            family = NetFamily.lookupByValue(family)
            netproto = NetProtocol.lookupByValue(netproto)
        if family is NetFamily.UNSPEC or netproto is NetProtocol.UNSPEC:
            return _info.ProxyInfo(line, None, None)

        addressFormat = cls.ADDRESSFORMATS[familyProto]
        addrInfo = line[16 : 16 + struct.calcsize(addressFormat)]
        if family is NetFamily.UNIX:
            with convertError(struct.error, MissingAddressData):
                source, dest = struct.unpack(addressFormat, addrInfo)
            return _info.ProxyInfo(
                line,
                address.UNIXAddress(source.rstrip(b"\x00")),
                address.UNIXAddress(dest.rstrip(b"\x00")),
            )

        addrType: Literal["TCP"] | Literal["UDP"] = "TCP"
        if netproto is NetProtocol.DGRAM:
            addrType = "UDP"
        addrCls: (
            type[address.IPv4Address] | type[address.IPv6Address]
        ) = address.IPv4Address
        addrParser: Callable[[bytes], bytes] = cls._bytesToIPv4
        if family is NetFamily.INET6:
            addrCls = address.IPv6Address
            addrParser = cls._bytesToIPv6

        with convertError(struct.error, MissingAddressData):
            info = struct.unpack(addressFormat, addrInfo)
            source, dest, sPort, dPort = info

        return _info.ProxyInfo(
            line,
            addrCls(addrType, addrParser(source).decode(), sPort),
            addrCls(addrType, addrParser(dest).decode(), dPort),
        )
