diff --git a/blockstack_zones/parse_zone_file.py b/blockstack_zones/parse_zone_file.py index aeb3b37..4dc5382 100644 --- a/blockstack_zones/parse_zone_file.py +++ b/blockstack_zones/parse_zone_file.py @@ -36,6 +36,7 @@ def make_rr_subparser(subparsers, rec_type, args_and_types): sp.add_argument("name", type=str) sp.add_argument("ttl", type=int, nargs='?') + sp.add_argument("class", type=str) sp.add_argument(rec_type, type=str) for (argname, argtype) in args_and_types: @@ -227,34 +228,6 @@ def flatten(text): return "\n".join(flattened) -def remove_class(text): - """ - Remove the CLASS from each DNS record, if present. - The only class that gets used today (for all intents - and purposes) is 'IN'. - """ - - # see RFC 1035 for list of classes - lines = text.split("\n") - ret = [] - for line in lines: - tokens = tokenize_line(line) - tokens_upper = [t.upper() for t in tokens] - - if "IN" in tokens_upper: - tokens.remove("IN") - elif "CS" in tokens_upper: - tokens.remove("CS") - elif "CH" in tokens_upper: - tokens.remove("CH") - elif "HS" in tokens_upper: - tokens.remove("HS") - - ret.append(serialize(tokens)) - - return "\n".join(ret) - - def add_default_name(text): """ Go through each line of the text and ensure that @@ -290,14 +263,32 @@ def parse_line(parser, record_token, parsed_records): global SUPPORTED_RECORDS line = " ".join(record_token) + class_inferred = False - # match parser to record type + # match parser to record type, add record type as first element if len(record_token) >= 2 and record_token[1] in SUPPORTED_RECORDS: - # with no ttl + # RR has no TTL or CLASS record_token = [record_token[1]] + record_token + record_token.insert(2, "IN") + class_inferred = True + elif len(record_token) >= 3 and record_token[2] in SUPPORTED_RECORDS: - # with ttl + # RR has a TTL or CLASS record_token = [record_token[2]] + record_token + if record_token[2][0].isdigit(): + # First character of token is numeric, must be TTL value and not a CLASS. + # Add default IN class + record_token.insert(3, "IN") + class_inferred = True + + elif len(record_token) >= 4 and record_token[3] in SUPPORTED_RECORDS: + # RR has a TTL and CLASS + record_token = [record_token[3]] + record_token + + # Class and TTL can be provided in either order, always place TTL before CLASS + if not record_token[2][0].isdigit(): + record_token[2], record_token[3] = record_token[3], record_token[2] + try: rr, unmatched = parser.parse_known_args(record_token) @@ -324,12 +315,16 @@ def parse_line(parser, record_token, parsed_records): if record_dict[field] is None: del record_dict[field] - current_origin = record_dict.get('$ORIGIN', parsed_records.get('$ORIGIN', None)) + current_origin = record_dict.get('$origin', parsed_records.get('$origin', None)) # special record-specific fix-ups if record_type == 'PTR': record_dict['fullname'] = record_dict['name'] + '.' + current_origin - + + # Add a hint that no CLASS was provide for this record, defaulted to IN + if class_inferred: + record_dict['_missing_class'] = True + if len(record_dict) > 0: if record_type.startswith("$"): # put the value directly @@ -371,7 +366,6 @@ def parse_zone_file(text, ignore_invalid=False): """ text = remove_comments(text) text = flatten(text) - text = remove_class(text) text = add_default_name(text) json_zone_file = parse_lines(text, ignore_invalid=ignore_invalid) return json_zone_file diff --git a/blockstack_zones/record_processors.py b/blockstack_zones/record_processors.py index fb8b2ab..a9b2465 100644 --- a/blockstack_zones/record_processors.py +++ b/blockstack_zones/record_processors.py @@ -117,6 +117,12 @@ def process_rr(data, record_type, record_keys, field, template): if data[i].get('ttl') is not None: record_data.append( str(data[i]['ttl']) ) + # Do not output a CLASS field if the '_missing_class' flag indicates + # that it was not included in the original zone file. + # Records which did not have a CLASS should be serialized without a CLASS + if not data[i].get('_missing_class'): + record_data.append(str(data[i].get('class', 'IN'))) + record_data.append(record_type) record_data += [str(data[i][record_key]) for record_key in record_keys] record += " ".join(record_data) + "\n" diff --git a/setup.py b/setup.py index 57d4fcb..2d12f86 100755 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name='blockstack-zones', - version='0.1.6', + version='0.14.0', url='https://github.com/blockstack/dns-zone-file-py', license='MIT', author='Blockstack Developers', diff --git a/test_sample_data.py b/test_sample_data.py index 6919059..616d043 100644 --- a/test_sample_data.py +++ b/test_sample_data.py @@ -15,10 +15,11 @@ $ORIGIN example.com $TTL 86400 -server1 IN A 10.0.1.5 +server1 A 10.0.1.5 server2 IN A 10.0.1.7 -dns1 IN A 10.0.1.2 -dns2 IN A 10.0.1.3 +server3 3600 A 10.0.1.7 +dns1 3600 IN A 10.0.1.2 +dns2 IN 3600 A 10.0.1.3 ftp IN CNAME server1 mail IN CNAME server1 @@ -59,9 +60,10 @@ "uri": [{ "name": "@", "ttl": "1D", + "class": "IN", "priority": 1, "weight": 10, - "target": "https://mq9.s3.amazonaws.com/naval.id/profile.json" + "target": "https://mq9.s3.amazonaws.com/naval.id/profile.json", }] }, "sample_2": { @@ -77,33 +79,33 @@ "minimum": 86400 }, "ns": [ - { "host": "NS1.NAMESERVER.NET." }, - { "host": "NS2.NAMESERVER.NET." } + { "host": "NS1.NAMESERVER.NET.", "class": "IN" }, + { "host": "NS2.NAMESERVER.NET.", "class": "IN" } ], "a": [ - { "name": "@", "ip": "127.0.0.1" }, - { "name": "www", "ip": "127.0.0.1" }, - { "name": "mail", "ip": "127.0.0.1" } + { "name": "@", "ip": "127.0.0.1", "class": "IN", }, + { "name": "www", "ip": "127.0.0.1", "class": "IN", "ttl": 3600 }, + { "name": "mail", "ip": "127.0.0.1", "ttl": 3600, "_missing_class": True } ], "aaaa": [ - { "ip": "::1" }, - { "name": "mail", "ip": "2001:db8::1" } + { "ip": "::1", "class": "IN" }, + { "name": "mail", "ip": "2001:db8::1", "class": "IN" } ], "cname": [ - { "name": "mail1", "alias": "mail" }, - { "name": "mail2", "alias": "mail" } + { "name": "mail1", "alias": "mail", "class": "IN" }, + { "name": "mail2", "alias": "mail", "class": "IN" } ], "mx": [ - { "preference": 0, "host": "mail1" }, - { "preference": 10, "host": "mail2" } + { "preference": 0, "host": "mail1", "class": "IN" }, + { "preference": 10, "host": "mail2", "class": "IN" } ], "txt": [ - { "name": "txt1", "txt": "hello" }, - { "name": "txt2", "txt": "world" } + { "name": "txt1", "txt": "hello", "class": "IN" }, + { "name": "txt2", "txt": "world", "class": "IN" } ], "srv": [ - { "name": "_xmpp-client._tcp", "target": "jabber", "priority": 10, "weight": 0, "port": 5222 }, - { "name": "_xmpp-server._tcp", "target": "jabber", "priority": 10, "weight": 0, "port": 5269 } + { "name": "_xmpp-client._tcp", "class": "IN", "target": "jabber", "priority": 10, "weight": 0, "port": 5222 }, + { "name": "_xmpp-server._tcp", "class": "IN", "target": "jabber", "priority": 10, "weight": 0, "port": 5269 } ] }, "sample_3": { @@ -119,25 +121,25 @@ "minimum": 86400 }, "ns": [ - { "host": "NS1.NAMESERVER.NET." }, - { "host": "NS2.NAMESERVER.NET." } + { "host": "NS1.NAMESERVER.NET.", "class": "IN" }, + { "host": "NS2.NAMESERVER.NET.", "class": "IN" } ], "a": [ - { "name": "@", "ip": "127.0.0.1" }, - { "name": "www", "ip": "127.0.0.1" }, - { "name": "mail", "ip": "127.0.0.1" } + { "name": "@", "ip": "127.0.0.1", "class": "IN" }, + { "name": "www", "ip": "127.0.0.1", "class": "IN" }, + { "name": "mail", "ip": "127.0.0.1", "class": "IN" } ], "aaaa": [ - { "ip": "::1" }, - { "name": "mail", "ip": "2001:db8::1" } + { "ip": "::1", "class": "IN" }, + { "name": "mail", "ip": "2001:db8::1", "class": "IN" } ], "cname":[ - { "name": "mail1", "alias": "mail" }, - { "name": "mail2", "alias": "mail" } + { "name": "mail1", "alias": "mail", "class": "IN" }, + { "name": "mail2", "alias": "mail", "class": "IN" } ], "mx":[ - { "preference": 0, "host": "mail1" }, - { "preference": 10, "host": "mail2" } + { "preference": 0, "host": "mail1", "class": "IN" }, + { "preference": 10, "host": "mail2", "class": "IN" } ] } } \ No newline at end of file diff --git a/unit_tests.py b/unit_tests.py index e01d466..da5495f 100644 --- a/unit_tests.py +++ b/unit_tests.py @@ -19,7 +19,7 @@ def test_zone_file_creation_1(self): self.assertTrue(isinstance(zone_file, (unicode, str))) self.assertTrue("$ORIGIN" in zone_file) self.assertTrue("$TTL" in zone_file) - self.assertTrue("@ 1D URI" in zone_file) + self.assertTrue("@ 1D IN URI" in zone_file) def test_zone_file_creation_2(self): json_zone_file = zone_file_objects["sample_2"] @@ -29,6 +29,10 @@ def test_zone_file_creation_2(self): self.assertTrue("$ORIGIN" in zone_file) self.assertTrue("$TTL" in zone_file) self.assertTrue("@ IN SOA" in zone_file) + # www has a TTL and a class + self.assertTrue("www 3600 IN A 127.0.0.1" in zone_file) + # mail has "_missing_class" set, confirm no class is output + self.assertTrue("mail 3600 A 127.0.0.1" in zone_file) def test_zone_file_creation_3(self): json_zone_file = zone_file_objects["sample_3"] @@ -58,6 +62,23 @@ def test_zone_file_parsing_2(self): self.assertTrue("$ttl" in zone_file) self.assertTrue("$origin" in zone_file) + a_records = {record["name"]: record for record in zone_file["a"]} + # Confirm that all records have class "IN" + self.assertTrue(all([(record["class"] == "IN") for record in a_records.values()])) + # TTL and no CLASS + self.assertEqual(a_records["server1"].get("_missing_class"), True) + # CLASS and no TTL + self.assertEqual(a_records["server2"].get("_missing_class"), None) + # TTL and no CLASS + self.assertEqual(a_records["server3"].get("ttl"), 3600) + self.assertEqual(a_records["server3"].get("_missing_class"), True) + # TTL and CLASS + self.assertEqual(a_records["dns1"].get("ttl"), 3600) + self.assertEqual(a_records["dns1"].get("_missing_class"), None) + # Reversed TTL and CLASS field order + self.assertEqual(a_records["dns2"].get("ttl"), 3600) + self.assertEqual(a_records["dns2"].get("_missing_class"), None) + def test_zone_file_parsing_3(self): zone_file = parse_zone_file(zone_files["sample_3"]) #print json.dumps(zone_file, indent=2)