From d970df0dd7f3fb23c60bd75f021f0ee0ac7118e7 Mon Sep 17 00:00:00 2001 From: Istvan Ruzman Date: Mon, 21 Feb 2022 16:36:52 +0100 Subject: [PATCH] add: initial evs support --- src/pyrad3/dictionary.py | 143 ++++++++++++++++++++++++--------------- src/pyrad3/types.py | 4 +- tests/dictionaries/dict | 79 ++++++++++----------- tests/test_dictionary.py | 25 ++++++- 4 files changed, 152 insertions(+), 99 deletions(-) diff --git a/src/pyrad3/dictionary.py b/src/pyrad3/dictionary.py index 263b260..150ec1e 100644 --- a/src/pyrad3/dictionary.py +++ b/src/pyrad3/dictionary.py @@ -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 diff --git a/src/pyrad3/types.py b/src/pyrad3/types.py index 176e62c..8d27ab6 100644 --- a/src/pyrad3/types.py +++ b/src/pyrad3/types.py @@ -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 diff --git a/tests/dictionaries/dict b/tests/dictionaries/dict index ef461b9..fbcab0c 100644 --- a/tests/dictionaries/dict +++ b/tests/dictionaries/dict @@ -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 diff --git a/tests/test_dictionary.py b/tests/test_dictionary.py index bbbfda1..edc6771 100644 --- a/tests/test_dictionary.py +++ b/tests/test_dictionary.py @@ -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]