Dictionary File
Implements an iterable file format that handles the
RADIUS $INCLUDE directives behind the scene.
$INCLUDE resolution is sandboxed to a base directory so a malicious
or sloppy dictionary can't pull in /etc/passwd or escape via
../../. The base directory defaults to the directory of the
entry-point file (or os.curdir when the entry-point is a stream).
Pass include_base_dir to DictFile or Dictionary to lock
includes to an explicit trusted root.
DictFile
Dictionary file class
An iterable file type that handles $INCLUDE directives
internally. $INCLUDE paths are confined to include_base_dir
so a malicious dictionary can't read arbitrary files.
Source code in pyrad2/dictfile.py
| class DictFile:
"""Dictionary file class
An iterable file type that handles ``$INCLUDE`` directives
internally. ``$INCLUDE`` paths are confined to ``include_base_dir``
so a malicious dictionary can't read arbitrary files.
"""
__slots__ = ("stack", "_include_base")
def __init__(
self,
fil: str | io.TextIOWrapper,
*,
include_base_dir: Optional[str] = None,
) -> None:
"""Initialize the file reader and queue ``fil`` for iteration.
Args:
fil: A dictionary file path or an already-open text stream.
include_base_dir: Trusted base directory for ``$INCLUDE``
resolution. Nested includes whose resolved path falls
outside this directory are rejected with ``ParseError``.
Defaults to the directory of ``fil`` when it's a path,
or ``os.curdir`` when it's a stream.
"""
self.stack: list[_Node] = []
if include_base_dir is not None:
self._include_base = os.path.realpath(include_base_dir)
elif isinstance(fil, str):
entry_dir = os.path.dirname(os.path.abspath(fil)) or os.curdir
self._include_base = os.path.realpath(entry_dir)
else:
self._include_base = os.path.realpath(os.curdir)
self.__read_node(fil, is_entry_point=True)
def __read_node(
self, fil: str | io.TextIOWrapper, *, is_entry_point: bool = False
) -> None:
parentdir = self.__cur_dir()
if isinstance(fil, str):
if os.path.isabs(fil):
fname = fil
else:
fname = os.path.join(parentdir, fil)
# Nested ``$INCLUDE`` paths are sandboxed; the entry-point
# file is exempt because it implicitly defines the base.
if not is_entry_point:
resolved = os.path.realpath(fname)
try:
common = os.path.commonpath([self._include_base, resolved])
except ValueError:
common = ""
if common != self._include_base:
raise ParseError(
"$INCLUDE %r escapes the dictionary base directory %r"
% (fil, self._include_base),
file=self.file(),
line=self.line(),
)
# ``with`` so a parser error inside ``_Node.__init__`` doesn't
# leak the file descriptor.
with open(fname) as fd:
node = _Node(fd, fil, parentdir)
else:
node = _Node(fil, "", parentdir)
self.stack.append(node)
def __cur_dir(self) -> str:
if self.stack:
return self.stack[-1].dir
else:
return os.path.realpath(os.curdir)
def __get_include(self, line: str) -> Optional[str]:
line = line.split("#", 1)[0].strip()
tokens = line.split()
if tokens and tokens[0].upper() == "$INCLUDE":
return " ".join(tokens[1:])
else:
return None
def line(self) -> int:
"""Returns line number of current file"""
if self.stack:
return self.stack[-1].current
else:
return -1
def file(self) -> str:
"""Returns name of current file"""
if self.stack:
return self.stack[-1].name
else:
return ""
def __iter__(self) -> Self:
return self
def __next__(self) -> str:
while self.stack:
line = self.stack[-1].next()
if line is None:
self.stack.pop()
else:
inc = self.__get_include(line)
if inc:
self.__read_node(inc)
else:
return line
raise StopIteration
|
__init__(fil, *, include_base_dir=None)
Initialize the file reader and queue fil for iteration.
Parameters:
| Name |
Type |
Description |
Default |
fil
|
str | TextIOWrapper
|
A dictionary file path or an already-open text stream.
|
required
|
include_base_dir
|
Optional[str]
|
Trusted base directory for $INCLUDE
resolution. Nested includes whose resolved path falls
outside this directory are rejected with ParseError.
Defaults to the directory of fil when it's a path,
or os.curdir when it's a stream.
|
None
|
Source code in pyrad2/dictfile.py
| def __init__(
self,
fil: str | io.TextIOWrapper,
*,
include_base_dir: Optional[str] = None,
) -> None:
"""Initialize the file reader and queue ``fil`` for iteration.
Args:
fil: A dictionary file path or an already-open text stream.
include_base_dir: Trusted base directory for ``$INCLUDE``
resolution. Nested includes whose resolved path falls
outside this directory are rejected with ``ParseError``.
Defaults to the directory of ``fil`` when it's a path,
or ``os.curdir`` when it's a stream.
"""
self.stack: list[_Node] = []
if include_base_dir is not None:
self._include_base = os.path.realpath(include_base_dir)
elif isinstance(fil, str):
entry_dir = os.path.dirname(os.path.abspath(fil)) or os.curdir
self._include_base = os.path.realpath(entry_dir)
else:
self._include_base = os.path.realpath(os.curdir)
self.__read_node(fil, is_entry_point=True)
|
line()
Returns line number of current file
Source code in pyrad2/dictfile.py
| def line(self) -> int:
"""Returns line number of current file"""
if self.stack:
return self.stack[-1].current
else:
return -1
|
file()
Returns name of current file
Source code in pyrad2/dictfile.py
| def file(self) -> str:
"""Returns name of current file"""
if self.stack:
return self.stack[-1].name
else:
return ""
|