class ExpResCriterion_RTT(ExpResCriterion):
    """Criterion: rtt

    Test the median round trip time toward destination.

    Available for: ping, traceroute.

    `rtt`: maximum RTT (in ms).

    `rtt_tolerance` (optional): tolerance (in %) on `rtt`.

    If `rtt_tolerance` is not given, match when measured RTT is less
    than `rtt`, otherwise match when measured RTT is within `rtt`
    +/- `rtt_tolerance` %.

    Examples:

    expected_results:
      LowRTT:
        rtt: 50
      Near150:
        rtt: 150
        rtt_tolerance: 30
    """

    CRITERION_NAME = "rtt"
    AVAILABLE_FOR_MSM_TYPE = ["traceroute", "ping"]
    MANDATORY_CFG_FIELDS = []
    OPTIONAL_CFG_FIELDS = ["rtt_tolerance"]

    def __init__(self, cfg, expres):
        ExpResCriterion.__init__(self, cfg, expres)

        self.rtt = self._enforce_param("rtt", int)
        self.rtt_tolerance = self._enforce_param("rtt_tolerance", int) or 0

    def __str__(self):
        if self.rtt:
            if self.rtt_tolerance:
                return "RTT must be within {}ms +/- {}%".format(
                    self.rtt, self.rtt_tolerance
                )
            else:
                return "RTT must be less than {}ms".format(
                        self.rtt
                )

    def prepare(self, result):
        res = ParsedResult_RTT(self.expres.monitor, result)
        self.res_rtt = res.rtt

    def result_matches(self, result):
        result_rtt = self.res_rtt

        if result_rtt:
            if not self.rtt_tolerance:
                logger.debug(
                    "  verifying if RTT {} < {}...".format(
                        result_rtt, self.rtt
                    )
                )

                if result_rtt > self.rtt:
                    return False
            else:
                logger.debug(
                    "  verifying if RTT {} within {} +/- {}%...".format(
                        result_rtt, self.rtt,
                        self.rtt_tolerance
                    )
                )

                delta = self.rtt * self.rtt_tolerance / 100
                if abs(self.rtt - result_rtt) > delta:
                    return False
        else:
            raise ResultProcessingError(
                "Can't verify RTT: RTT is unknown"
            )

        return True class ExpResCriterion_DstResponded(ExpResCriterion):
    """Criterion: dst_responded

    Verify if destination responded.

    Available for: traceroute, ping, sslcert.

    `dst_responded`: boolean indicating if the destination is expected to be
    responding or not.

    For ping, a destination is responding if a probe received at least one
    reply packet.
    For sslcert, a destination is responding if at least one certificate is
    received by the probe.

    If `dst_responded` is True, match when a destination is responding.
    If `dst_responded` is False, match when a destination is not responding.

    Example:

    expected_results:
      DestinationReachable:
        dst_responded: True
    """

    CRITERION_NAME = "dst_responded"
    AVAILABLE_FOR_MSM_TYPE = ["traceroute", "ping", "sslcert"]
    MANDATORY_CFG_FIELDS = []
    OPTIONAL_CFG_FIELDS = []

    def __init__(self, cfg, expres):
        ExpResCriterion.__init__(self, cfg, expres)

        self.dst_responded = self._enforce_param("dst_responded", bool)

    def __str__(self):
        if self.dst_responded:
            return "Destination must respond"
        else:
            return "Destination must not respond"

    def prepare(self, result):
        res = ParsedResult_DstResponded(self.expres.monitor, result)
        self.res_responded = res.responded

    def result_matches(self, result):
        result_responded = self.res_responded

        logger.debug(
            "  testing if target responded ({}) is {}...".format(
                result_responded, self.dst_responded
            )
        )

        if result_responded and not self.dst_responded:
            logger.debug("  target responded while it should not")
            return False
        if not result_responded and self.dst_responded:
            logger.debug("  target did not respond")
            return False

        return True class ExpResCriterion_DstIP(ExpResCriterion):
    """Criterion: dst_ip

    Verify that the destination IP used by the probe for the measurement is
    the expected one.

    Available for: traceroute, ping, sslcert.

    `dst_ip`: list of expected IP addresses (or prefixes).

    Match when the probe destination IP is one of the expected ones (or falls
    within one of the expected prefixes).

    Examples:

    dst_ip: 192.168.0.1

    dst_ip:
    - 192.168.0.1
    - 2001:DB8::1

    dst_ip:
    - 192.168.0.1
    - 10.0.0.0/8
    - 2001:DB8::/32
    """

    CRITERION_NAME = "dst_ip"
    AVAILABLE_FOR_MSM_TYPE = ["traceroute", "ping", "sslcert"]
    MANDATORY_CFG_FIELDS = []
    OPTIONAL_CFG_FIELDS = []

    def __init__(self, cfg, expres):
        ExpResCriterion.__init__(self, cfg, expres)

        self.dst_ip = []
        dst_ip = self._enforce_list("dst_ip", str)

        for ip in dst_ip:
            try:
                self.dst_ip.append(IPy.IP(ip))
            except:
                raise ConfigError("Invalid IP address/net: {}".format(ip))

    def __str__(self):
        all_subnets = True
        for ip in self.dst_ip:
            all_subnets = all_subnets and ip.prefixlen() not in [32, 128]

        more_than_one = len(self.dst_ip) > 1

        if all_subnets:
            tpl = "Destination IP must fall into {}"
        else:
            if more_than_one:
                tpl = "Destination IP must be in {}"
            else:
                tpl = "Destination IP must be {}"

        return tpl.format(self._str_list())

    def prepare(self, result):
        res = ParsedResult_DstIP(self.expres.monitor, result)
        self.res_dst_ip = res.dst_ip

    def result_matches(self, result):
        result_dst_ip = self.res_dst_ip

        try:
            result_dst_ip = IPy.IP(result_dst_ip)
        except:
            raise ResultProcessingError(
                "Invalid destination IP address: {}".format(result_dst_ip)
            )

        match = False

        for dst_ip in self.dst_ip:
            if dst_ip.prefixlen() not in [32, 128]:
                logger.debug(
                    "  verifying if destination IP {} falls into "
                    "the expected subnet ({})...".format(
                        result_dst_ip, dst_ip
                    )
                )

                match = result_dst_ip in dst_ip
            else:
                logger.debug(
                    "  verifying if destination IP {} matches "
                    "the expected one ({})".format(
                        result_dst_ip, self.dst_ip
                    )
                )

                match = result_dst_ip == dst_ip

            if match:
                break

        return match class ExpResCriterion_DstAS(ExpResCriterion):
    """Criterion: dst_as

    Verify the traceroute destination's AS number.

    Available for: traceroute

    `dst_as`: list of Autonomous System numbers.

    It builds the path of ASs traversed by the traceroute.
    Match when the last AS in the path is one of the expected ones.

    Examples:

    dst_as:
    - 64496

    dst_as:
    - 64496
    - 65551
    """

    CRITERION_NAME = "dst_as"
    AVAILABLE_FOR_MSM_TYPE = ["traceroute"]
    MANDATORY_CFG_FIELDS = []
    OPTIONAL_CFG_FIELDS = []

    def __init__(self, cfg, expres):
        ExpResCriterion.__init__(self, cfg, expres)

        self.dst_as = self._enforce_list("dst_as", int)

    def __str__(self):
        return "Destination AS: {}".format(self._str_list())

    def display_string(self):
        more_than_one = len(self.dst_as) > 1
        return(
            "    - target must be in {}the following ASN{}: {}".format(
                "one of " if more_than_one else "",
                "s" if more_than_one else "",
                self._str_list()
            )
        )

    def prepare(self, result):
        res = ParsedResult_DstAS(self.expres.monitor, result)
        self.res_dst_as = res.dst_as

    def result_matches(self, result):
        logger.debug(
            "  verifying if destination AS {} in {}...".format(
                self.res_dst_as, self._str_list()
            )
        )

        if self.res_dst_as not in self.dst_as:
            return False

        return True class ExpResCriterion_ASPath(ExpResCriterion):
    """Criterion: as_path

    Verify the path of ASs traversed by a traceroute.

    Available for: traceroute

    `as_path`: list of Autonomous System path.

    An AS path is made of AS numbers separated by white spaces. It can
    contain two special tokens:

    - "S", that is expanded with the probe's source AS number;

    - "IX", that represents an Internet Exchange Point peering network for
      those IXPs which don't announce their peering prefixes via BGP.

    The "IX" token is meagniful only if the `ip_cache.use_ixps_info`
    global configuration parameter is True.

    It builds the path of ASs traversed by the traceroute.
    Match when the AS path or a contiguous part of it is one of
    the expected ones.

    Examples:

    as_path: 64496 64497

    as_path:
    - 64496 64497
    - 64498 64499 64500

    as_path:
    - S 64496 64497

    as_path:
    - S IX 64500
    """

    CRITERION_NAME = "as_path"
    AVAILABLE_FOR_MSM_TYPE = ["traceroute"]
    MANDATORY_CFG_FIELDS = []
    OPTIONAL_CFG_FIELDS = []

    def __init__(self, cfg, expres):
        ExpResCriterion.__init__(self, cfg, expres)

        self.as_path = self._enforce_list("as_path", str)

        # AS path space-separated list of AS numbers

        for path in self.as_path:
            try:
                asns_list = path.split(" ")

                for asn in asns_list:
                    if not asn.isdigit() and asn not in ["S", "IX"]:
                        raise ConfigError(
                            "Invalid ASN: {}".format(asn)
                        )
            except Exception as e:
                raise ConfigError(
                    "Invalid syntax for as_path {} - {}. It must be a "
                    "space-separated list of AS numbers. The 'S' string "
                    "can be used to match the source probe's "
                    "ASN. The 'IX' string can be used to match crossing "
                    "of any peering LAN for those IXs which don't "
                    "announce their peering LAN prefixes".format(path, str(e))
                )

    def __str__(self):
        return "AS path: {}".format(self._str_list())

    def display_string(self):
        more_than_one = len(self.as_path) > 1
        return(
            "    - target must be reached via {}the following "
            "AS path{}: {}".format(
                "one of " if more_than_one else "",
                "s" if more_than_one else "",
                self._str_list()
            )
        )

    def prepare(self, result):
        res = ParsedResult_ASPath(self.expres.monitor, result)
        self.res_as_path = res.as_path
        self.res_as_path_ixps = res.as_path_ixps

    def result_matches(self, result):
        matching_as_path_found = False

        probe = self.monitor.get_probe(result)

        # if the expected path contains the 'IX' macro, it is
        # tested against the as_path_ixps list, otherwise against
        # the as_path list

        for exp_as_path in self.as_path:
            if "IX" in exp_as_path:
                path = " ".join(self.res_as_path_ixps)
            else:
                path = " ".join(self.res_as_path)

            logger.debug(
                "  verifying if AS path {} matches {}...".format(
                    path, exp_as_path
                )
            )

            if re.search(
                r" {} ".format(
                    exp_as_path.replace("S", str(probe.asn))
                ),
                " {} ".format(path.replace("S", str(probe.asn)))
            ):
                logger.debug(
                    "    path {} matches {}".format(
                        path, exp_as_path
                    )
                )
                matching_as_path_found = True
                break

        return matching_as_path_found class ExpResCriterion_UpstreamAS(ExpResCriterion):
    """Criterion: upstream_as

    Verify the traceroute destination upstream's AS number.

    Available for: traceroute

    `upstream_as`: list of Autonomous System numbers.

    It builds the path of ASs traversed by the traceroute.
    Match when the penultimate AS in the path is one of the expected ones.

    Examples:

    upstream_as:
    - 64496

    upstream_as:
    - 64496
    - 64497
    """

    CRITERION_NAME = "upstream_as"
    AVAILABLE_FOR_MSM_TYPE = ["traceroute"]
    MANDATORY_CFG_FIELDS = []
    OPTIONAL_CFG_FIELDS = []

    def __init__(self, cfg, expres):
        ExpResCriterion.__init__(self, cfg, expres)

        self.upstream_as = self._enforce_list("upstream_as", int)

    def __str__(self):
        return "Target upstream AS: {}".format(self._str_list())

    def display_string(self):
        more_than_one = len(self.upstream_as) > 1
        return(
            "    - target must be reached via {}the following "
            "upstream ASN{}: {}".format(
                "one of " if more_than_one else "",
                "s" if more_than_one else "",
                self._str_list()
            )
        )

    def prepare(self, result):
        res = ParsedResult_UpstreamAS(self.expres.monitor, result)
        self.res_upstream_as = res.upstream_as

    def result_matches(self, result):
        upstream_as = self.res_upstream_as

        logger.debug(
            "  verifying if upstream AS {} in {}...".format(
                upstream_as, self._str_list()
            )
        )

        if upstream_as not in self.upstream_as:
            return False

        return True class ExpResCriterion_CertFP(ExpResCriterion):
    """Criterion: cert_fp

    Verify SSL certificates' fingerprints.

    Available for: sslcert

    `cert_fp`: list of certificates' SHA256 fingerprints or SHA256
    fingerprints of the chain.

    A fingerprint must be in the format 12:34:AB:CD:EF:... 32 blocks of 2
    characters hex values separated by colon (":").

    The `cert_fp` parameter can contain stand-alone fingerprints or bundle of
    fingerprints in the format "fingerprint1,fingerprint2,fingerprintN".

    A result matches if any of its certificates' fingerprint is in the list
    of stand-alone expected fingerprints or if the full chain fingerprints is
    in the list of bundle fingerprints.

    Examples:

    expected_results:
      MatchLeafCertificate:
        cert_fp: 01:02:[...]:31:32
      MatchLeacCertificates:
        cert_fp:
        - 01:02:[...]:31:32
        - 12:34:[...]:CD:EF
      MatchLeafOrChain:
        cert_fp:
        - 01:02:[...]:31:32
        - 12:34:[...]:CD:EF,56:78:[...]:AB:CD
    """

    CRITERION_NAME = "cert_fp"
    AVAILABLE_FOR_MSM_TYPE = ["sslcert"]
    MANDATORY_CFG_FIELDS = []
    OPTIONAL_CFG_FIELDS = []

    FP_PATTERN = re.compile("^[0-9A-F]{2}(:[0-9A-F]{2}){31}$",
                            flags=re.IGNORECASE)

    def _validate_fp(self, v):
        # v can be str or list
        if isinstance(v, str):
            fp = v

            if self.FP_PATTERN.match(fp):
                return fp.upper()
            else:
                raise ConfigError(
                    "Invalid SHA256 fingerprint for cert_fp: {}. "
                    "It must be in the format "
                    "12:34:AB:CD:EF:...[,56:78:9A...]: ".format(
                        fp
                    )
                )
        elif isinstance(v, list):
            ret = []
            for fp in v:
                ret.append(self._validate_fp(fp))
            return ret
        else:
            raise ConfigError(
                "Invalid type for fp: {}".format(type(fp))
            )

    def __init__(self, cfg, expres):
        ExpResCriterion.__init__(self, cfg, expres)

        # list of stand-alone fingerprints or chain of fps
        # [ fp1, fp2, [fp3, fp4] ]
        # will be converted in self.standalone_fps and self.chain_fps
        # a match occurs when
        # - at least one certificate's fp == a stand-alone fp
        # - all the certificates' fps == a chain of fps
        self.cert_fp = self._enforce_list("cert_fp", str)

        self.standalone_fps = []
        self.chain_fps = []

        for fp in self.cert_fp:
            if "," in fp:
                self.chain_fps.append(self._validate_fp(fp.split(",")))
            else:
                self.standalone_fps.append(self._validate_fp(fp))

    def __str__(self):
        return "Certificate SHA256 fingerpring: {}".format(
            self._str_list()
        )

    @staticmethod
    def _str_fp(s):
        return "{}:[...]:{}".format(
            ":".join(s.split(":")[0:2]),
            ":".join(s.split(":")[-2:])
        )

    def _str_list(self):
        res = []
        for fp in self.standalone_fps:
            res.append(self._str_fp(fp))
        for chain in self.chain_fps:
            res.append(
                "({})".format(
                    ", ".join(map(self._str_fp, chain))
                )
            )
        return ", ".join(res)

    def display_string(self):
        more_than_one = len(self.cert_fp) > 1
        return(
            "    - certificate SHA256 fingerprint must be {}the following: "
            "{}".format(
                "one of " if more_than_one else "",
                self._str_list()
            )
        )

    def prepare(self, result):
        res = ParsedResult_CertFps(self.expres.monitor, result)
        self.res_cer_fps = res.cer_fps

    def result_matches(self, result):
        cer_fps = self.res_cer_fps

        logger.debug(
            "  verifying if certificates fingerprints {} in {}...".format(
                ", ".join(cer_fps), self._str_list()
            )
        )

        for cer in result.certificates:
            if cer.checksum_sha256.upper() in self.standalone_fps:
                return True

        for chain in self.chain_fps:
            if sorted(cer_fps) == sorted(chain):
                return True

        return False class ExpResCriterion_DNSFlags(ExpResCriterion_DNSBased):
    """Criterion: dns_flags

    Verify if DNS responses received by a probe have the expected
    headers flags on.

    Available for: dns.

    `dns_flags`: list of expected DNS flag ("aa", "ad", "cd", "qr", "ra",
    "rd").

    Match when all the responses received by a probe have all the expected
    flags on.

    Example:

    expected_results:
      AA_and_AD:
        dns_flags:
        - aa
        - ad
    """

    CRITERION_NAME = "dns_flags"
    AVAILABLE_FOR_MSM_TYPE = ["dns"]
    MANDATORY_CFG_FIELDS = []
    OPTIONAL_CFG_FIELDS = []

    def __init__(self, cfg, expres):
        ExpResCriterion_DNSBased.__init__(self, cfg, expres)

        self.dns_flags = set()
        dns_flags = self._enforce_list("dns_flags", str)

        for flag in dns_flags:
            if flag.lower() not in ParsedResult_DNSHeader.DNS_HEADER_FLAGS:
                raise ConfigError("Invalid DNS flag: {}".format(flag))
            if flag.lower() not in self.dns_flags:
                self.dns_flags.add(flag.lower())

    def __str__(self):
        return "DNS response flags: {}".format(
            ", ".join(sorted(self.dns_flags))
        )

    def prepare_response(self, result, response):
        res = ParsedResult_DNSHeader(self.expres.monitor, result, response)
        self.response_flags = res.flags

    def response_matches(self, response):
        response_flags = self.response_flags

        logger.debug(
            "  verifying if expected flags ({}) are "
            "in the response's flags ({})...".format(
                ", ".join(self.dns_flags),
                ", ".join(response_flags)
            )
        )

        if not self.dns_flags.issubset(response_flags):
            return False

        return True class ExpResCriterion_DNSRCode(ExpResCriterion_DNSBased):
    """Criterion: dns_rcode

    Verify if DNS responses received by a probe have the expected rcode.

    Available for: dns.

    `dns_rcode`: list of expected DNS rcodes ("NOERROR", "FORMERR", "SERVFAIL",
    "NXDOMAIN", "NOTIMP", "REFUSED", "YXDOMAIN", "YXRRSET", "NXRRSET",
    "NOTAUTH", "NOTZONE", "BADVERS").

    Match when all the responses received by a probe have one of the expected
    rcodes listed in `dns_rcode`.

    Example:

    expected_results:
      DNS_NoError_or_NXDomain:
        dns_rcode:
        - "NOERROR"
        - "NXDOMAIN"
    """

    CRITERION_NAME = "dns_rcode"
    AVAILABLE_FOR_MSM_TYPE = ["dns"]
    MANDATORY_CFG_FIELDS = []
    OPTIONAL_CFG_FIELDS = []

    def __init__(self, cfg, expres):
        ExpResCriterion_DNSBased.__init__(self, cfg, expres)

        dns_rcode = self._enforce_list("dns_rcode", str)

        self.dns_rcode = set()
        for rcode in dns_rcode:
            if rcode not in ParsedResult_DNSHeader.DNS_RCODES:
                raise ConfigError(
                    "Invalid rcode: {}. Must be one of {}.".format(
                        rcode, ", ".join(ParsedResult_DNSHeader.DNS_RCODES)
                    )
                )
            self.dns_rcode.add(rcode)

    def __str__(self):
        return "DNS rcodes: {}".format(
            ", ".join(sorted(self.dns_rcode))
        )

    def prepare_response(self, result, response):
        res = ParsedResult_DNSHeader(self.expres.monitor, result, response)
        self.response_rcode = res.rcode

    def response_matches(self, response):
        response_rcode = self.response_rcode

        logger.debug(
            "  verifying if response's rcode ({}) is one of "
            "the expected ones ({})...".format(
                response_rcode,
                ", ".join(self.dns_rcode)
            )
        )
        if response_rcode not in self.dns_rcode:
            return False

        return True class ExpResCriterion_EDNS(ExpResCriterion_DNSBased):
    """Criterion: edns

    Verify EDNS extension of DNS responses received by probes.

    Available for: dns.

    `edns`: boolean indicating whether EDNS support is expected or not.

    `edns_size` (optional): minimum expected size.

    `edns_do` (optional): boolean indicating the expected presence of DO flag.

    `edns_nsid` (optional): list of expected NSID values.

    The optional parameters are taken into account only when `edns` is True.

    If `edns` is True, match when all the responses contain EDNS extension,
    otherwise when all the responses do not contain it.
    If `edns_size` is given, the size reported must be >= than the expected
    one.
    If `edns_do` is given, all the responses must have (or have not) the DO
    flag on.
    If `edns_nsid` is given, all the responses must contain and EDNS NSID
    option which falls within the list of values herein specified.

    Examples:

    edns: true

    edns: true
    edns_do: true

    edns: true
    edns_nsid:
    - "ods01.l.root-servers.org"
    - "kbp01.l.root-servers.org"
    """

    CRITERION_NAME = "edns"
    AVAILABLE_FOR_MSM_TYPE = ["dns"]
    MANDATORY_CFG_FIELDS = []
    OPTIONAL_CFG_FIELDS = ["edns_size", "edns_do", "edns_nsid"]

    def __init__(self, cfg, expres):
        ExpResCriterion_DNSBased.__init__(self, cfg, expres)

        self.edns = self._enforce_param("edns", bool)
        self.edns_size = self._enforce_param("edns_size", int)
        self.edns_do = self._enforce_param("edns_do", bool)
        self.edns_nsid = self._enforce_list("edns_nsid", str)

    def __str__(self):
        if self.edns:
            r = "EDNS supported"
            if self.edns_size is not None:
                r += "; size >= {}".format(self.edns_size)
            if self.edns_do is not None:
                if self.edns_do:
                    r += "; DO flag on"
                else:
                    r += "; DO flag off"
            if self.edns_nsid:
                r += "; NSID in "
                r += ", ".join(self.edns_nsid)
        else:
            r = "EDNS not supported"
        return r

    def prepare_response(self, result, response):
        res = ParsedResult_EDNS(self.expres.monitor, result, response)
        self.response_edns = res.edns
        self.response_edns_size = res.edns_size
        self.response_edns_do = res.edns_do
        self.response_edns_nsid = res.edns_nsid

    def response_matches(self, response):
        if self.response_edns and not self.edns:
            logger.debug(
                "  EDNS is supported while it shouldn't"
            )
            return False

        if not self.response_edns and self.edns:
            logger.debug(
                "  EDNS is not supported while it should be"
            )
            return False

        if self.edns and self.edns_size:
            if self.response_edns_size < self.edns_size:
                logger.debug(
                    "  EDNS udp size {} < {}".format(
                        self.response_edns_size, self.edns_size
                    )
                )
                return False

        if self.edns and self.edns_do is not None:
            if self.response_edns_do and not self.edns_do:
                logger.debug(
                    "  EDNS DO flag is on while it should be off"
                )
                return False

            if not self.response_edns_do and self.edns_do:
                logger.debug(
                    "  EDNS DO flag is off while is should be on"
                )
                return False

        if self.edns and self.edns_nsid:
            if not self.response_edns_nsid:
                logger.debug(
                    "  EDNS NSID option missing"
                )
                return False

            logger.debug(
                "  verifying if NSID {} is in {}...".format(
                    self.response_edns_nsid, ", ".join(self.edns_nsid)
                )
            )
            if self.response_edns_nsid not in self.edns_nsid:
                return False

        return True class ExpResCriterion_DNSAnswers(ExpResCriterion_DNSBased):
    """Criterion: dns_answers

    Verify if the responses received by a probe contain the expected
    records.

    Available for: dns.

    `dns_answers`: one or more sections where records are searched on. Must
    be one of "answers", "authorities", "additionals".

    Each section must contain a list of records.

    Match when all the responses received by a probe contain at least one
    record matching the expected ones in each of the given sections.

    Example:

    dns_answers:
        answers:
            - <record1>
            - <record2>
        authorities:
            - <record3>
            - <record4>
    """

    CRITERION_NAME = "dns_answers"
    AVAILABLE_FOR_MSM_TYPE = ["dns"]
    MANDATORY_CFG_FIELDS = []
    OPTIONAL_CFG_FIELDS = []

    def __init__(self, cfg, expres):
        ExpResCriterion_DNSBased.__init__(self, cfg, expres)

        self.dns_answers = self._enforce_param("dns_answers", dict)

        self.sections = []

        SECTIONS = ["answers", "authorities", "additionals"]
        for section in self.dns_answers:
            if section.lower() not in SECTIONS:
                raise ConfigError(
                    "Invalid section: {}. Must be one of {}".format(
                        section, ", ".join(SECTIONS)
                    )
                )

            if isinstance(self.dns_answers[section], list):
                section_cfg = self.dns_answers[section]
            elif isinstance(self.dns_answers[section], dict):
                section_cfg = [self.dns_answers[section]]
            else:
                raise ConfigError(
                    "Invalid section {}".format(section)
                )
            self.sections.append(
                ExpResCriterion_AnswersSection(
                    section, section_cfg
                )
            )

    def __str__(self):
        r = ""
        for section in self.sections:
            if r != "":
                r += "; "
            r += "{} section: {}".format(
                section.name,
                ", ".join(map(str, section.records))
            )
        return r

    def prepare_response(self, result, response):
        # This class doesn't behave like others and doesn't use
        # the prepare() method to parse results.
        # It implements checks directly in the response_matches()
        # method, that verifes every record directly.
        pass

    def response_matches(self, response):
        for section in self.sections:
            answer_section = getattr(response.abuf, section.name)
            if len(answer_section) == 0:
                logger.debug(
                    "  section {} not found in the response".format(
                        section.name
                    )
                )
                return False

            for record in section.records:
                for answer_record in answer_section:
                    if record.record_matches(answer_record):
                        return True

        return False
from .ExpResCriteriaCommon import ExpResCriterion_RTT, \
                                  ExpResCriterion_DstResponded, \
                                  ExpResCriterion_DstIP
from .ExpResCriteriaTraceroute import ExpResCriterion_DstAS, \
                                      ExpResCriterion_ASPath, \
                                      ExpResCriterion_UpstreamAS
from .ExpResCriteriaSSL import ExpResCriterion_CertFP
from .ExpResCriteriaDNS import ExpResCriterion_DNSFlags, \
                               ExpResCriterion_DNSRCode, \
                               ExpResCriterion_EDNS, \
                               ExpResCriterion_DNSAnswers
# Copyright (C) 2016 Pier Carlo Chiodi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.



CRITERIA_CLASSES_COMMON = [
    ExpResCriterion_RTT,
    ExpResCriterion_DstResponded,
    ExpResCriterion_DstIP
]

CRITERIA_CLASSES_TRACEROUTE = [
    ExpResCriterion_DstAS,
    ExpResCriterion_ASPath,

    ExpResCriterion_UpstreamAS,