add: initial evs support

This commit is contained in:
Istvan Ruzman
2022-02-21 16:36:52 +01:00
parent 10d7f0b7f4
commit d970df0dd7
4 changed files with 152 additions and 99 deletions

View File

@@ -7,6 +7,7 @@ Classes and Types to parse and represent a RADIUS dictionary.
"""
import logging
from contextlib import suppress
from os.path import dirname, isabs, join, normpath
from typing import IO, Dict, Generator, List, Optional, Sequence, Tuple, Union
@@ -16,11 +17,11 @@ LOG = logging.getLogger(__name__)
INTEGER_TYPES: Dict[str, Tuple[int, int]] = {
"byte": (0, 255),
"short": (0, 2 ** 16 - 1),
"signed": (-(2 ** 31), 2 ** 31 - 1),
"integer": (0, 2 ** 32 - 1),
"integer64": (0, 2 ** 64 - 1),
"BYTE": (0, 255),
"SHORT": (0, 2 ** 16 - 1),
"SIGNED": (-(2 ** 31), 2 ** 31 - 1),
"INTEGER": (0, 2 ** 32 - 1),
"INTEGER64": (0, 2 ** 64 - 1),
}
@@ -111,6 +112,7 @@ class Dictionary:
self.attrindex: Dict[Union[str, int, Tuple[int, ...]], Attribute] = {}
self.rfc_vendor = Vendor("RFC", 0, 1, 1, False, {})
self.cur_vendor = self.rfc_vendor
self.evs: Dict[str, List[int]] = {}
if __dictio is not None:
loader = dict_parser(dictionary, __dictio)
else:
@@ -120,11 +122,12 @@ class Dictionary:
def read_dictionary(self, reader: Generator[Tuple[int, List[str]], None, None]):
"""Read and parse a (Free)RADIUS dictionary."""
self.filestack: List[str] = []
cur_evs: List[int] = []
for line_num, tokens in reader:
key = tokens[0]
if key == "ATTRIBUTE":
# logging is done within the method
self._parse_attribute(tokens, line_num)
self._parse_attribute(tokens, line_num, cur_evs)
elif key == "VALUE":
# logging is done within the method
self._parse_value(tokens, line_num)
@@ -140,10 +143,11 @@ class Dictionary:
self._parse_vendor(tokens, line_num)
LOG.info("Register vendor %s", tokens[1])
elif key == "BEGIN-VENDOR":
self._parse_begin_vendor(tokens, line_num)
cur_evs = self._parse_begin_vendor(tokens, line_num)
LOG.info("Open Vendor section %s", tokens[1])
elif key == "END-VENDOR":
self._parse_end_vendor(tokens, line_num)
cur_evs = []
LOG.info("Close Vendor section %s", tokens[1])
elif key == "BEGIN-TLV":
raise NotImplementedError(
@@ -164,73 +168,93 @@ class Dictionary:
vendor_name = tokens[1]
vendor_id = int(tokens[2], 0)
continuation = False
t_len, l_len = 1, 1
# Parse optional vendor specification
try:
with suppress(IndexError):
vendor_format = tokens[3].split("=")
if vendor_format[0] != "format":
if vendor_format[0] == "format":
try:
vendor_format = vendor_format[1].split(",")
t_len, l_len = (int(a) for a in vendor_format[:2])
if t_len not in [1, 2, 4]:
raise ParseError(
filename,
f'Invalid type length definition "{t_len}" for vendor {vendor_name}',
line_num,
)
if l_len not in [0, 1, 2]:
raise ParseError(
filename,
f'Invalid length definition "{l_len}" for vendor {vendor_name}',
line_num,
)
with suppress(IndexError):
if vendor_format[2] == "c":
if not vendor_name.upper() == "WIMAX":
# Not sure why, but FreeRADIUS has this limit,
# so we just do the same cause they know better than me
raise ParseError(
filename,
"continuation-bit is only supported for WiMAX",
line_num,
)
continuation = True
except IndexError as exc:
raise ParseError(
filename,
f"Invalid format definition for vendor {vendor_name}",
line_num,
) from exc
except ValueError as exc:
raise ParseError(
filename,
f"Syntax error in specification for vendor {vendor_name}",
line_num,
) from exc
else:
raise ParseError(
filename,
f"Unknown option {vendor_format[0]} for vendor definition",
line_num,
)
try:
vendor_format = vendor_format[1].split(",")
t_len, l_len = (int(a) for a in vendor_format[:2])
if t_len not in [1, 2, 4]:
raise ParseError(
filename,
f'Invalid type length definition "{t_len}" for vendor {vendor_name}',
line_num,
)
if l_len not in [0, 1, 2]:
raise ParseError(
filename,
f'Invalid length definition "{l_len}" for vendor {vendor_name}',
line_num,
)
try:
if vendor_format[2] == "c":
if not vendor_name.upper() == "WIMAX":
# Not sure why, but FreeRADIUS has this limit,
# so we just do the same cause they know better than me
raise ParseError(
filename,
"continuation-bit is only supported for WiMAX",
line_num,
)
continuation = True
except IndexError:
pass
except ValueError as exc:
raise ParseError(
filename,
f"Syntax error in specification for vendor {vendor_name}",
line_num,
) from exc
except IndexError:
# no format definition
t_len, l_len = 1, 1
vendor = Vendor(vendor_name, vendor_id, t_len, l_len, continuation, {})
self.vendor_lookup_id_by_name[vendor_name] = vendor_id
self.vendor[vendor_id] = vendor
def _parse_begin_vendor(self, tokens: Sequence[str], line_num: int):
def _parse_begin_vendor(self, tokens: Sequence[str], line_num: int) -> List[int]:
"""Parse the BEGIN-VENDOR line of (Free)RADIUS dictionaries."""
filename = self.filestack[-1]
evs = []
if self.cur_vendor != self.rfc_vendor:
raise ParseError(
filename,
"vendor-begin sections are not allowed to be nested",
line_num,
)
if len(tokens) != 2:
if len(tokens) not in [2, 3]:
raise ParseError(
filename,
"Incorrect number of tokens for begin-vendor statement",
line_num,
)
with suppress(IndexError):
vendor_format = tokens[2].split("=")
if vendor_format[0] != "format":
raise ParseError(
filename, f"Invalid token {tokens[3]} for vendor begin", line_num
)
if vendor_format[1].upper() in self.evs:
try:
evs = self.evs[vendor_format[1].upper()]
except KeyError as exc:
raise ParseError(
filename, f"Unknown EVS {vendor_format[1]}", line_num
) from exc
try:
vendor_id = self.vendor_lookup_id_by_name[tokens[1]]
self.cur_vendor = self.vendor[vendor_id]
@@ -240,6 +264,7 @@ class Dictionary:
f"Unknown vendor {tokens[1]} in begin-vendor statement",
line_num,
) from exc
return evs
def _parse_end_vendor(self, tokens: Sequence[str], line_num: int):
"""Parse the END-VENDOR line of (Free)RADIUS dictionaries."""
@@ -299,10 +324,12 @@ class Dictionary:
return has_tag, encrypt
def _parse_attribute_code(self, attr_code: str, line_num: int) -> List[int]:
def _parse_attribute_code(
self, attr_code: str, line_num: int, evs: List[int]
) -> List[int]:
filename = self.filestack[-1]
tlength = self.cur_vendor.tlength
codes = []
codes = evs.copy()
for code in attr_code.split("."):
try:
code_num = _parse_number(code)
@@ -325,7 +352,7 @@ class Dictionary:
codes.append(code_num)
return codes
def _parse_attribute(self, tokens: Sequence[str], line_num: int):
def _parse_attribute(self, tokens: Sequence[str], line_num: int, evs: List[int]):
"""Parse an ATTRIBUTE line of (Free)RADIUS dictionaries."""
filename = self.filestack[-1]
if not len(tokens) in [4, 5]:
@@ -348,7 +375,7 @@ class Dictionary:
line_num,
)
codes = self._parse_attribute_code(attr_code, line_num)
codes = self._parse_attribute_code(attr_code, line_num, evs)
# TODO: Do we some explicit handling of tlvs?
# if len(codes) > 1:
@@ -358,13 +385,13 @@ class Dictionary:
base_datatype = datatype.split("[")[0].replace("-", "")
try:
attribute_type = Datatype[base_datatype]
attribute_type = Datatype[base_datatype.upper()]
except KeyError as exc:
raise ParseError(filename, f"Illegal type: {datatype}", line_num) from exc
attribute = Attribute(
name,
codes[-1],
codes, # [-1],
attribute_type,
{},
has_tag,
@@ -377,6 +404,11 @@ class Dictionary:
)
self.cur_vendor.attrs[attrcode] = attribute
if datatype == "evs":
if not isinstance(attrcode, tuple):
raise ParseError(filename, "evs must be a tlv", line_num)
self.evs[name.upper()] = list(attrcode)
if self.cur_vendor != self.rfc_vendor:
codes = [26, self.cur_vendor.code] + codes
LOG.info(
@@ -433,8 +465,7 @@ class Dictionary:
except KeyError as exc:
raise ParseError(
filename,
f"only attributes with integer typed datatypes can have"
f"value definitions {attribute.datatype}",
f"only attributes with integer typed datatypes can have value definitions {attribute.datatype}",
line_num,
) from exc
attribute.values[value] = key

View File

@@ -10,7 +10,7 @@ we don't support them (yet).
from dataclasses import dataclass
from enum import Enum, IntEnum, auto
from typing import Dict, Tuple, Union
from typing import Dict, List, Tuple, Union
class Code(IntEnum):
@@ -78,7 +78,7 @@ class Attribute: # pylint: disable=too-many-instance-attributes
"""RADIUS Attribute definition"""
name: str
code: int
code: Union[int, List[int]]
datatype: Datatype
values: Dict[Union[int, str], Union[int, str]]
has_tag: bool = False

View File

@@ -40,44 +40,6 @@ ATTRIBUTE RFC-SPACE-TAGGED-COMBOIP 114 comboip has_tag
ATTRIBUTE RFC-SPACE-TAGGED-IFID 115 ifid has_tag
ATTRIBUTE RFC-SPACE-TAGGED-ETHER 116 ether has_tag
# TODO How are EVS meant to be specified?
# ATTRIBUTE VENDOR10-EVS-TYPE-STRING 19.21.1234.1 string
# ATTRIBUTE VENDOR10-EVS-TYPE-OCTETS 19.21.1234.2 octets
# ATTRIBUTE VENDOR10-EVS-TYPE-DATE 19.21.1234.3 date
# ATTRIBUTE VENDOR10-EVS-TYPE-ABINARY 19.21.1234.4 abinary
# ATTRIBUTE VENDOR10-EVS-TYPE-BYTE 19.21.1234.5 byte
# ATTRIBUTE VENDOR10-EVS-TYPE-SHORT 19.21.1234.6 short
# ATTRIBUTE VENDOR10-EVS-TYPE-INTEGER 19.21.1234.7 integer
# ATTRIBUTE VENDOR10-EVS-TYPE-SIGNED 19.21.1234.8 signed
# ATTRIBUTE VENDOR10-EVS-TYPE-INTEGER64 19.21.1234.9 integer64
# ATTRIBUTE VENDOR10-EVS-TYPE-IPADDR 19.21.1234.10 ipaddr
# ATTRIBUTE VENDOR10-EVS-TYPE-IPV4PREFIX 19.21.1234.11 ipv4prefix
# ATTRIBUTE VENDOR10-EVS-TYPE-IPV6ADDR 19.21.1234.12 ipv6addr
# ATTRIBUTE VENDOR10-EVS-TYPE-IPV6PREFIX 19.21.1234.13 ipv6prefix
# ATTRIBUTE VENDOR10-EVS-TYPE-COMBOIP 19.21.1234.14 comboip
# ATTRIBUTE VENDOR10-EVS-TYPE-IFID 19.21.1234.15 ifid
# ATTRIBUTE VENDOR10-EVS-TYPE-ETHER 19.21.1234.16 ether
# ATTRIBUTE VENDOR10-EVS-TYPE-TLV 19.21.1234.18 tlv
#
# ATTRIBUTE VENDOR10-LONG-EVS-TYPE-STRING 20.21.1234.1 string
# ATTRIBUTE VENDOR10-LONG-EVS-TYPE-OCTETS 20.21.1234.2 octets
# ATTRIBUTE VENDOR10-LONG-EVS-TYPE-DATE 20.21.1234.3 date
# ATTRIBUTE VENDOR10-LONG-EVS-TYPE-ABINARY 20.21.1234.4 abinary
# ATTRIBUTE VENDOR10-LONG-EVS-TYPE-BYTE 20.21.1234.5 byte
# ATTRIBUTE VENDOR10-LONG-EVS-TYPE-SHORT 20.21.1234.6 short
# ATTRIBUTE VENDOR10-LONG-EVS-TYPE-INTEGER 20.21.1234.7 integer
# ATTRIBUTE VENDOR10-LONG-EVS-TYPE-SIGNED 20.21.1234.8 signed
# ATTRIBUTE VENDOR10-LONG-EVS-TYPE-INTEGER64 20.21.1234.9 integer64
# ATTRIBUTE VENDOR10-LONG-EVS-TYPE-IPADDR 20.21.1234.10 ipaddr
# ATTRIBUTE VENDOR10-LONG-EVS-TYPE-IPV4PREFIX 20.21.1234.11 ipv4prefix
# ATTRIBUTE VENDOR10-LONG-EVS-TYPE-IPV6ADDR 20.21.1234.12 ipv6addr
# ATTRIBUTE VENDOR10-LONG-EVS-TYPE-IPV6PREFIX 20.21.1234.13 ipv6prefix
# ATTRIBUTE VENDOR10-LONG-EVS-TYPE-COMBOIP 20.21.1234.14 comboip
# ATTRIBUTE VENDOR10-LONG-EVS-TYPE-IFID 20.21.1234.15 ifid
# ATTRIBUTE VENDOR10-LONG-EVS-TYPE-ETHER 20.21.1234.16 ether
# ATTRIBUTE VENDOR10-LONG-EVS-TYPE-TLV 20.21.1234.18 tlv
VENDOR TEST10 1234 format=1,0
BEGIN-VENDOR TEST10
@@ -118,6 +80,47 @@ ATTRIBUTE VENDOR10-TAGGED-ETHER 116 ether has_tag
END-VENDOR TEST10
BEGIN-VENDOR TEST10 format=RFC-SPACE-TYPE-EVS
ATTRIBUTE VENDOR10-EVS-TYPE-STRING 19.21.1234.1 string
ATTRIBUTE VENDOR10-EVS-TYPE-OCTETS 19.21.1234.2 octets
ATTRIBUTE VENDOR10-EVS-TYPE-DATE 19.21.1234.3 date
ATTRIBUTE VENDOR10-EVS-TYPE-ABINARY 19.21.1234.4 abinary
ATTRIBUTE VENDOR10-EVS-TYPE-BYTE 19.21.1234.5 byte
ATTRIBUTE VENDOR10-EVS-TYPE-SHORT 19.21.1234.6 short
ATTRIBUTE VENDOR10-EVS-TYPE-INTEGER 19.21.1234.7 integer
ATTRIBUTE VENDOR10-EVS-TYPE-SIGNED 19.21.1234.8 signed
ATTRIBUTE VENDOR10-EVS-TYPE-INTEGER64 19.21.1234.9 integer64
ATTRIBUTE VENDOR10-EVS-TYPE-IPADDR 19.21.1234.10 ipaddr
ATTRIBUTE VENDOR10-EVS-TYPE-IPV4PREFIX 19.21.1234.11 ipv4prefix
ATTRIBUTE VENDOR10-EVS-TYPE-IPV6ADDR 19.21.1234.12 ipv6addr
ATTRIBUTE VENDOR10-EVS-TYPE-IPV6PREFIX 19.21.1234.13 ipv6prefix
ATTRIBUTE VENDOR10-EVS-TYPE-COMBOIP 19.21.1234.14 comboip
ATTRIBUTE VENDOR10-EVS-TYPE-IFID 19.21.1234.15 ifid
ATTRIBUTE VENDOR10-EVS-TYPE-ETHER 19.21.1234.16 ether
ATTRIBUTE VENDOR10-EVS-TYPE-TLV 19.21.1234.18 tlv
END-VENDOR
BEGIN-VENDOR TEST10 format=RFC-SPACE-TYPE-EVS
ATTRIBUTE VENDOR10-LONG-EVS-TYPE-STRING 20.21.1234.1 string
ATTRIBUTE VENDOR10-LONG-EVS-TYPE-OCTETS 20.21.1234.2 octets
ATTRIBUTE VENDOR10-LONG-EVS-TYPE-DATE 20.21.1234.3 date
ATTRIBUTE VENDOR10-LONG-EVS-TYPE-ABINARY 20.21.1234.4 abinary
ATTRIBUTE VENDOR10-LONG-EVS-TYPE-BYTE 20.21.1234.5 byte
ATTRIBUTE VENDOR10-LONG-EVS-TYPE-SHORT 20.21.1234.6 short
ATTRIBUTE VENDOR10-LONG-EVS-TYPE-INTEGER 20.21.1234.7 integer
ATTRIBUTE VENDOR10-LONG-EVS-TYPE-SIGNED 20.21.1234.8 signed
ATTRIBUTE VENDOR10-LONG-EVS-TYPE-INTEGER64 20.21.1234.9 integer64
ATTRIBUTE VENDOR10-LONG-EVS-TYPE-IPADDR 20.21.1234.10 ipaddr
ATTRIBUTE VENDOR10-LONG-EVS-TYPE-IPV4PREFIX 20.21.1234.11 ipv4prefix
ATTRIBUTE VENDOR10-LONG-EVS-TYPE-IPV6ADDR 20.21.1234.12 ipv6addr
ATTRIBUTE VENDOR10-LONG-EVS-TYPE-IPV6PREFIX 20.21.1234.13 ipv6prefix
ATTRIBUTE VENDOR10-LONG-EVS-TYPE-COMBOIP 20.21.1234.14 comboip
ATTRIBUTE VENDOR10-LONG-EVS-TYPE-IFID 20.21.1234.15 ifid
ATTRIBUTE VENDOR10-LONG-EVS-TYPE-ETHER 20.21.1234.16 ether
ATTRIBUTE VENDOR10-LONG-EVS-TYPE-TLV 20.21.1234.18 tlv
END-VENDOR
VENDOR TEST11 1235 format=1,1
BEGIN-VENDOR TEST11

View File

@@ -119,7 +119,8 @@ def test_valid_attribute_numbers(number):
@pytest.mark.parametrize(
"invalid_number", ["1000", "ABCD", "-1", "inf", "INF", "-INF", "2e4", "2.5e3"],
"invalid_number",
["1000", "ABCD", "-1", "inf", "INF", "-INF", "2e4", "2.5e3"],
)
def test_invalid_attribute_numbers(invalid_number):
dictionary = StringIO(f"ATTRIBUTE NAME {invalid_number} integer64")
@@ -247,7 +248,6 @@ def test_value_number_out_of_limit(value_num, attr_type):
"tlv",
"extended",
"long-extended",
"evs",
],
)
def test_all_datatypes_rfc_space(datatype):
@@ -255,6 +255,11 @@ def test_all_datatypes_rfc_space(datatype):
Dictionary("", dictionary)
def test_evs_datatype():
dictionary = StringIO("ATTRIBUTE TEST-ATTRIBUTE 1.10 evs\n")
Dictionary("", dictionary)
@pytest.mark.parametrize(
"datatype",
[
@@ -299,7 +304,8 @@ def test_invalid_datatypes_in_vendor_space(datatype):
@pytest.mark.parametrize(
"invalid_number", ["ABCD", "-1", "inf", "INF", "-INF", "0.1", "2e4", "2.5e3"],
"invalid_number",
["ABCD", "-1", "inf", "INF", "-INF", "0.1", "2e4", "2.5e3"],
)
def test_invalid_value_numbers(invalid_number):
dictionary = StringIO(
@@ -401,3 +407,16 @@ def test_get_nonexisting_attributes_from_dictionaries(attribute):
dd = Dictionary("", dictionary)
with pytest.raises(KeyError):
_ = dd[attribute]
def test_extended_evs():
dictionary = StringIO(
"ATTRIBUTE RFC-EXTENDED 10 extended\n"
"ATTRIBUTE RFC-EXTENDED-EVS 10.20 evs\n"
"VENDOR TEST-VENDOR 1234\n"
"BEGIN-VENDOR TEST-VENDOR format=RFC-EXTENDED-EVS\n"
"ATTRIBUTE VENDOR-ATTRIBUTE 10 integer\n"
"END-VENDOR TEST-VENDOR"
)
dd = Dictionary("", dictionary)
assert dd["VENDOR-ATTRIBUTE"].code == [10, 20, 10]