diff --git a/twisted/names/dns.py b/twisted/names/dns.py index 321440f177f..47b530b736f 100644 --- a/twisted/names/dns.py +++ b/twisted/names/dns.py @@ -16,7 +16,8 @@ 'A', 'A6', 'AAAA', 'AFSDB', 'CNAME', 'DNAME', 'HINFO', 'MAILA', 'MAILB', 'MB', 'MD', 'MF', 'MG', 'MINFO', 'MR', 'MX', - 'NAPTR', 'NS', 'NULL', 'PTR', 'RP', 'SOA', 'SPF', 'SRV', 'TXT', 'WKS', + 'NAPTR', 'NS', 'NULL', 'OPT', 'PTR', 'RP', 'SOA', 'SPF', 'SRV', 'TXT', + 'WKS', 'ANY', 'CH', 'CS', 'HS', 'IN', @@ -43,9 +44,7 @@ # System imports -import warnings - -import struct, random, types, socket +import struct, random, socket from itertools import chain from io import BytesIO @@ -119,6 +118,7 @@ def randomSource(): NAPTR = 35 A6 = 38 DNAME = 39 +OPT = 41 SPF = 99 QUERY_TYPES = { @@ -148,6 +148,7 @@ def randomSource(): NAPTR: 'NAPTR', A6: 'A6', DNAME: 'DNAME', + OPT: 'OPT', SPF: 'SPF' } @@ -574,6 +575,247 @@ def __repr__(self): +@implementer(IEncodable) +class _OPTHeader(tputil.FancyStrMixin, tputil.FancyEqMixin, object): + """ + An OPT record header. + + @ivar name: The DNS name associated with this record. Since this + is a pseudo record, the name is always an L{Name} instance + with value b'', which represents the DNS root zone. This + attribute is a readonly property. + + @ivar type: The DNS record type. This is a fixed value of 41 + (C{dns.OPT} for OPT Record. This attribute is a readonly + property. + + @see: L{_OPTHeader.__init__} for documentation of other public + instance attributes. + + @see: L{https://tools.ietf.org/html/rfc6891#section-6.1.2} + + @since: 13.2 + """ + showAttributes = ( + ('name', lambda n: nativeString(n.name)), 'type', 'udpPayloadSize', + 'extendedRCODE', 'version', 'dnssecOK', 'options') + + compareAttributes = ( + 'name', 'type', 'udpPayloadSize', 'extendedRCODE', 'version', + 'dnssecOK', 'options') + + def __init__(self, udpPayloadSize=4096, extendedRCODE=0, version=0, + dnssecOK=False, options=None): + """ + @type udpPayloadSize: L{int} + @param payload: The number of octets of the largest UDP + payload that can be reassembled and delivered in the + requestor's network stack. + + @type extendedRCODE: L{int} + @param extendedRCODE: Forms the upper 8 bits of extended + 12-bit RCODE (together with the 4 bits defined in + [RFC1035]. Note that EXTENDED-RCODE value 0 indicates + that an unextended RCODE is in use (values 0 through 15). + + @type version: L{int} + @param version: Indicates the implementation level of the + setter. Full conformance with this specification is + indicated by version C{0}. + + @type dnssecOK: L{bool} + @param dnssecOK: DNSSEC OK bit as defined by [RFC3225]. + + @type options: L{list} + @param options: A L{list} of 0 or more L{_OPTVariableOption} + instances. + """ + self.udpPayloadSize = udpPayloadSize + self.extendedRCODE = extendedRCODE + self.version = version + self.dnssecOK = dnssecOK + + if options is None: + options = [] + self.options = options + + + @property + def name(self): + """ + A readonly property for accessing the C{name} attribute of + this record. + + @return: The DNS name associated with this record. Since this + is a pseudo record, the name is always an L{Name} instance + with value b'', which represents the DNS root zone. + """ + return Name(b'') + + + @property + def type(self): + """ + A readonly property for accessing the C{type} attribute of + this record. + + @return: The DNS record type. This is a fixed value of 41 + (C{dns.OPT} for OPT Record. + """ + return OPT + + + def encode(self, strio, compDict=None): + """ + Encode this L{_OPTHeader} instance to bytes. + + @type strio: L{file} + @param strio: the byte representation of this L{_OPTHeader} + will be written to this file. + + @type compDict: L{dict} or L{None} + @param compDict: A dictionary of backreference addresses that + have have already been written to this stream and that may + be used for DNS name compression. + """ + b = BytesIO() + for o in self.options: + o.encode(b) + optionBytes = b.getvalue() + + RRHeader( + name=self.name.name, + type=self.type, + cls=self.udpPayloadSize, + ttl=( + self.extendedRCODE << 24 + | self.version << 16 + | self.dnssecOK << 15), + payload=UnknownRecord(optionBytes) + ).encode(strio, compDict) + + + def decode(self, strio, length=None): + """ + Decode bytes into an L{_OPTHeader} instance. + + @type strio: L{file} + @param strio: Bytes will be read from this file until the full + L{_OPTHeader} is decoded. + + @type length: L{int} or L{None} + @param length: Not used. + """ + + h = RRHeader() + h.decode(strio, length) + h.payload = UnknownRecord(readPrecisely(strio, h.rdlength)) + + newOptHeader = self.fromRRHeader(h) + + for attrName in self.compareAttributes: + if attrName not in ('name', 'type'): + setattr(self, attrName, getattr(newOptHeader, attrName)) + + + @classmethod + def fromRRHeader(cls, rrHeader): + """ + A classmethod for constructing a new L{_OPTHeader} from the + attributes and payload of an existing L{RRHeader} instance. + + @type rrHeader: L{RRHeader} + @param rrHeader: An L{RRHeader} instance containing an + L{UnknownRecord} payload. + + @return: An instance of L{_OPTHeader}. + @rtype: L{_OPTHeader} + """ + options = None + if rrHeader.payload is not None: + options = [] + optionsBytes = BytesIO(rrHeader.payload.data) + optionsBytesLength = len(rrHeader.payload.data) + while optionsBytes.tell() < optionsBytesLength: + o = _OPTVariableOption() + o.decode(optionsBytes) + options.append(o) + + # Decode variable options if present + return cls( + udpPayloadSize=rrHeader.cls, + extendedRCODE=rrHeader.ttl >> 24, + version=rrHeader.ttl >> 16 & 0xff, + dnssecOK=(rrHeader.ttl & 0xffff) >> 15, + options=options + ) + + + +@implementer(IEncodable) +class _OPTVariableOption(tputil.FancyStrMixin, tputil.FancyEqMixin, object): + """ + A class to represent OPT record variable options. + + @see: L{_OPTVariableOption.__init__} for documentation of public + instance attributes. + + @see: L{https://tools.ietf.org/html/rfc6891#section-6.1.2} + + @since: 13.2 + """ + showAttributes = ('code', ('data', nativeString)) + compareAttributes = ('code', 'data') + + _fmt = '!HH' + + def __init__(self, code=0, data=b''): + """ + @type code: L{int} + @param code: The option code + + @type data: L{bytes} + @param data: The option data + """ + self.code = code + self.data = data + + + def encode(self, strio, compDict=None): + """ + Encode this L{_OPTVariableOption} to bytes. + + @type strio: L{file} + @param strio: the byte representation of this + L{_OPTVariableOption} will be written to this file. + + @type compDict: L{dict} or L{None} + @param compDict: A dictionary of backreference addresses that + have have already been written to this stream and that may + be used for DNS name compression. + """ + strio.write( + struct.pack(self._fmt, self.code, len(self.data)) + self.data) + + + def decode(self, strio, length=None): + """ + Decode bytes into an L{_OPTVariableOption} instance. + + @type strio: L{file} + @param strio: Bytes will be read from this file until the full + L{_OPTVariableOption} is decoded. + + @type length: L{int} or L{None} + @param length: Not used. + """ + l = struct.calcsize(self._fmt) + buff = readPrecisely(strio, l) + self.code, length = struct.unpack(self._fmt, buff) + self.data = readPrecisely(strio, length) + + + @implementer(IEncodable) class RRHeader(tputil.FancyEqMixin): """ @@ -1604,6 +1846,7 @@ def __hash__(self): return hash(tuple(self.data)) + @implementer(IEncodable, IRecord) class UnknownRecord(tputil.FancyEqMixin, tputil.FancyStrMixin, object): """ diff --git a/twisted/names/test/test_dns.py b/twisted/names/test/test_dns.py index 2a912101fc5..b24a6b952c3 100644 --- a/twisted/names/test/test_dns.py +++ b/twisted/names/test/test_dns.py @@ -12,6 +12,7 @@ import struct +from zope.interface.verify import verifyClass from twisted.python.failure import Failure from twisted.internet import address, task @@ -1957,3 +1958,533 @@ def test_caseInsensitiveComparison(self): assertIsSubdomainOf(self, b'foo.example.com', b'EXAMPLE.COM') assertIsSubdomainOf(self, b'FOO.EXAMPLE.COM', b'example.com') + + + +class OPTNonStandardAttributes(object): + """ + Generate byte and instance representations of an L{dns._OPTHeader} + where all attributes are set to non-default values. + + For testing whether attributes have really been read from the byte + string during decoding. + """ + @classmethod + def bytes(cls, excludeName=False, excludeOptions=False): + """ + Return L{bytes} representing an encoded OPT record. + + @param excludeName: A flag that controls whether to exclude + the name field. This allows a non-standard name to be + prepended during the test. + @type excludeName: L{bool} + + @param excludeOptions: A flag that controls whether to exclude + the RDLEN field. This allows encoded variable options to be + appended during the test. + @type excludeOptions: L{bool} + + @return: L{bytes} representing the encoded OPT record returned + by L{object}. + """ + rdlen = b'\x00\x00' # RDLEN 0 + if excludeOptions: + rdlen = b'' + + return ( + b'\x00' # 0 root zone + b'\x00\x29' # type 41 + b'\x02\x00' # udpPayloadsize 512 + b'\x03' # extendedRCODE 3 + b'\x04' # version 4 + b'\x80\x00' # DNSSEC OK 1 + Z + ) + rdlen + + + @classmethod + def object(cls): + """ + Return a new L{dns._OPTHeader} instance. + + @return: A L{dns._OPTHeader} instance with attributes that + match the encoded record returned by L{bytes}. + """ + return dns._OPTHeader( + udpPayloadSize=512, + extendedRCODE=3, + version=4, + dnssecOK=1) + + + +class OPTHeaderTests(ComparisonTestsMixin, unittest.TestCase): + """ + Tests for L{twisted.names.dns._OPTHeader}. + """ + def test_interface(self): + """ + L{dns._OPTHeader} implements L{dns.IEncodable}. + """ + verifyClass(dns.IEncodable, dns._OPTHeader) + + + def test_name(self): + """ + L{dns._OPTHeader.name} is a instance attribute whose value is + fixed as the root domain + """ + self.assertEqual(dns._OPTHeader().name, dns.Name(b'')) + + + def test_nameReadonly(self): + """ + L{dns._OPTHeader.name} is readonly. + """ + h = dns._OPTHeader() + self.assertRaises( + AttributeError, setattr, h, 'name', dns.Name(b'example.com')) + + + def test_type(self): + """ + L{dns._OPTHeader.type} is an instance attribute with fixed value + 41. + """ + self.assertEqual(dns._OPTHeader().type, 41) + + + def test_typeReadonly(self): + """ + L{dns._OPTHeader.type} is readonly. + """ + h = dns._OPTHeader() + self.assertRaises( + AttributeError, setattr, h, 'type', dns.A) + + + def test_udpPayloadSize(self): + """ + L{dns._OPTHeader.udpPayloadSize} defaults to 4096 as + recommended in rfc6891 section-6.2.5. + """ + self.assertEqual(dns._OPTHeader().udpPayloadSize, 4096) + + + def test_udpPayloadSizeOverride(self): + """ + L{dns._OPTHeader.udpPayloadSize} can be overridden in the + constructor. + """ + self.assertEqual(dns._OPTHeader(udpPayloadSize=512).udpPayloadSize, 512) + + + def test_extendedRCODE(self): + """ + L{dns._OPTHeader.extendedRCODE} defaults to 0. + """ + self.assertEqual(dns._OPTHeader().extendedRCODE, 0) + + + def test_extendedRCODEOverride(self): + """ + L{dns._OPTHeader.extendedRCODE} can be overridden in the + constructor. + """ + self.assertEqual(dns._OPTHeader(extendedRCODE=1).extendedRCODE, 1) + + + def test_version(self): + """ + L{dns._OPTHeader.version} defaults to 0. + """ + self.assertEqual(dns._OPTHeader().version, 0) + + + def test_versionOverride(self): + """ + L{dns._OPTHeader.version} can be overridden in the + constructor. + """ + self.assertEqual(dns._OPTHeader(version=1).version, 1) + + + def test_dnssecOK(self): + """ + L{dns._OPTHeader.dnssecOK} defaults to False. + """ + self.assertEqual(dns._OPTHeader().dnssecOK, False) + + + def test_dnssecOKOverride(self): + """ + L{dns._OPTHeader.dnssecOK} can be overridden in the + constructor. + """ + self.assertEqual(dns._OPTHeader(dnssecOK=True).dnssecOK, True) + + + def test_options(self): + """ + L{dns._OPTHeader.options} defaults to empty list. + """ + self.assertEqual(dns._OPTHeader().options, []) + + + def test_optionsOverride(self): + """ + L{dns._OPTHeader.options} can be overridden in the + constructor. + """ + h = dns._OPTHeader(options=[(1, 1, b'\x00')]) + self.assertEqual(h.options, [(1, 1, b'\x00')]) + + + def test_encode(self): + """ + L{dns._OPTHeader.encode} packs the header fields and writes + them to a file like object passed in as an argument. + """ + b = BytesIO() + + OPTNonStandardAttributes.object().encode(b) + self.assertEqual( + b.getvalue(), + OPTNonStandardAttributes.bytes() + ) + + + def test_encodeWithOptions(self): + """ + L{dns._OPTHeader.options} is a list of L{dns._OPTVariableOption} + instances which are packed into the rdata area of the header. + """ + h = OPTNonStandardAttributes.object() + h.options = [ + dns._OPTVariableOption(1, b'foobarbaz'), + dns._OPTVariableOption(2, b'qux'), + ] + b = BytesIO() + + h.encode(b) + self.assertEqual( + b.getvalue(), + + OPTNonStandardAttributes.bytes(excludeOptions=True) + ( + b'\x00\x14' # RDLEN 20 + + b'\x00\x01' # OPTION-CODE + b'\x00\x09' # OPTION-LENGTH + b'foobarbaz' # OPTION-DATA + + b'\x00\x02' # OPTION-CODE + b'\x00\x03' # OPTION-LENGTH + b'qux' # OPTION-DATA + )) + + + def test_decode(self): + """ + L{dns._OPTHeader.decode} unpacks the header fields from a file + like object and populates the attributes of an existing + L{dns._OPTHeader} instance. + """ + decodedHeader = dns._OPTHeader() + decodedHeader.decode(BytesIO(OPTNonStandardAttributes.bytes())) + + self.assertEqual( + decodedHeader, + OPTNonStandardAttributes.object()) + + + def test_decodeAllExpectedBytes(self): + """ + L{dns._OPTHeader.decode} reads all the bytes of the record + that is being decoded. + """ + # Check that all the input data has been consumed. + b = BytesIO(OPTNonStandardAttributes.bytes()) + + decodedHeader = dns._OPTHeader() + decodedHeader.decode(b) + + self.assertEqual(b.tell(), len(b.getvalue())) + + + def test_decodeOnlyExpectedBytes(self): + """ + L{dns._OPTHeader.decode} reads only the bytes from the current + file position to the end of the record that is being + decoded. Trailing bytes are not consumed. + """ + b = BytesIO(OPTNonStandardAttributes.bytes() + + b'xxxx') # Trailing bytes + + decodedHeader = dns._OPTHeader() + decodedHeader.decode(b) + + self.assertEqual(b.tell(), len(b.getvalue())-len(b'xxxx')) + + + def test_decodeDiscardsName(self): + """ + L{dns._OPTHeader.decode} discards the name which is encoded in + the supplied bytes. The name attribute of the resulting + L{dns._OPTHeader} instance will always be L{dns.Name(b'')}. + """ + b = BytesIO(OPTNonStandardAttributes.bytes(excludeName=True) + + b'\x07example\x03com\x00') + + h = dns._OPTHeader() + h.decode(b) + self.assertEqual(h.name, dns.Name(b'')) + + + def test_decodeRdlengthTooShort(self): + """ + L{dns._OPTHeader.decode} raises an exception if the supplied + RDLEN is too short. + """ + b = BytesIO( + OPTNonStandardAttributes.bytes(excludeOptions=True) + ( + b'\x00\x05' # RDLEN 5 Too short - should be 6 + + b'\x00\x01' # OPTION-CODE + b'\x00\x02' # OPTION-LENGTH + b'\x00\x00' # OPTION-DATA + )) + h = dns._OPTHeader() + self.assertRaises(EOFError, h.decode, b) + + + def test_decodeRdlengthTooLong(self): + """ + L{dns._OPTHeader.decode} raises an exception if the supplied + RDLEN is too long. + """ + b = BytesIO( + OPTNonStandardAttributes.bytes(excludeOptions=True) + ( + + b'\x00\x07' # RDLEN 7 Too long - should be 6 + + b'\x00\x01' # OPTION-CODE + b'\x00\x02' # OPTION-LENGTH + b'\x00\x00' # OPTION-DATA + )) + h = dns._OPTHeader() + self.assertRaises(EOFError, h.decode, b) + + + def test_decodeWithOptions(self): + """ + If the OPT bytes contain variable options, + L{dns._OPTHeader.decode} will populate a list + L{dns._OPTHeader.options} with L{dns._OPTVariableOption} + instances. + """ + + b = BytesIO( + OPTNonStandardAttributes.bytes(excludeOptions=True) + ( + + b'\x00\x14' # RDLEN 20 + + b'\x00\x01' # OPTION-CODE + b'\x00\x09' # OPTION-LENGTH + b'foobarbaz' # OPTION-DATA + + b'\x00\x02' # OPTION-CODE + b'\x00\x03' # OPTION-LENGTH + b'qux' # OPTION-DATA + )) + + h = dns._OPTHeader() + h.decode(b) + self.assertEqual( + h.options, + [dns._OPTVariableOption(1, b'foobarbaz'), + dns._OPTVariableOption(2, b'qux'),] + ) + + + def test_fromRRHeader(self): + """ + L{_OPTHeader.fromRRHeader} accepts an L{RRHeader} instance and + returns an L{_OPTHeader} instance whose attribute values have + been derived from the C{cls}, C{ttl} and C{payload} attributes + of the original header. + """ + genericHeader = dns.RRHeader( + b'example.com', + type=dns.OPT, + cls=0xffff, + ttl=(0xfe << 24 + | 0xfd << 16 + | True << 15), + payload=dns.UnknownRecord(b'\xff\xff\x00\x03abc')) + + decodedOptHeader = dns._OPTHeader.fromRRHeader(genericHeader) + + expectedOptHeader = dns._OPTHeader( + udpPayloadSize=0xffff, + extendedRCODE=0xfe, + version=0xfd, + dnssecOK=1, + options=[dns._OPTVariableOption(code=0xffff, data=b'abc')]) + + self.assertEqual(decodedOptHeader, expectedOptHeader) + + + def test_repr(self): + """ + L{dns._OPTHeader.__repr__} displays the name and type and all + the fixed and extended header values of the OPT record. + """ + self.assertEqual( + repr(dns._OPTHeader()), + '<_OPTHeader ' + 'name= ' + 'type=41 ' + 'udpPayloadSize=4096 ' + 'extendedRCODE=0 ' + 'version=0 ' + 'dnssecOK=False ' + 'options=[]>') + + + def test_equalityUdpPayloadSize(self): + """ + Two L{OPTHeader} instances compare equal if they have the same + udpPayloadSize. + """ + self.assertNormalEqualityImplementation( + dns._OPTHeader(udpPayloadSize=512), + dns._OPTHeader(udpPayloadSize=512), + dns._OPTHeader(udpPayloadSize=4096)) + + + def test_equalityExtendedRCODE(self): + """ + Two L{OPTHeader} instances compare equal if they have the same + extendedRCODE. + """ + self.assertNormalEqualityImplementation( + dns._OPTHeader(extendedRCODE=1), + dns._OPTHeader(extendedRCODE=1), + dns._OPTHeader(extendedRCODE=2)) + + + def test_equalityVersion(self): + """ + Two L{OPTHeader} instances compare equal if they have the same + version. + """ + self.assertNormalEqualityImplementation( + dns._OPTHeader(version=1), + dns._OPTHeader(version=1), + dns._OPTHeader(version=2)) + + + def test_equalityDnssecOK(self): + """ + Two L{OPTHeader} instances compare equal if they have the same + dnssecOK flags. + """ + self.assertNormalEqualityImplementation( + dns._OPTHeader(dnssecOK=1), + dns._OPTHeader(dnssecOK=1), + dns._OPTHeader(dnssecOK=0)) + + + def test_equalityOptions(self): + """ + Two L{OPTHeader} instances compare equal if they have the same + options. + """ + self.assertNormalEqualityImplementation( + dns._OPTHeader(options=[dns._OPTVariableOption(1, b'x')]), + dns._OPTHeader(options=[dns._OPTVariableOption(1, b'x')]), + dns._OPTHeader(options=[dns._OPTVariableOption(2, b'y')])) + + + +class OPTVariableOptionTests(ComparisonTestsMixin, unittest.TestCase): + """ + Tests for L{dns._OPTVariableOption}. + """ + def test_interface(self): + """ + L{dns._OPTVariableOption} implements L{dns.IEncodable}. + """ + verifyClass(dns.IEncodable, dns._OPTVariableOption) + + + def test_constructorArguments(self): + """ + L{dns._OPTVariableOption.__init__} requires code and data + arguments which are saved as public instance attributes. + """ + h = dns._OPTVariableOption(1, b'x') + self.assertEqual(h.code, 1) + self.assertEqual(h.data, b'x') + + + def test_repr(self): + """ + L{dns._OPTVariableOption.__repr__} displays the code and data + of the option. + """ + self.assertEqual( + repr(dns._OPTVariableOption(1, b'x')), + '<_OPTVariableOption ' + 'code=1 ' + "data=x" + '>') + + + def test_equality(self): + """ + Two OPTVariableOption instances compare equal if they have the same + code and data values. + """ + self.assertNormalEqualityImplementation( + dns._OPTVariableOption(1, b'x'), + dns._OPTVariableOption(1, b'x'), + dns._OPTVariableOption(2, b'x')) + + self.assertNormalEqualityImplementation( + dns._OPTVariableOption(1, b'x'), + dns._OPTVariableOption(1, b'x'), + dns._OPTVariableOption(1, b'y')) + + + def test_encode(self): + """ + L{dns._OPTVariableOption.encode} encodes the code and data + instance attributes to a byte string which also includes the + data length. + """ + o = dns._OPTVariableOption(1, b'foobar') + b = BytesIO() + o.encode(b) + self.assertEqual( + b.getvalue(), + b'\x00\x01' # OPTION-CODE 1 + b'\x00\x06' # OPTION-LENGTH 6 + b'foobar' # OPTION-DATA + ) + + + def test_decode(self): + """ + L{dns._OPTVariableOption.decode} is a classmethod that decodes + a byte string and returns a L{dns._OPTVariableOption} instance. + """ + b = BytesIO( + b'\x00\x01' # OPTION-CODE 1 + b'\x00\x06' # OPTION-LENGTH 6 + b'foobar' # OPTION-DATA + ) + + o = dns._OPTVariableOption() + o.decode(b) + self.assertEqual(o.code, 1) + self.assertEqual(o.data, b'foobar') diff --git a/twisted/names/topfiles/5668.misc b/twisted/names/topfiles/5668.misc new file mode 100644 index 00000000000..e69de29bb2d