summaryrefslogtreecommitdiff
path: root/project2
diff options
context:
space:
mode:
Diffstat (limited to 'project2')
-rw-r--r--project2/proj2_s4498062/.gitignore2
-rw-r--r--project2/proj2_s4498062/README.md45
-rw-r--r--project2/proj2_s4498062/cloogle.zone13
-rw-r--r--project2/proj2_s4498062/dns/__init__.py1
-rw-r--r--project2/proj2_s4498062/dns/cache.py119
-rw-r--r--project2/proj2_s4498062/dns/classes.py54
-rw-r--r--project2/proj2_s4498062/dns/domainname.py125
-rw-r--r--project2/proj2_s4498062/dns/message.py288
-rw-r--r--project2/proj2_s4498062/dns/rcodes.py82
-rw-r--r--project2/proj2_s4498062/dns/regexes.py46
-rw-r--r--project2/proj2_s4498062/dns/resolver.py153
-rw-r--r--project2/proj2_s4498062/dns/resource.py248
-rw-r--r--project2/proj2_s4498062/dns/server.py133
-rw-r--r--project2/proj2_s4498062/dns/types.py69
-rw-r--r--project2/proj2_s4498062/dns/zone.py54
-rwxr-xr-xproject2/proj2_s4498062/dns_client.py35
-rwxr-xr-xproject2/proj2_s4498062/dns_server.py38
-rwxr-xr-xproject2/proj2_s4498062/dns_tests.py88
18 files changed, 1593 insertions, 0 deletions
diff --git a/project2/proj2_s4498062/.gitignore b/project2/proj2_s4498062/.gitignore
new file mode 100644
index 0000000..0191c0c
--- /dev/null
+++ b/project2/proj2_s4498062/.gitignore
@@ -0,0 +1,2 @@
+*.pyc
+.dns.cache
diff --git a/project2/proj2_s4498062/README.md b/project2/proj2_s4498062/README.md
new file mode 100644
index 0000000..ce89073
--- /dev/null
+++ b/project2/proj2_s4498062/README.md
@@ -0,0 +1,45 @@
+# Project 2 Framework
+
+## Description
+
+This directory contains a framework for a DNS resolver and a recursive DNS server.
+The framework provides classes for manipulating DNS messages (and converting them to bytes).
+The framework also contains a few stubs which you need to implement.
+Most files contain pointers to the relevant sections of RFC 1034 and RFC 1035.
+These are not the only relevant sections though, and you might need to read more of the RFCs.
+
+It is probably a good idea to read RFC 1034 before proceeding.
+This RFC explains an overview of DNS and introduces some of the naming which is also used in the framework.
+
+## File structure
+
+* proj1_sn1_sn2
+ * dns
+ * cache.py: Contains a cache for the resolver. You have to implement this.
+ * classes.py: Enum of CLASSes and QCLASSes.
+ * domainname.py: Classes for reading and writing domain names as bytes.
+ * message.py: Classes for DNS messages.
+ * rcodes.py: Enum of RCODEs.
+ * resolver.py: Class for a DNS resolver. You have to implement this.
+ * resource.py: Classes for DNS resource records.
+ * server.py: Contains a DNS server. You have to implement this.
+ * types.py: Enum of TYPEs and QTYPEs.
+ * zone.py: name space zones. You have to implement this.
+ * dns_client.py: A simple DNS client, which serves as an example user of the resolver.
+ * dns_server.py: Code for starting the DNS server and parsing args.
+ * dns_tests.py: Tests for your resolver, cache and server. You have to implement this.
+
+## Implementation Hints and Tips
+
+You should start with implementing the resolver, which you need for the server.
+You will need message.py, resource.py, types.py, classes.py and rcodes.py.
+You can ignore the code for converting from and to bytes from these files if
+you want, but it might be useful (especially for debugging).
+
+After finishing the resolver you need to implement caching and the DNS server.
+You can implement these in any order that you like.
+I suggest implementing the recursive part (the resolving) of your DNS server, before implementing the management of the servers zone.
+
+Wireshark and dns_client.py are useful tools for debugging your resolver.
+Wireshark and nslookup are useful tools for debugging your server.
+
diff --git a/project2/proj2_s4498062/cloogle.zone b/project2/proj2_s4498062/cloogle.zone
new file mode 100644
index 0000000..807b3f8
--- /dev/null
+++ b/project2/proj2_s4498062/cloogle.zone
@@ -0,0 +1,13 @@
+; vim: ft=bindzone:
+; The Cloogle zone file
+
+cloogle.org. 3600000 NS ns1.p01.antagonist.nl
+cloogle.org. 3600000 NS ns2.p01.antagonist.net
+cloogle.org. 3600000 NS ns3.p01.antagonist.de
+
+cloogle.org. 3600000 A 84.22.111.158
+cloogle.org. 3600000 AAAA 2a02:2770:17:0:21a:4aff:fe1d:9a23
+
+www.cloogle.org. 3600000 CNAME cloogle.org
+
+; End of file
diff --git a/project2/proj2_s4498062/dns/__init__.py b/project2/proj2_s4498062/dns/__init__.py
new file mode 100644
index 0000000..992b090
--- /dev/null
+++ b/project2/proj2_s4498062/dns/__init__.py
@@ -0,0 +1 @@
+"""DNS tools"""
diff --git a/project2/proj2_s4498062/dns/cache.py b/project2/proj2_s4498062/dns/cache.py
new file mode 100644
index 0000000..a8b62be
--- /dev/null
+++ b/project2/proj2_s4498062/dns/cache.py
@@ -0,0 +1,119 @@
+"""A cache for resource records
+
+This module contains a class which implements a cache for DNS resource records,
+you still have to do most of the implementation. The module also provides a
+class and a function for converting ResourceRecords from and to JSON strings.
+It is highly recommended to use these.
+"""
+
+import json
+import time
+
+from dns.resource import ResourceRecord, RecordData
+from dns.types import Type
+from dns.classes import Class
+
+
+class ResourceEncoder(json.JSONEncoder):
+ """ Conver ResourceRecord to JSON
+
+ Usage:
+ string = json.dumps(records, cls=ResourceEncoder, indent=4)
+ """
+ def default(self, obj):
+ if isinstance(obj, ResourceRecord):
+ return {
+ "name": obj.name,
+ "type": Type.to_string(obj.type_),
+ "class": Class.to_string(obj.class_),
+ "ttl": obj.ttl,
+ "rdata": obj.rdata.data,
+ "timestamp": obj.timestamp
+ }
+ return json.JSONEncoder.default(self, obj)
+
+
+def resource_from_json(dct):
+ """ Convert JSON object to ResourceRecord
+
+ Usage:
+ records = json.loads(string, object_hook=resource_from_json)
+ """
+ name = dct["name"]
+ type_ = Type.from_string(dct["type"])
+ class_ = Class.from_string(dct["class"])
+ ttl = dct["ttl"]
+ rdata = RecordData.create(type_, dct["rdata"])
+ timestamp = dct["timestamp"]
+ return ResourceRecord(name, type_, class_, ttl, rdata, timestamp)
+
+
+class RecordCache(object):
+ """ Cache for ResourceRecords """
+
+ FILE = '.dns.cache'
+
+ def __init__(self, ttl):
+ """ Initialize the RecordCache
+
+ Args:
+ ttl (int): TTL of cached entries (if > 0)
+ """
+ self.records = []
+ self.read_cache_file()
+ self.ttl = ttl
+
+ def __del__(self):
+ self.write_cache_file()
+
+ def remove_old(self):
+ """Remove entries for which the TTL has expired"""
+ now = int(time.time())
+ for record in reversed(self.records):
+ if min(self.ttl, record.ttl) + record.timestamp < now:
+ self.records.remove(record)
+
+ def lookup(self, dname, type_, class_):
+ """ Lookup resource records in cache
+
+ Lookup for the resource records for a domain name with a specific type
+ and class.
+
+ Args:
+ dname (str): domain name
+ type_ (Type): type
+ class_ (Class): class
+ """
+ self.remove_old()
+ return [
+ r for r in self.records
+ if r.match(name=dname, type_=type_, class_=class_)]
+
+ def add_records_from(self, msg):
+ for record in msg.answers + msg.authorities + msg.additionals:
+ if record.type_ in [Type.A, Type.AAAA, Type.CNAME, Type.NS] and \
+ record.class_ == Class.IN:
+ self.add_record(record)
+
+ def add_record(self, record):
+ """ Add a new Record to the cache
+
+ Args:
+ record (ResourceRecord): the record added to the cache
+ """
+ self.records.append(record)
+
+ def read_cache_file(self):
+ """ Read the cache file from disk """
+ try:
+ with open(self.FILE, 'r') as jsonfile:
+ self.records = json.load(
+ jsonfile, object_hook=resource_from_json)
+ except IOError:
+ pass
+
+ def write_cache_file(self):
+ """ Write the cache file to disk """
+ self.remove_old()
+ with open(self.FILE, 'w') as jsonfile:
+ json.dump(self.records, jsonfile, cls=ResourceEncoder, indent=4)
diff --git a/project2/proj2_s4498062/dns/classes.py b/project2/proj2_s4498062/dns/classes.py
new file mode 100644
index 0000000..b6123cd
--- /dev/null
+++ b/project2/proj2_s4498062/dns/classes.py
@@ -0,0 +1,54 @@
+""" DNS CLASS and QCLASS values
+
+This module contains an Enum of CLASS and QCLASS values. The Enum also contains
+a method for converting values to strings. See sections 3.2.4 and 3.2.5 of RFC
+1035 for more information.
+"""
+
+
+class Class(object):
+ """ Enum of CLASS and QCLASS values
+
+ Usage:
+ >>> Class.IN
+ 1
+ >>> Class.ANY
+ 255
+ """
+
+ # pylint: disable=invalid-name
+ IN = 1
+ CS = 2
+ CH = 3
+ HS = 4
+ ANY = 255
+
+ by_string = {
+ "IN": IN,
+ "CS": CS,
+ "CH": CH,
+ "HS": HS,
+ "*": ANY
+ }
+
+ by_value = dict([(y, x) for x, y in by_string.items()])
+
+ @staticmethod
+ def to_string(class_):
+ """ Convert a Class to a string
+
+ Usage:
+ >>> Class.to_string(Class.IN)
+ 'IN'
+ """
+ return Class.by_value[class_]
+
+ @staticmethod
+ def from_string(string):
+ """ Convert a string to a Class
+
+ Usage:
+ >>> Class.from_string('IN')
+ 1
+ """
+ return Class.by_string[string]
diff --git a/project2/proj2_s4498062/dns/domainname.py b/project2/proj2_s4498062/dns/domainname.py
new file mode 100644
index 0000000..81b5f4c
--- /dev/null
+++ b/project2/proj2_s4498062/dns/domainname.py
@@ -0,0 +1,125 @@
+""" Parsing and composing domain names
+
+This module contains two classes for converting domain names to and from bytes.
+You won't have to use these classes. They're used internally in Message,
+Question, ResourceRecord and RecordData. You can read section 4.1.4 of RFC 1035
+if you want more info.
+"""
+
+import struct
+
+
+class Composer(object):
+ """ Converts a string representation of a domain name to bytes """
+
+ def __init__(self):
+ self.offsets = dict()
+
+ def to_bytes(self, offset, dnames):
+ """Convert each domain name in to bytes"""
+ result = b""
+ for dname in dnames:
+ # Split domain name into labels
+ labels = dname.split(".")
+
+ # Determine keys of subdomains in offset dict
+ keys = []
+ for label in reversed(labels):
+ name = label
+ if keys:
+ name += "." + keys[-1]
+ keys.append(name)
+ keys.reverse()
+
+ # Convert label to bytes
+ add_null = True
+ for j, label in enumerate(labels):
+ if keys[j] in self.offsets:
+ offset = self.offsets[keys[j]]
+ pointer = (3 << 14) + offset
+ result += struct.pack("!H", pointer)
+ add_null = False
+ offset += 2
+ break
+ else:
+ self.offsets[keys[j]] = offset
+ result += struct.pack("!B{}s".format(len(label)),
+ len(label),
+ label)
+ offset += 1 + len(label)
+
+ # Add null character at end
+ if add_null:
+ result += b"\x00"
+ offset += 1
+
+ return result
+
+
+class Parser(object):
+ """ Convert byte representations of domain names to strings """
+
+ def __init__(self):
+ self.labels = dict()
+
+ def from_bytes(self, packet, offset, num):
+ """ Convert domain name from bytes to string
+
+ Args:
+ packet (bytes): packet containing the domain name
+ offset (int): offset of domain name in packet
+ num (int): number of domain names to decode
+
+ Returns:
+ str, int
+ """
+
+ dnames = []
+
+ # Read the domain names
+ for _ in range(num):
+ # Read a new domain name
+ dname = ""
+ prev_offsets = []
+ done = False
+ while done is False:
+ # Read length of next label
+ llength = struct.unpack_from("!B", packet, offset)[0]
+
+ # Done reading domain when length is zero
+ if llength == 0:
+ offset += 1
+ break
+
+ # Compression label
+ elif (llength >> 6) == 3:
+ new_offset = offset + 2
+ target = struct.unpack_from("!H", packet, offset)[0]
+ target -= 3 << 14
+ label = self.labels[target]
+ done = True
+
+ # Normal label
+ else:
+ new_offset = offset + llength + 1
+ label = struct.unpack_from("{}s".format(llength),
+ packet, offset+1)[0]
+
+ # Add label to dictionary
+ self.labels[offset] = label
+ for prev_offset in prev_offsets:
+ self.labels[prev_offset] += "." + label
+ prev_offsets.append(offset)
+
+ # Update offset
+ offset = new_offset
+
+ # Append label to domain name
+ if len(dname) > 0:
+ dname += "."
+ dname += label
+
+ # Append domain name to list
+ dnames.append(dname)
+
+ return dnames, offset
diff --git a/project2/proj2_s4498062/dns/message.py b/project2/proj2_s4498062/dns/message.py
new file mode 100644
index 0000000..fd858b2
--- /dev/null
+++ b/project2/proj2_s4498062/dns/message.py
@@ -0,0 +1,288 @@
+""" DNS messages
+
+This module contains classes for DNS messages, their header section and
+question fields. See section 4 of RFC 1035 for more info.
+"""
+
+import struct
+
+from dns.domainname import Parser, Composer
+from dns.resource import ResourceRecord
+from dns.types import Type
+from dns.classes import Class
+
+
+class Message(object):
+ """ DNS message """
+
+ def __init__(
+ self, header, questions=None, answers=None, authorities=None,
+ additionals=None):
+ """ Create a new DNS message
+
+ Args:
+ header (Header): the header section
+ questions ([Question]): the question section
+ answers ([ResourceRecord]): the answer section
+ authorities ([ResourceRecord]): the authority section
+ additionals ([ResourceRecord]): the additional section
+ """
+ if questions is None:
+ questions = []
+ if answers is None:
+ answers = []
+ if authorities is None:
+ authorities = []
+ if additionals is None:
+ additionals = []
+
+ self.header = header
+ self.questions = questions
+ self.answers = answers
+ self.authorities = authorities
+ self.additionals = additionals
+
+ @property
+ def resources(self):
+ """ Getter for all resource records """
+ return self.answers + self.authorities + self.additionals
+
+ def to_bytes(self):
+ """ Convert Message to bytes """
+ composer = Composer()
+
+ # Add header
+ result = self.header.to_bytes()
+
+ # Add questions
+ for question in self.questions:
+ offset = len(result)
+ result += question.to_bytes(offset, composer)
+
+ # Add answers
+ for answer in self.answers:
+ offset = len(result)
+ result += answer.to_bytes(offset, composer)
+
+ # Add authorities
+ for authority in self.authorities:
+ offset = len(result)
+ result += authority.to_bytes(offset, composer)
+
+ # Add additionals
+ for additional in self.additionals:
+ offset = len(result)
+ result += additional.to_bytes(offset, composer)
+
+ return result
+
+ @classmethod
+ def from_bytes(cls, packet):
+ """ Create Message from bytes
+
+ Args:
+ packet (bytes): byte representation of the message
+ """
+ parser = Parser()
+
+ # Parse header
+ header, offset = Header.from_bytes(packet), 12
+
+ # Parse questions
+ questions = []
+ for _ in range(header.qd_count):
+ question, offset = Question.from_bytes(packet, offset, parser)
+ questions.append(question)
+
+ # Parse answers
+ answers = []
+ for _ in range(header.an_count):
+ answer, offset = ResourceRecord.from_bytes(packet, offset, parser)
+ answers.append(answer)
+
+ # Parse authorities
+ authorities = []
+ for _ in range(header.ns_count):
+ auth, offset = ResourceRecord.from_bytes(packet, offset, parser)
+ authorities.append(auth)
+
+ # Parse additionals
+ additionals = []
+ for _ in range(header.ar_count):
+ addit, offset = ResourceRecord.from_bytes(packet, offset, parser)
+ additionals.append(addit)
+
+ return cls(header, questions, answers, authorities, additionals)
+
+
+class Header(object):
+ """ The header section of a DNS message
+
+ Contains a number of properties which are accessible as normal member
+ variables.
+
+ See section 4.1.1 of RFC 1035 for their meaning.
+ """
+
+ # pylint: disable=missing-docstring, invalid-name
+
+ def __init__(self, ident, flags, qd_count, an_count, ns_count, ar_count):
+ """ Create a new Header object
+
+ Args:
+ ident (int): identifier
+ qd_count (int): number of entries in question section
+ an_count (int): number of entries in answer section
+ ns_count (int): number of entries in authority section
+ ar_count (int): number of entries in additional section
+ """
+ self.ident = ident
+ self._flags = flags
+ self.qd_count = qd_count
+ self.an_count = an_count
+ self.ns_count = ns_count
+ self.ar_count = ar_count
+
+ def to_bytes(self):
+ """ Convert header to bytes """
+ return struct.pack("!6H",
+ self.ident,
+ self._flags,
+ self.qd_count,
+ self.an_count,
+ self.ns_count,
+ self.ar_count)
+
+ @classmethod
+ def from_bytes(cls, packet):
+ """ Convert Header from bytes """
+ if len(packet) < 12:
+ raise ValueError("header is too short")
+ return cls(*struct.unpack_from("!6H", packet))
+
+ @property
+ def flags(self):
+ return self._flags
+ @flags.setter
+ def flags(self, value):
+ if value >= (1 << 16):
+ raise ValueError("value too big for flags")
+ self._flags = value
+
+ @property
+ def qr(self):
+ return self._flags & (1 << 15)
+ @qr.setter
+ def qr(self, value):
+ if value:
+ self._flags |= (1 << 15)
+ else:
+ self._flags &= ~(1 << 15)
+
+ @property
+ def opcode(self):
+ return (self._flags & (((1 << 4) - 1) << 11)) >> 11
+ @opcode.setter
+ def opcode(self, value):
+ if value > 0b1111:
+ raise ValueError("invalid opcode")
+ self._flags &= ~(((1 << 4) - 1) << 11)
+ self._flags |= value << 11
+
+ @property
+ def aa(self):
+ return self._flags & (1 << 10)
+ @aa.setter
+ def aa(self, value):
+ if value:
+ self._flags |= (1 << 10)
+ else:
+ self._flags &= ~(1 << 10)
+
+ @property
+ def tc(self):
+ return self._flags & (1 << 9)
+ @tc.setter
+ def tc(self, value):
+ if value:
+ self._flags |= (1 << 9)
+ else:
+ self._flags &= ~(1 << 9)
+
+ @property
+ def rd(self):
+ return self._flags & (1 << 8)
+ @rd.setter
+ def rd(self, value):
+ if value:
+ self._flags |= (1 << 8)
+ else:
+ self._flags &= ~(1 << 8)
+
+ @property
+ def ra(self):
+ return self._flags & (1 << 7)
+ @ra.setter
+ def ra(self, value):
+ if value:
+ self._flags |= (1 << 7)
+ else:
+ self._flags &= ~(1 << 7)
+
+ @property
+ def z(self):
+ return self._flags & (((1 << 3) - 1) << 4) >> 4
+ @z.setter
+ def z(self, value):
+ if value:
+ raise ValueError("non-zero zero flag")
+
+ @property
+ def rcode(self):
+ return self._flags & ((1 << 4) - 1)
+ @rcode.setter
+ def rcode(self, value):
+ if value > 0b1111:
+ raise ValueError("invalid return code")
+ self._flags &= ~((1 << 4) - 1)
+ self._flags |= value
+
+
+class Question(object):
+ """ An entry in the question section.
+
+ See section 4.1.2 of RFC 1035 for more info.
+ """
+
+ def __init__(self, qname, qtype, qclass):
+ """ Create a new entry in the question section
+
+ Args:
+ qname (str): QNAME
+ qtype (Type): QTYPE
+ qclass (Class): QCLASS
+ """
+ self.qname = qname
+ self.qtype = qtype
+ self.qclass = qclass
+
+ def to_bytes(self, offset, composer):
+ """ Convert Question to bytes """
+ bqname = composer.to_bytes(offset, [self.qname])
+ bqtype = struct.pack("!H", self.qtype)
+ bqclass = struct.pack("!H", self.qclass)
+ return bqname + bqtype + bqclass
+
+ @classmethod
+ def from_bytes(cls, packet, offset, parser):
+ """ Convert Question from bytes """
+ qnames, offset = parser.from_bytes(packet, offset, 1)
+ qname = qnames[0]
+ qtype, qclass = struct.unpack_from("!2H", packet, offset)
+ return cls(qname, qtype, qclass), offset + 4
+
+ def __repr__(self):
+ return '{} {} {}'.format(
+ self.qname,
+ Type.to_string(self.qtype),
+ Class.to_string(self.qclass))
diff --git a/project2/proj2_s4498062/dns/rcodes.py b/project2/proj2_s4498062/dns/rcodes.py
new file mode 100644
index 0000000..4f40621
--- /dev/null
+++ b/project2/proj2_s4498062/dns/rcodes.py
@@ -0,0 +1,82 @@
+#!/usr/bin/env python2
+
+""" DNS RCODE values
+
+This module contains an Enum of RCODE values. See section 4.1.4 of RFC 1035 for
+more info.
+"""
+
+
+class RCode(object):
+ """ Enum of RCODE values
+
+ Usage:
+ >>> NoError
+ 0
+ >>> NXDomain
+ 3
+ """
+
+ NoError = 0
+ FormErr = 1
+ ServFail = 2
+ NXDomain = 3
+ NotImp = 4
+ Refused = 5
+ YXDomain = 6
+ YXRRSet = 7
+ NXRRSet = 8
+ NotAuth = 9
+ NotZone = 10
+ BADVERS = 16
+ BADSIG = 16
+ BADKEY = 17
+ BADTIME = 18
+ BADMODE = 19
+ BADNAME = 20
+ BADALG = 21
+ BADTRUNC = 22
+
+ by_string = {
+ "NoError": NoError,
+ "FormErr": FormErr,
+ "ServFail": ServFail,
+ "NXDomain": NXDomain,
+ "NotImp": NotImp,
+ "Refused": Refused,
+ "YXDomain": YXDomain,
+ "YXRRSet": YXRRSet,
+ "NXRRSet": NXRRSet,
+ "NotAuth": NotAuth,
+ "NotZone": NotZone,
+ "BADVERS": BADVERS,
+ "BADSIG": BADSIG,
+ "BADKEY": BADKEY,
+ "BADTIME": BADTIME,
+ "BADMODE": BADMODE,
+ "BADNAME": BADNAME,
+ "BADALG": BADALG,
+ "BADTRUNC": BADTRUNC
+ }
+
+ by_value = dict([(y, x) for x, y in by_string.items()])
+
+ @staticmethod
+ def to_string(rcode):
+ """ Convert an RCode to a string
+
+ Usage:
+ >>> RCode.to_string(RCode.NoError)
+ 'NoError'
+ """
+ return RCode.by_value[rcode]
+
+ @staticmethod
+ def from_string(string):
+ """ Convert a string to an RCode
+
+ Usage:
+ >>> RCode.from_string('NoError')
+ 0
+ """
+ return RCode.by_string[string]
diff --git a/project2/proj2_s4498062/dns/regexes.py b/project2/proj2_s4498062/dns/regexes.py
new file mode 100644
index 0000000..ffa1770
--- /dev/null
+++ b/project2/proj2_s4498062/dns/regexes.py
@@ -0,0 +1,46 @@
+"""Regexes used in the DNS protocol"""
+
+
+def grpm(regex):
+ """Make a matching group"""
+ return grp(regex, matching=True)
+
+
+def grp(regex, matching=False):
+ """Make a group"""
+ return r'(' + (r'' if matching else r'?:') + regex + r')'
+
+
+def opt(regex):
+ """Make an optional group"""
+ return grp(grp(regex) + r'?')
+
+
+def regex_opt_r(*regexes):
+ """Make a group that matches one of the given regexes"""
+ return grp(r'|'.join(regexes))
+
+
+DIGIT = r'\d'
+LETTER = r'[a-zA-Z]'
+LETDIG = grp(regex_opt_r(DIGIT, LETTER))
+LETDIGHYP = grp(regex_opt_r(LETDIG, r'-'))
+LDHSTR = grp(LETDIGHYP + r'+')
+LABEL = grp(LETTER + opt(opt(LDHSTR) + LETDIG))
+SUBDOMAIN = grp(grpm(grp(LABEL + r'\.') + r'*') + grpm(LABEL))
+DOMAIN = regex_opt_r(SUBDOMAIN, r' ')
+
+# Fast, non-matching domain
+_DOMAIN = r'(?:(?:[a-zA-Z](?:[a-zA-Z\d\-]*[a-zA-Z\d])?\.)*)?'
+
+IP = r'(?:(?:\d{1,3}\.){3}\d{1,3})'
+
+CLASS = regex_opt_r(r'IN', r'CH')
+TYPE = regex_opt_r(r'A', r'CNAME', r'HINFO', r'MX', r'NS', r'PTR', r'SOA')
+TTL = r'\d+'
+RDATA = r'.*?'
+RR = regex_opt_r(
+ grp(grpm(TTL) + r'\s+' + opt(grpm(CLASS))),
+ grp(grpm(CLASS) + r'\s+' + opt(grpm(TTL)))
+ ) + r'\s+' + grpm(TYPE) + r'\s+' + grpm(RDATA) + r'\s*(?:(?<!\\);)?\s*$'
+ZONE_LINE_DOMAIN = r'^' + grpm(_DOMAIN) + r'\s+' + RR
diff --git a/project2/proj2_s4498062/dns/resolver.py b/project2/proj2_s4498062/dns/resolver.py
new file mode 100644
index 0000000..4c09681
--- /dev/null
+++ b/project2/proj2_s4498062/dns/resolver.py
@@ -0,0 +1,153 @@
+#!/usr/bin/env python2
+
+""" DNS Resolver
+
+This module contains a class for resolving hostnames. You will have to implement
+things in this module. This resolver will be both used by the DNS client and the
+DNS server, but with a different list of servers.
+"""
+
+from random import randint
+import socket
+
+from dns.classes import Class
+from dns.types import Type
+
+from dns.cache import RecordCache
+from dns.message import Message, Question, Header
+
+
+class Resolver(object):
+ """ DNS resolver """
+
+ ROOT_SERVERS = [
+ '198.41.0.4',
+ '192.228.79.201',
+ '192.33.4.12',
+ '199.7.91.13',
+ '192.203.230.10',
+ '192.5.5.241',
+ '192.112.36.4',
+ '198.97.190.53',
+ '192.36.148.17',
+ '192.58.128.30',
+ '193.0.14.129',
+ '199.7.83.42',
+ '202.12.27.33'
+ ]
+
+ def __init__(self, caching, ttl, timeout=3):
+ """ Initialize the resolver
+
+ Args:
+ caching (bool): caching is enabled if True
+ ttl (int): ttl of cache entries (if > 0)
+ """
+ self.caching = caching
+ self.ttl = ttl
+ self.timeout = timeout
+
+ if self.caching:
+ self.cache = RecordCache(ttl)
+
+ def do_query(self, hint, hostname, type_, class_=Class.IN, caching=True):
+ """Do a query to a hint"""
+ if self.caching and caching:
+ records = self.cache.lookup(hostname, type_, class_)
+ if records != []:
+ return records
+
+ ident = randint(0, 65535)
+ header = Header(ident, 0, 1, 0, 0, 0)
+ header.qr = 0
+ header.opcode = 0
+ header.rd = 1
+ req = Message(header, [Question(hostname, type_, class_)])
+
+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ sock.settimeout(self.timeout)
+ sock.sendto(req.to_bytes(), (hint, 53))
+
+ try:
+ data = sock.recv(512)
+ resp = Message.from_bytes(data)
+ if resp.header.ident == ident:
+ if self.caching and caching:
+ self.cache.add_records_from(resp)
+ return resp.answers + resp.authorities + resp.additionals
+ except socket.timeout:
+ pass
+ return []
+
+ def do_query_to_multiple(
+ self, hints, hostname, type_, class_=Class.IN, caching=True):
+ """Do a query to multiple hints, return the remaining hints"""
+ while hints != []:
+ hint = hints.pop()
+ response = self.do_query(hint, hostname, type_, class_, caching)
+ if response is not None:
+ return hints, response
+ return [], []
+
+ def gethostbyname(self, hostname):
+ """ Translate a host name to IPv4 address.
+
+ Args:
+ hostname (str): the hostname to resolve
+
+ Returns:
+ (str, [str], [str]): (hostname, aliaslist, ipaddrlist)
+ """
+ if self.caching:
+ addrs = self.cache.lookup(hostname, Type.A, Class.IN)
+ cnames = self.cache.lookup(hostname, Type.CNAME, Class.IN)
+ if addrs != []:
+ return hostname, \
+ [r.rdata.data for r in cnames], \
+ [r.rdata.data for r in addrs]
+ for cname in cnames:
+ cname, aliases, addrs = self.gethostbyname(cname.rdata.data)
+ if addrs != []:
+ return str(cname), aliases, addrs
+
+ if hostname == '':
+ return hostname, [], []
+
+ hints = self.ROOT_SERVERS[:]
+ aliases = []
+ while hints != []:
+ hints, info = self.do_query_to_multiple(hints, hostname, Type.A)
+
+ aliases += [
+ r.rdata.data for r in info
+ if r.match(type_=Type.CNAME, class_=Class.IN, name=hostname)]
+
+ # Case 1: answer
+ ips = [
+ r.rdata.data for r in info
+ if r.match(type_=Type.A, class_=Class.IN, name=hostname)]
+ if ips != []:
+ return hostname, aliases, ips
+
+ # Case 2: name servers
+ auths = [
+ r.rdata.data for r in info
+ if r.match(type_=Type.NS, class_=Class.IN)]
+ ips = [
+ add.rdata.data for ns in auths for add in info
+ if add.match(name=ns, type_=Type.A)]
+ if ips != []:
+ hints += ips
+ continue
+ if auths != []:
+ auths = [h for a in auths for h in self.gethostbyname(a)[2]]
+ hints += auths
+ continue
+
+ # Case 3: aliases
+ for alias in aliases:
+ _, extra_aliases, alias_addresses = self.gethostbyname(alias)
+ if alias_addresses != []:
+ return hostname, aliases + extra_aliases, alias_addresses
+
+ return hostname, aliases, []
diff --git a/project2/proj2_s4498062/dns/resource.py b/project2/proj2_s4498062/dns/resource.py
new file mode 100644
index 0000000..e552c22
--- /dev/null
+++ b/project2/proj2_s4498062/dns/resource.py
@@ -0,0 +1,248 @@
+""" A DNS resource record
+
+This class contains classes for DNS resource records and record data. This
+module is fully implemented. You will have this module in the implementation
+of your resolver and server.
+"""
+
+import socket
+import struct
+import time
+
+from dns.types import Type
+
+
+class ResourceRecord(object):
+ """ DNS resource record """
+ def __init__(self, name, type_, class_, ttl, rdata, timestamp=None):
+ """ Create a new resource record
+
+ Args:
+ name (str): domain name
+ type_ (Type): the type
+ class_ (Class): the class
+ rdata (RecordData): the record data
+ """
+ self.name = name
+ self.type_ = type_
+ self.class_ = class_
+ self.ttl = ttl
+ self.rdata = rdata
+ self.timestamp = int(timestamp or time.time())
+
+ def match(self, name=None, type_=None, class_=None, ttl=None):
+ """Check if the record matches properties"""
+ return (name is None or self.name == name) and \
+ (type_ is None or self.type_ == type_) and \
+ (class_ is None or self.class_ == class_) and \
+ (ttl is None or self.ttl == ttl)
+
+ def to_bytes(self, offset, composer):
+ """ Convert ResourceRecord to bytes """
+ record = composer.to_bytes(offset, [self.name])
+ record += struct.pack("!HHI", self.type_, self.class_, self.ttl)
+ offset += len(record) + 2
+ rdata = self.rdata.to_bytes(offset, composer)
+ record += struct.pack("!H", len(rdata)) + rdata
+ return record
+
+ @classmethod
+ def from_bytes(cls, packet, offset, parser):
+ """ Convert ResourceRecord from bytes """
+ names, offset = parser.from_bytes(packet, offset, 1)
+ name = names[0]
+ type_, class_, ttl, rdlen = struct.unpack_from("!HHIH", packet, offset)
+ offset += 10
+ rdata = RecordData.from_bytes(type_, packet, offset, rdlen, parser)
+ offset += rdlen
+ return cls(name, type_, class_, ttl, rdata), offset
+
+
+class RecordData(object):
+ """ Record Data """
+
+ def __init__(self, data):
+ """ Initialize the record data
+
+ Args:
+ data (str): data
+ """
+ self.data = data
+
+ @staticmethod
+ def create(type_, data):
+ """ Create a RecordData object from bytes
+
+ Args:
+ type_ (Type): type
+ packet (bytes): packet
+ offset (int): offset in message
+ rdlength (int): length of rdata
+ parser (int): domain name parser
+ """
+ classdict = {
+ Type.A: ARecordData,
+ Type.CNAME: CNAMERecordData,
+ Type.NS: NSRecordData,
+ Type.AAAA: AAAARecordData
+ }
+ if type_ in classdict:
+ return classdict[type_](data)
+ else:
+ return GenericRecordData(data)
+
+ @staticmethod
+ def from_bytes(type_, packet, offset, rdlength, parser):
+ """ Create a RecordData object from bytes
+
+ Args:
+ type_ (Type): type
+ packet (bytes): packet
+ offset (int): offset in message
+ rdlength (int): length of rdata
+ parser (int): domain name parser
+ """
+ classdict = {
+ Type.A: ARecordData,
+ Type.CNAME: CNAMERecordData,
+ Type.NS: NSRecordData,
+ Type.AAAA: AAAARecordData
+ }
+ if type_ in classdict:
+ return classdict[type_].from_bytes(
+ packet, offset, rdlength, parser)
+ else:
+ return GenericRecordData.from_bytes(
+ packet, offset, rdlength, parser)
+
+
+class ARecordData(RecordData):
+ """Data of an A record"""
+
+ def to_bytes(self, offset, composer):
+ """ Convert to bytes
+
+ Args:
+ offset (int): offset in message
+ composer (Composer): domain name composer
+ """
+ return socket.inet_aton(self.data)
+
+ @classmethod
+ def from_bytes(cls, packet, offset, rdlength, parser):
+ """ Create a RecordData object from bytes
+
+ Args:
+ packet (bytes): packet
+ offset (int): offset in message
+ rdlength (int): length of rdata
+ parser (int): domain name parser
+ """
+ data = socket.inet_ntoa(packet[offset:offset+4])
+ return cls(data)
+
+
+class CNAMERecordData(RecordData):
+ """Data of a CNAME record"""
+
+ def to_bytes(self, offset, composer):
+ """ Convert to bytes
+
+ Args:
+ offset (int): offset in message
+ composer (Composer): domain name composer
+ """
+ return composer.to_bytes(offset, [self.data])
+
+ @classmethod
+ def from_bytes(cls, packet, offset, rdlength, parser):
+ """ Create a RecordData object from bytes
+
+ Args:
+ packet (bytes): packet
+ offset (int): offset in message
+ rdlength (int): length of rdata
+ parser (int): domain name parser
+ """
+ names, offset = parser.from_bytes(packet, offset, 1)
+ data = names[0]
+ return cls(data)
+
+
+class NSRecordData(RecordData):
+ """Data of an NS record"""
+
+ def to_bytes(self, offset, composer):
+ """ Convert to bytes
+
+ Args:
+ offset (int): offset in message
+ composer (Composer): domain name composer
+ """
+ return composer.to_bytes(offset, [self.data])
+
+ @classmethod
+ def from_bytes(cls, packet, offset, rdlength, parser):
+ """ Create a RecordData object from bytes
+
+ Args:
+ packet (bytes): packet
+ offset (int): offset in message
+ rdlength (int): length of rdata
+ parser (int): domain name parser
+ """
+ names, offset = parser.from_bytes(packet, offset, 1)
+ data = names[0]
+ return cls(data)
+
+
+class AAAARecordData(RecordData):
+ """Data of an AAAA record"""
+
+ def to_bytes(self, offset, composer):
+ """ Convert to bytes
+
+ Args:
+ offset (int): offset in message
+ composer (Composer): domain name composer
+ """
+ return socket.inet_pton(socket.AF_INET6, self.data)
+
+ @classmethod
+ def from_bytes(cls, packet, offset, rdlength, parser):
+ """ Create a RecordData object from bytes
+
+ Args:
+ packet (bytes): packet
+ offset (int): offset in message
+ rdlength (int): length of rdata
+ parser (int): domain name parser
+ """
+ data = socket.inet_ntop(socket.AF_INET6, packet[offset:offset+16])
+ return cls(data)
+
+
+class GenericRecordData(RecordData):
+ """Data of a generic record"""
+
+ def to_bytes(self, offset, composer):
+ """ Convert to bytes
+
+ Args:
+ offset (int): offset in message
+ composer (Composer): domain name composer
+ """
+ return self.data
+
+ @classmethod
+ def from_bytes(cls, packet, offset, rdlength, parser):
+ """ Create a RecordData object from bytes
+
+ Args:
+ packet (bytes): packet
+ offset (int): offset in message
+ rdlength (int): length of rdata
+ parser (int): domain name parser
+ """
+ data = packet[offset:offset+rdlength]
+ return cls(data)
diff --git a/project2/proj2_s4498062/dns/server.py b/project2/proj2_s4498062/dns/server.py
new file mode 100644
index 0000000..10cad8b
--- /dev/null
+++ b/project2/proj2_s4498062/dns/server.py
@@ -0,0 +1,133 @@
+""" A recursive DNS server
+
+This module provides a recursive DNS server. You will have to implement this
+server using the algorithm described in section 4.3.2 of RFC 1034.
+"""
+import re
+import socket
+from threading import Thread
+
+import dns.regexes as rgx
+from dns.classes import Class
+from dns.types import Type
+from dns.message import Header, Message
+from dns.resolver import Resolver
+from dns.resource import \
+ ResourceRecord, ARecordData, NSRecordData, CNAMERecordData
+
+
+class RequestHandler(Thread):
+ """ A handler for requests to the DNS server """
+
+ def __init__(self, skt, ttl, data, addr, zone):
+ # pylint: disable=too-many-arguments
+ """ Initialize the handler thread """
+ super(RequestHandler, self).__init__()
+ self.daemon = True
+ self.skt = skt
+ self.ttl = ttl
+ self.data = data
+ self.addr = addr
+ self.zone = zone
+
+ def run(self):
+ """ Run the handler thread """
+ resolver = Resolver(True, self.ttl)
+
+ request = Message.from_bytes(self.data)
+ answs, adds, auths = [], [], []
+
+ for req in request.questions:
+ rrs = [
+ r for r in self.zone
+ if r.match(type_=req.qtype, class_=req.qclass, name=req.qname)]
+ if rrs != []:
+ auths += rrs
+ elif req.qtype in [Type.A, Type.CNAME] and req.qclass == Class.IN:
+ name, cnames, addrs = resolver.gethostbyname(req.qname)
+ if name != req.qname:
+ answs.append(ResourceRecord(
+ str(req.qname), Type.CNAME, Class.IN, self.ttl,
+ CNAMERecordData(str(name))))
+ # pylint: disable=bad-continuation
+ addrs = [ResourceRecord(
+ name, Type.A, Class.IN, self.ttl,
+ ARecordData(data))
+ for data in addrs]
+ cnames = [ResourceRecord(
+ name, Type.CNAME, Class.IN, self.ttl,
+ CNAMERecordData(data))
+ for data in cnames]
+ if req.qtype == Type.A:
+ answs += addrs + cnames
+ if req.qtype == Type.CNAME:
+ answs += cnames
+
+ header = Header(
+ request.header.ident, 0, 0, len(answs), len(auths), len(adds))
+ response = Message(header, None, answs, auths, adds)
+
+ self.skt.sendto(response.to_bytes(), self.addr)
+
+
+class Server(object):
+ """ A recursive DNS server """
+
+ def __init__(self, port, caching, ttl):
+ """ Initialize the server
+
+ Args:
+ port (int): port that server is listening on
+ caching (bool): server uses resolver with caching if true
+ ttl (int): ttl for records (if > 0) of cache
+ """
+ self.caching = caching
+ self.ttl = ttl
+ self.port = port
+ self.done = False
+ self.zone = []
+
+ self.skt = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+
+ def serve(self):
+ """ Start serving request """
+ self.skt.bind(('localhost', self.port))
+ while not self.done:
+ data, addr = self.skt.recvfrom(512)
+ reqh = RequestHandler(
+ self.skt, self.ttl, data, addr, zone=self.zone)
+ reqh.start()
+
+ def shutdown(self):
+ """ Shutdown the server """
+ self.skt.close()
+ self.done = True
+
+ def parse_zone_file(self, fname):
+ """Parse a zone file
+
+ Will crash if the zone file has incorrect syntax.
+ """
+ with open(fname) as zonef:
+ zone = zonef.read()
+ ttl, class_ = 3600000, Class.IN
+ for match in re.finditer(rgx.ZONE_LINE_DOMAIN, zone, re.MULTILINE):
+ match = match.groups()
+ name = match[0][:-1]
+ ttl = int(match[1] or match[4] or ttl * 1000) / 1000
+ class_ = Class.from_string(
+ match[2] or match[3] or Class.to_string(class_))
+ type_ = Type.from_string(match[5])
+ data = match[6]
+
+ if type_ == Type.A:
+ cls = ARecordData
+ elif type_ == Type.NS:
+ cls = NSRecordData
+ elif type_ == Type.CNAME:
+ cls = CNAMERecordData
+ else:
+ continue
+
+ record = ResourceRecord(name, type_, class_, ttl, cls(data))
+ self.zone.append(record)
diff --git a/project2/proj2_s4498062/dns/types.py b/project2/proj2_s4498062/dns/types.py
new file mode 100644
index 0000000..494232c
--- /dev/null
+++ b/project2/proj2_s4498062/dns/types.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python2
+
+""" DNS TYPE and QTYPE values
+
+This module contains an Enum for TYPE and QTYPE values. This Enum also contains
+a method for converting Enum values to strings. See sections 3.2.2 and 3.2.3 of
+RFC 1035 for more information.
+"""
+
+
+class Type(object):
+ """ DNS TYPE and QTYPE
+
+ Usage:
+ >>> Type.A
+ 1
+ >>> Type.CNAME
+ 5
+ """
+ # pylint: disable=invalid-name
+ A = 1
+ NS = 2
+ CNAME = 5
+ SOA = 6
+ WKS = 11
+ PTR = 12
+ HINFO = 13
+ MINFO = 14
+ MX = 15
+ TXT = 16
+ AAAA = 28
+ ANY = 255
+
+ by_string = {
+ "A": A,
+ "NS": NS,
+ "CNAME": CNAME,
+ "SOA": SOA,
+ "WKS": WKS,
+ "PTR": PTR,
+ "HINFO": HINFO,
+ "MINFO": MINFO,
+ "MX": MX,
+ "TXT": TXT,
+ "AAAA": AAAA,
+ "*": ANY
+ }
+
+ by_value = dict([(y, x) for x, y in by_string.items()])
+
+ @staticmethod
+ def to_string(type_):
+ """ Convert a Type to a string
+
+ Usage:
+ >>> Type.to_string(Type.A)
+ 'A'
+ """
+ return Type.by_value[type_]
+
+ @staticmethod
+ def from_string(string):
+ """ Convert a string to a Type
+
+ Usage:
+ >>> Type.from_string('CNAME')
+ 5
+ """
+ return Type.by_string[string]
diff --git a/project2/proj2_s4498062/dns/zone.py b/project2/proj2_s4498062/dns/zone.py
new file mode 100644
index 0000000..8ada3db
--- /dev/null
+++ b/project2/proj2_s4498062/dns/zone.py
@@ -0,0 +1,54 @@
+#!/usr/bin/env python2
+
+""" Zones of domain name space
+
+See section 6.1.2 of RFC 1035 and section 4.2 of RFC 1034.
+Instead of tree structures we simply use dictionaries from domain names to
+zones or record sets.
+
+These classes are merely a suggestion, feel free to use something else.
+"""
+
+
+class Catalog(object):
+ """ A catalog of zones """
+
+ def __init__(self):
+ """ Initialize the catalog """
+ self.zones = {}
+
+ def add_zone(self, name, zone):
+ """ Add a new zone to the catalog
+
+ Args:
+ name (str): root domain name
+ zone (Zone): zone
+ """
+ self.zones[name] = zone
+
+
+class Zone(object):
+ """ A zone in the domain name space """
+
+ def __init__(self):
+ """ Initialize the Zone """
+ self.records = {}
+
+ def add_node(self, name, record_set):
+ """ Add a record set to the zone
+
+ Args:
+ name (str): domain name
+ record_set ([ResourceRecord]): resource records
+ """
+ self.records[name] = record_set
+
+ def read_master_file(self, filename):
+ """ Read the zone from a master file
+
+ See section 5 of RFC 1035.
+
+ Args:
+ filename (str): the filename of the master file
+ """
+ pass
diff --git a/project2/proj2_s4498062/dns_client.py b/project2/proj2_s4498062/dns_client.py
new file mode 100755
index 0000000..0f5e50f
--- /dev/null
+++ b/project2/proj2_s4498062/dns_client.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python2
+
+""" Simple DNS client
+
+A simple example of a client using the DNS resolver.
+"""
+
+import dns.resolver
+
+
+def main():
+ """DNS client"""
+ # Parse arguments
+ import argparse
+ parser = argparse.ArgumentParser(description="DNS Client")
+ parser.add_argument("hostname", help="hostname to resolve")
+ parser.add_argument(
+ "-c", "--caching", action="store_true",
+ help="Enable caching")
+ parser.add_argument(
+ "-t", "--ttl", metavar="time", type=int, default=0,
+ help="TTL value of cached entries")
+ args = parser.parse_args()
+
+ # Resolve hostname
+ resolver = dns.resolver.Resolver(args.caching, args.ttl)
+ hostname, aliases, addresses = resolver.gethostbyname(args.hostname)
+
+ # Print output
+ print hostname
+ print aliases
+ print addresses
+
+if __name__ == "__main__":
+ main()
diff --git a/project2/proj2_s4498062/dns_server.py b/project2/proj2_s4498062/dns_server.py
new file mode 100755
index 0000000..3bdd04d
--- /dev/null
+++ b/project2/proj2_s4498062/dns_server.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python2
+
+""" DNS server
+
+This script contains the code for starting a DNS server.
+"""
+
+import dns.server
+
+
+def main():
+ """DNS server"""
+ # Parse arguments
+ import argparse
+ parser = argparse.ArgumentParser(description="DNS Server")
+ parser.add_argument(
+ "-c", "--caching", action="store_true",
+ help="Enable caching")
+ parser.add_argument(
+ "-t", "--ttl", metavar="time", type=int, default=0,
+ help="TTL value of cached entries (if > 0)")
+ parser.add_argument(
+ "-p", "--port", type=int, default=5300,
+ help="Port which server listens on")
+ args = parser.parse_args()
+
+ # Start server
+ server = dns.server.Server(args.port, args.caching, args.ttl)
+ server.parse_zone_file('cloogle.zone')
+
+ try:
+ server.serve()
+ except KeyboardInterrupt:
+ server.shutdown()
+ print
+
+if __name__ == "__main__":
+ main()
diff --git a/project2/proj2_s4498062/dns_tests.py b/project2/proj2_s4498062/dns_tests.py
new file mode 100755
index 0000000..4a054b6
--- /dev/null
+++ b/project2/proj2_s4498062/dns_tests.py
@@ -0,0 +1,88 @@
+#!/usr/bin/env python2
+""" Tests for the DNS resolver and server """
+
+import sys
+import time
+import unittest
+
+from dns.cache import RecordCache
+from dns.classes import Class
+from dns.resolver import Resolver
+from dns.resource import ResourceRecord, CNAMERecordData
+from dns.types import Type
+
+portnr = 5300
+server = '127.0.0.1'
+
+class TestResolver(unittest.TestCase):
+ """Test cases for the resolver with caching disabled"""
+
+ def setUp(self):
+ self.resolv = Resolver(False, 0)
+
+ def test_solve(self):
+ """Test solving some FQDN"""
+ host, aliases, addrs = self.resolv.gethostbyname('camilstaps.nl')
+ self.assertEqual(host, 'camilstaps.nl')
+ self.assertEqual(aliases, [])
+ self.assertEqual(addrs, ['84.22.111.158'])
+
+ def test_nonexistant(self):
+ """Test solving a nonexistant FQDN"""
+ host, aliases, addrs = self.resolv.gethostbyname('nothing.ru.nl')
+ self.assertEqual(host, 'nothing.ru.nl')
+ self.assertEqual(aliases, [])
+ self.assertEqual(addrs, [])
+
+
+class TestResolverCache(unittest.TestCase):
+ """Test cases for the resolver with caching enabled"""
+
+ TTL = 3
+
+ def setup_resolver(self):
+ """Setup a resolver with an invalid cache"""
+ self.resolv = Resolver(True, self.TTL)
+ self.cache = RecordCache(self.TTL)
+ self.cache.add_record(ResourceRecord(
+ 'nothing.ru.nl', Type.CNAME, Class.IN, self.TTL,
+ CNAMERecordData('camilstaps.nl')))
+ self.resolv.cache = self.cache
+
+ def test_solve_invalid(self):
+ """Test solving an invalid cached FQDN"""
+ self.setup_resolver()
+ host, aliases, addrs = self.resolv.gethostbyname('nothing.ru.nl')
+ self.assertEqual(host, 'camilstaps.nl')
+ self.assertEqual(aliases, [])
+ self.assertEqual(addrs, ['84.22.111.158'])
+
+ def test_solve_invalid_after_expiration(self):
+ """Test solving an invalid cached FQDN after TTL expiration"""
+ self.setup_resolver()
+ time.sleep(self.TTL + 1)
+ host, aliases, addrs = self.resolv.gethostbyname('nothing.ru.nl')
+ self.assertEqual(host, 'nothing.ru.nl')
+ self.assertEqual(aliases, [])
+ self.assertEqual(addrs, [])
+
+
+class TestServer(unittest.TestCase):
+ pass
+
+
+if __name__ == "__main__":
+ # Parse command line arguments
+ import argparse
+ parser = argparse.ArgumentParser(description="HTTP Tests")
+ parser.add_argument("-s", "--server", type=str, default="localhost")
+ parser.add_argument("-p", "--port", type=int, default=5300)
+ args, extra = parser.parse_known_args()
+ portnr = args.port
+ server = args.server
+
+ # Pass the extra arguments to unittest
+ sys.argv[1:] = extra
+
+ # Start test suite
+ unittest.main()