"""The censor's four filters. Three are given, and sni_filter is extra credit. Each filter looks at one record (see corpus.py for the format) and returns True to block it or False to let it through. It may read only the record and the Censor below: a real censor sees traffic, not the answer key. The four strategies read different parts of a record: DNS the name the client looked up IP the destination address SNI the server name in the TLS ClientHello keyword the payload, for any listed word Python references: str methods (lower, endswith, split): https://docs.python.org/3/library/stdtypes.html#string-methods the `in` operator on strings: https://docs.python.org/3/reference/expressions.html#membership-test-operations """ from dataclasses import dataclass, field @dataclass class Censor: """What the censor has decided to block. Given; experiments.py builds it.""" hosts: list # site names, such as "freeleaf-news.example" ips: set = field(default_factory=set) # addresses to drop, such as "203.0.113.10" keywords: list = field(default_factory=list) # words to look for in readable payloads def name_matches(name, hosts): """True when `name` is one of `hosts` or a subdomain of one. Blocking "freeleaf-news.example" should also block "www.freeleaf-news.example", but not "notfreeleaf-news.example". DNS names do not care about upper and lower case, and a name may end with a dot ("freeleaf-news.example.") and still mean the same site. `name` may be None; nothing matches None. """ if name is None: return False name = name.lower().rstrip(".") return any(name == h or name.endswith("." + h) for h in hosts) def dns_filter(record, censor): """Block when the name the client looked up is a blocked site.""" return name_matches(record["dns"], censor.hosts) def ip_filter(record, censor): """Block when the destination address is one the censor drops.""" return record["ip"] in censor.ips def sni_filter(record, censor): """Block when the TLS server name is a blocked site. Extra credit. `name_matches` above already does the matching, so this is a few lines once you see it. Leave it unwritten and part B prints one fewer strategy. """ raise NotImplementedError("sni_filter") def keyword_filter(record, censor): """Block when any keyword appears anywhere in the payload, in any mix of upper and lower case. Encrypted payloads are in the corpus too; think about what this filter can and cannot see in them. """ text = record["payload"].lower() return any(k.lower() in text for k in censor.keywords) FILTERS = {"DNS": dns_filter, "IP": ip_filter, "SNI": sni_filter, "keyword": keyword_filter}