Commit 1c6b668b authored by Štěpán Balážik's avatar Štěpán Balážik
Browse files

before nuking strictyaml

parent 0da71ed1
__pycache__/
*.py[cod]
{
"path": "examples/iter_minim_a.yaml",
"config": "",
"server_groups": [
{
"step_range": [
0,
100
],
"addresses": [
"127.0.0.10"
],
"rtts": null,
"matchers": [
null,
null
]
},
{
"step_range": [
null,
null
],
"addresses": [
"192.5.6.30"
],
"rtts": null,
"matchers": [
null
]
},
{
"step_range": [
null,
null
],
"addresses": [
"1.2.3.4"
],
"rtts": null,
"matchers": [
null
]
}
],
"steps": [
{
"id": 1,
"generator": null,
"checker": null
},
{
"time_add": 100
}
]
}
\ No newline at end of file
# Reimplementation of iter_minim_a.rpl from Deckard showcasing some of the features of the new parser
config: "No configuration handling yet. :("
# Parsing:
# --------
# In an interactive python console, run
#
# from mockdns.parser import ParsedScenario
# scenario = ParsedScenario("examples/iter_minim_a.yaml")
#
# and explore the resulting object.
server_groups:
# defaults for Matchpart:
# -----------------------
# criteria: []
# adjust: []
# message:
# id: <random>
# opcode: QUERY
# rcode: NOERROR
# flags: QR
# eflags: [] # ENS flags
# question: []
# answer: []
# authority: []
# additional: []
# edns:
# version: 0
# payload: 4096
ranges:
# k.root-servers.net.
- first_step: 0 # default: start of scenario
last_step: 100 # default: end of scenario
- first_step: 0
last_step: 100
addresses:
- 127.0.0.10 # can be both list and one value
- 127.0.0.10
matchers:
- type: Matchpart
options:
criteria: opcode, qtype, qname # can be both list and comma-separated
criteria: opcode, qtype, qname
adjust:
- copy_id # can be both list and comma-separated
- copy_id
message:
question: . IN NS # can be both list and one value
answer: . 3600 IN NS K.ROOT-SERVERS.NET. # ttl is required for now
question: . IN NS
answer: . 3600 IN NS K.ROOT-SERVERS.NET.
additional: K.ROOT-SERVERS.NET. 3600 IN A 127.0.0.10
- type: Matchpart
......@@ -57,7 +27,7 @@ ranges:
authority: com. 3600 IN NS a.gtld-servers.net.
additional: a.gtld-servers.net. 3600 IN A 192.5.6.30
# a.gtld-servers.net.
- addresses: 192.5.6.30
matchers:
- type: Matchpart
......@@ -72,7 +42,7 @@ ranges:
authority: example.com. 3600 IN NS ns.example.com.
additional: ns.example.com. 3600 IN A 1.2.3.4
# ns.example.com
- addresses: 1.2.3.4
matchers:
- type: Matchpart
......@@ -87,7 +57,7 @@ ranges:
steps:
- step_id: 1
- id: 1
generator:
type: Static
options:
......@@ -100,5 +70,7 @@ steps:
flags: QR, RD, RA
question: www.example.com. IN A
answer: www.example.com. 3600 IN A 10.20.30.40
- id: 3
time_add: 100
from mockdns.parser.parser import ParsedScenario
from mockdns.parser.parser import YAMLScenario, JSONScenario
"""Define Abstract Base Classes for Actors"""
from abc import ABC, abstractmethod
from copy import deepcopy
from typing import Any, Dict, Iterable, Iterator, Mapping, Optional
from dns.message import Message
......@@ -9,10 +10,24 @@ from mockdns.scenario import DataMismatch
class Actor(ABC):
def __init__(self, yaml: strictyaml.YAML):
def __init__(self, yaml: Optional[strictyaml.YAML]):
if yaml is None:
return
self.options = yaml["options"].data
self.line_number = yaml.start_line
self.yaml = yaml
# self._dict = yaml["options"].data
# self._dict.update({"__line_number": self.line_number})
# self.yaml = yaml
# @classmethod
# def from_dict(cls, d: Dict[str, Any]):
# actor = cls(None)
# actor._dict = deepcopy(d)
# print(cls, d)
# actor.line_number = d.pop("__line_number")
# actor.options = d
# actor.yaml = None
# return actor
@staticmethod
@abstractmethod
......@@ -22,6 +37,9 @@ class Actor(ABC):
def set_up(self) -> None:
pass
# def to_dict(self) -> Dict[str, Any]:
# return self._dict
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.options}, line: {self.line_number})"
......
......@@ -2,11 +2,12 @@ from copy import deepcopy
from typing import Iterable, Optional
import dns.message
from dns.opcode import _by_text as OPCODES
from dns.rcode import _by_text as RCODES
from dns.flags import _by_text as FLAGS
from dns.flags import _edns_by_text as EFLAGS
from dns.opcode import Opcode
from dns.rcode import Rcode
from dns.flags import Flag
from dns.flags import EDNSFlag
import strictyaml
import snscrape
from mockdns.parser.actor_abc import Checker, CheckerMismatch, Generator, Matcher, Actor
from mockdns.parser.schema import DNSQuestion, DNSRecord, ListOrCommaSeparated, OneOrListOf
......@@ -27,16 +28,16 @@ class _Matchpart(Actor):
{
strictyaml.Optional("id"): strictyaml.Int(),
strictyaml.Optional("opcode", default="QUERY"): strictyaml.Enum(
list(OPCODES.keys())
list(Opcode.__members__.keys())
),
strictyaml.Optional("rcode", default="NOERROR"): strictyaml.Enum(
list(RCODES.keys())
list(Rcode.__members__.keys())
),
strictyaml.Optional("flags", default=["QR"]): ListOrCommaSeparated(
strictyaml.Enum(list(FLAGS.keys()))
strictyaml.Enum(list(Flag.__members__.keys()))
),
strictyaml.Optional("eflags", default=[]): ListOrCommaSeparated(
strictyaml.Enum(list(EFLAGS.keys()))
strictyaml.Enum(list(EDNSFlag.__members__.keys()))
),
strictyaml.Optional("question", default=[]): OneOrListOf(DNSQuestion()),
strictyaml.Optional("answer", default=[]): OneOrListOf(DNSRecord()),
......
from typing import Any, Dict, List, Mapping, Optional, Type, Union
import ipaddress
import json
import os.path
from typing import Any, cast, Dict, List, Mapping, Optional, Type, Tuple, Union
import strictyaml
from mockdns.parser.actor_abc import Actor, Checker, Generator, Matcher
from mockdns.parser.actors import * # pylint: disable=unused-wildcard-import
from mockdns.scenario import Config, Range, Scenario, Step, Time, RTT
from mockdns.parser.schema import SCHEMA, STEP_SCHEMA, TIME_SCHEMA
from mockdns.scenario import Config, ServerGroup, Scenario, Step, Time, RTT
from mockdns.parser.schema import SCHEMA, ACTOR_STEP_SCHEMA, TIME_ADD_SCHEMA, TIME_SET_SCHEMA
MATCHERS = {cls.__name__: cls for cls in Matcher.__subclasses__()}
CHECKERS = {cls.__name__: cls for cls in Checker.__subclasses__()}
GENERATORS = {cls.__name__: cls for cls in Generator.__subclasses__()}
class ParsedScenario(Scenario):
@staticmethod
def get_actor(
parsed_actor: strictyaml.YAML, superclass: str, subclasses: Dict[str, Type]
) -> Actor:
try:
type_name = parsed_actor["type"]
cls = subclasses[type_name]
assert issubclass(cls, Actor)
schema = cls.schema()
except KeyError:
raise RuntimeError(
f"Unknown {superclass} type {type_name} at line {parsed_actor.start_line}, make sure there's a class of this name inhereting from Matcher in some .py file in parser/actors directory."
)
def _get_actor(parsed_actor: Union[strictyaml.YAML, Dict[str, Any]], superclass: str, subclasses: Dict[str, Type]) -> Actor:
try:
type_name = parsed_actor["type"]
cls = subclasses[type_name]
assert issubclass(cls, Actor)
schema = cls.schema()
except KeyError:
if isinstance(parsed_actor, strictyaml.YAML):
line = parsed_actor.start_line
else:
line = parsed_actor["__line_number"]
raise RuntimeError(
f"Unknown {superclass} type {type_name} at line {line}, make sure there's a class of this name inhereting from Matcher in some .py file in parser/actors directory."
)
if isinstance(parsed_actor, strictyaml.YAML):
parsed_actor["options"].revalidate(schema)
return cls(parsed_actor)
else:
return cls.from_dict(parsed_actor)
class ParsedConfig(Config):
def to_dict(self) -> Dict[str, Any]:
return {} # Not implemented for now
@classmethod
def from_dict(cls, d: Dict[str, Any]) -> "ParsedConfig":
return cls()
class ParsedRTT(RTT):
def to_dict(self):
return self.__dict__
@classmethod
def from_dict(cls, d: Dict[str, Any]) -> "ParsedRTT":
return cls(d["mean"], d["std"], d["loss"])
class ParsedServerGroup(ServerGroup):
def to_dict(self):
return {
"step_range": list(self.step_range),
"addresses": [str(a) for a in self.addresses],
"rtts": {str(a): rtt.to_dict() for a, rtt in self.rtts.items()} if self.rtts else None,
"matchers": [m.to_dict() for m in self.matchers]
}
@classmethod
def from_dict(cls, d: Dict[str, Any]) -> "ParsedServerGroup":
return cls(
cast(Tuple[Optional[int], Optional[int]], tuple(d["step_range"])),
[ipaddress.ip_address(ip) for ip in d["addresses"]],
{ipaddress.ip_address(ip): ParsedRTT.from_dict(rtt) for ip, rtt in d["rtts"].items()} if d["rtts"] else None,
[_get_actor(m, "matcher", MATCHERS).from_dict(m) for m in d["matchers"]]
)
class ParsedStep(Step):
def to_dict(self):
return {
"id": self.step_id,
"generator": self.generator.to_dict(),
"checker": self.checker.to_dict()
}
@classmethod
def from_yaml(cls, path) -> "ParsedScenario":
with open(path, "r") as f:
parsed = strictyaml.load(f.read(), SCHEMA, label=path)
def from_dict(cls, d: Dict[str, Any]) -> "ParsedStep":
return cls(
d["id"],
_get_actor(d, "generator", GENERATORS).from_dict(d["generator"]),
_get_actor(d, "checker", CHECKERS).from_dict(d["checker"])
)
ranges = []
for r in parsed["ranges"]:
class ParsedTime(Time):
def to_dict(self):
return {
"id": self.step_id,
"time_add" if self.action == Time.TimeAction.ADD else "time_set": self.value
}
@classmethod
def from_dict(cls, d: Dict[str, Any]) -> "ParsedTime":
if "time_add" in d:
action = Time.TimeAction.ADD
value = d["time_add"]
else:
action = Time.TimeAction.SET
value = d["time_set"]
return cls(d["id"], action, value)
class YAMLScenario(Scenario):
def __init__(self, yaml_path: str):
with open(yaml_path, "r") as f:
parsed = strictyaml.load(f.read(), SCHEMA, label=yaml_path)
server_groups = []
for r in parsed["server_groups"]:
matchers = []
for matcher in r["matchers"]:
m = cls.get_actor(matcher, "matcher", MATCHERS)
m = _get_actor(matcher, "matcher", MATCHERS)
assert isinstance(m, Matcher)
matchers.append(m)
rtt = (
......@@ -49,10 +128,10 @@ class ParsedScenario(Scenario):
if r["rtt"].data is not None
else None
)
ranges.append(
Range(
server_groups.append(
ParsedServerGroup(
(r["first_step"].data, r["last_step"].data),
r["addresses"].data,
[r["addresses"].data] if isinstance(r["addresses"].data, (ipaddress.IPv6Address, ipaddress.IPv4Address)) else r["addresses"].data,
rtt,
matchers,
)
......@@ -60,25 +139,54 @@ class ParsedScenario(Scenario):
steps: List[Union[Time, Step]] = []
for step in parsed["steps"]:
# This is fuggly but I don't now how to make it nicer :(
# Exception groups would be a solution, but Python 3.10 is comming to distributions never.
try:
step.revalidate(STEP_SCHEMA)
checker = cls.get_actor(step["checker"], "checker", CHECKERS)
step.revalidate(ACTOR_STEP_SCHEMA)
checker = _get_actor(step["checker"], "checker", CHECKERS)
assert isinstance(checker, Checker)
generator = cls.get_actor(step["generator"], "generator", GENERATORS)
generator = _get_actor(step["generator"], "generator", GENERATORS)
assert isinstance(generator, Generator)
steps.append(Step(step["step_id"].data, generator, checker))
steps.append(ParsedStep(step["id"].data, generator, checker))
except strictyaml.YAMLValidationError:
step.revalidate(TIME_SCHEMA)
if "time_add" in step:
steps.append(Time(Time.TimeAction.ADD, step["time_add"].data))
elif "time_set" in step:
steps.append(Time(Time.TimeAction.SET, step["time_set"].data))
else:
assert (
False
), "Should never happen, something is wrong with the builtin step schema."
return cls(path, Config(), ranges, steps)
def to_yaml(self):
pass
try:
step.revalidate(TIME_ADD_SCHEMA)
steps.append(ParsedTime(step["id"].data, Time.TimeAction.ADD, step["time_add"].data))
except strictyaml.YAMLValidationError:
step.revalidate(TIME_ADD_SCHEMA)
steps.append(ParsedTime(step["id"].data, Time.TimeAction.SET, step["time_set"].data))
super().__init__(yaml_path, ParsedConfig(), server_groups, steps)
def compile_to_json(self, path: str = None) -> None:
if path is None:
path = f"{os.path.splitext(self.path)[0]}.json"
with open(path, "w") as f:
json.dump(
{
"path": self.path,
"config": "", # Not implemented for now
"server_groups": [cast(ParsedServerGroup, g).to_dict() for g in self.server_groups],
"steps": [cast(ParsedStep, s).to_dict() for s in self.steps]
}, f, indent=4)
class JSONScenario(Scenario):
@staticmethod
def step_or_time(d: Dict[str, Any]) -> Union[ParsedStep, ParsedTime]:
if "generator" in d:
return ParsedStep.from_dict(d)
return ParsedTime.from_dict(d)
def __init__(self, json_path: str):
with open(json_path) as f:
parsed = json.load(f)
super().__init__(
json_path,
ParsedConfig.from_dict(parsed["config"]),
[ParsedServerGroup.from_dict(g) for g in parsed["server_groups"]],
[self.step_or_time(s) for s in parsed["steps"]]
)
......@@ -188,26 +188,38 @@ RTT_SCHEMA = strictyaml.Map(
}
)
STEP_SCHEMA = strictyaml.Map(
ACTOR_STEP_SCHEMA = strictyaml.Map(
{
"step_id": strictyaml.Int(),
"id": strictyaml.Int(),
"generator": ACTOR_SCHEMA,
"checker": ACTOR_SCHEMA,
}
)
TIME_SCHEMA = strictyaml.MapPattern(
strictyaml.Enum(["time_add", "time_set"]),
strictyaml.Int(),
minimum_keys=1,
maximum_keys=1,
TIME_ADD_SCHEMA = strictyaml.Map(
{
"id": strictyaml.Int(),
"time_add": strictyaml.Int()
}
)
TIME_SET_SCHEMA = strictyaml.Map(
{
"id": strictyaml.Int(),
"time_set": strictyaml.Int()
}
)
STEP_VARIANTS = [ACTOR_STEP_SCHEMA, TIME_SET_SCHEMA, TIME_ADD_SCHEMA]
_VARIANTS = dict(sum((list(schema._validator_dict.items()) for schema in STEP_VARIANTS), []))
STEP_SCHEMA = strictyaml.Map({strictyaml.Optional(key): value for key, value in _VARIANTS.items()})
SCHEMA = strictyaml.Map(
{
"config": strictyaml.Str(), # TODO
"ranges": strictyaml.Seq(
"server_groups": strictyaml.Seq(
strictyaml.Map(
{
strictyaml.Optional("first_step", drop_if_none=False): strictyaml.Int() | strictyaml.EmptyNone(),
......
......@@ -2,7 +2,7 @@ from mockdns.scenario.scenario import (
Config,
RTT,
MatcherNoResponse,
Range,
ServerGroup,
DataMismatch,
Step,
Scenario,
......
......@@ -34,20 +34,21 @@ class MatcherNoResponse(Exception):
@dataclass
class Range:
class ServerGroup:
"""
Range determines the answers to queries send in a range of steps.
ServerGroup determines the answers to queries send in a range of steps.
If queries destination matches one of the Range's addresses, the response
(or lack thereof) is deteremined by Ranges's matchers.
If queries destination `step_id` of a step falls into `step_range` of a ServerGroup
and matches one of the ServerGroup's addresses, the response
(or lack thereof) is deteremined by its matchers.
"""
# Defines first and last step id for which this range is aplicable.
# Defines first and last step id for which this ServerGroup is aplicable.
# Note that this range is inclusive and None means scenario start or end:
# (None, 100) means upto step 100, (100, None) means from step 100 to the end.
step_range: Tuple[Optional[int], Optional[int]]
# Queries to these addresses will be handled by this range if the current
# Queries to these addresses will be handled by this ServerGroup if the current
# step number belongs to step_range.
addresses: Iterable[IPAddress]
......@@ -55,7 +56,7 @@ class Range:
rtts: Optional[Mapping[IPAddress, RTT]]
# Matcher is a function which takes a DNS message from a binary under test
# to this Range. It can return a DNS message which is then sent as a reply.
# to this ServerGroup. It can return a DNS message which is then sent as a reply.
# It can return None which leads to the next Matcher in sequence to be called
# with the same input. It can also raise MatcherNoResponse if which case no
# reply will be sent. Also if all Matchers return None, no reply is sent but
......@@ -76,11 +77,11 @@ class Step:
to be met by the replies the program.
"""
# ID is used to identify ranges to be used for replies to the program under test
# ID is used to identify ServerGroup to be used for replies to the program under test
step_id: int
# Iterable of DNS messages to be sent to the program under test
generator: Sequence[Message]
generator: Iterable[Message]
# Function which takes the query sent to the program under test and the reply
# the program provided. Checks if the answer is correct and raises DataMismatch
......@@ -95,7 +96,7 @@ class Time:
ADD just adds the number of seconds from `value` to current time.
SET interprets `value` as UNIX timestamp or as DNSSEC timestamp (the ranges of these are disjunct :).
"""
step_id: int
class TimeAction(Enum):
ADD = auto()
SET = auto()
......@@ -106,7 +107,7 @@ class Time:
@dataclass
class Scenario:
name: str
path: str
config: Config
upstream: Iterable[Range]
server_groups: Iterable[ServerGroup]
steps: Sequence[Union[Step, Time]]
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment