215 lines
6.2 KiB
Python
215 lines
6.2 KiB
Python
# -*- 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.
|
|
"""
|
|
|
|
import binascii
|
|
import struct
|
|
|
|
from constantly import Values, ValueConstant
|
|
|
|
from zope.interface import implementer
|
|
from twisted.internet import address
|
|
from twisted.python import compat
|
|
|
|
from ._exceptions import (
|
|
convertError, InvalidProxyHeader, InvalidNetworkProtocol,
|
|
MissingAddressData
|
|
)
|
|
from . import _info
|
|
from . import _interfaces
|
|
|
|
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(object):
|
|
"""
|
|
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):
|
|
self.buffer = b''
|
|
|
|
|
|
def feed(self, data):
|
|
"""
|
|
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):
|
|
"""
|
|
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):
|
|
"""
|
|
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(
|
|
('%x' % (int(hexString[b:b+4], 16),)).encode('ascii')
|
|
for b in range(0, 32, 4)
|
|
)
|
|
|
|
|
|
@classmethod
|
|
def parse(cls, line):
|
|
"""
|
|
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 = 'TCP'
|
|
if netproto is NetProtocol.DGRAM:
|
|
addrType = 'UDP'
|
|
addrCls = address.IPv4Address
|
|
addrParser = 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), sPort),
|
|
addrCls(addrType, addrParser(dest), dPort),
|
|
)
|