From 457ad42b52513c1e69243c9c971291e02eb1963a Mon Sep 17 00:00:00 2001 From: Donncha O'Cearbhaill Date: Sun, 21 Aug 2016 19:23:43 +0200 Subject: [PATCH 01/10] Fix bug where the CLASS was not include in generated zones --- blockstack_zones/record_processors.py | 1 + 1 file changed, 1 insertion(+) diff --git a/blockstack_zones/record_processors.py b/blockstack_zones/record_processors.py index fb8b2ab..7b0a7c4 100644 --- a/blockstack_zones/record_processors.py +++ b/blockstack_zones/record_processors.py @@ -117,6 +117,7 @@ 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']) ) + record_data.append("IN") record_data.append(record_type) record_data += [str(data[i][record_key]) for record_key in record_keys] record += " ".join(record_data) + "\n" From 3578e00eec0f8370ca08adea21246c5bfccfff49 Mon Sep 17 00:00:00 2001 From: Donncha O'Cearbhaill Date: Sun, 21 Aug 2016 19:24:15 +0200 Subject: [PATCH 02/10] Fix bug in test for URI record --- unit_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unit_tests.py b/unit_tests.py index e01d466..b4feef3 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"] From 6bd86cece3cfc0eb2ce1497a95e3f508378a9b1f Mon Sep 17 00:00:00 2001 From: Donncha O'Cearbhaill Date: Sun, 21 Aug 2016 19:24:42 +0200 Subject: [PATCH 03/10] Add test for the missing CLASS bug --- unit_tests.py | 1 + 1 file changed, 1 insertion(+) diff --git a/unit_tests.py b/unit_tests.py index b4feef3..de51ba7 100644 --- a/unit_tests.py +++ b/unit_tests.py @@ -29,6 +29,7 @@ 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) + self.assertTrue("www IN A 127.0.0.1" in zone_file) def test_zone_file_creation_3(self): json_zone_file = zone_file_objects["sample_3"] From 6451a1ce41b6730ceceaed9a3201392bb3fd8ab6 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Mon, 22 Aug 2016 12:33:47 -0400 Subject: [PATCH 04/10] version bump, so the fix for handling zonefiles with missing $ORIGINs is present in pypi --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 57d4fcb..e74daf8 100755 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name='blockstack-zones', - version='0.1.6', + version='0.1.6.1', url='https://github.com/blockstack/dns-zone-file-py', license='MIT', author='Blockstack Developers', From cb0483878bd6c6b5da75997b82180510a89114e4 Mon Sep 17 00:00:00 2001 From: Burt Bielicki Date: Tue, 30 Aug 2016 14:00:08 -0700 Subject: [PATCH 05/10] Fix casing on dict lookup for PTR records --- blockstack_zones/parse_zone_file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blockstack_zones/parse_zone_file.py b/blockstack_zones/parse_zone_file.py index aeb3b37..f6221d6 100644 --- a/blockstack_zones/parse_zone_file.py +++ b/blockstack_zones/parse_zone_file.py @@ -324,7 +324,7 @@ 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': From 5f2af6c7a9fba48e6cad693bc2898a3a311ce40d Mon Sep 17 00:00:00 2001 From: Donncha O'Cearbhaill Date: Wed, 31 Aug 2016 12:07:36 +0200 Subject: [PATCH 06/10] Do not remove CLASS field when parsing zone files --- blockstack_zones/parse_zone_file.py | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/blockstack_zones/parse_zone_file.py b/blockstack_zones/parse_zone_file.py index aeb3b37..d77f082 100644 --- a/blockstack_zones/parse_zone_file.py +++ b/blockstack_zones/parse_zone_file.py @@ -227,34 +227,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 @@ -371,7 +343,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 From c4389299ffb9290b7564fc8c55a43c5bba77a498 Mon Sep 17 00:00:00 2001 From: Donncha O'Cearbhaill Date: Wed, 31 Aug 2016 15:32:40 +0200 Subject: [PATCH 07/10] Perform parsing and serialization of the CLASS field All records in a zone file without a CLASS field get tagged with the attribute `_missing_class`. If `_missing_class` is set then the CLASS field will be omited from that records when generating a zone file. --- blockstack_zones/parse_zone_file.py | 31 +++++++++++++++++++++++---- blockstack_zones/record_processors.py | 10 ++++++++- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/blockstack_zones/parse_zone_file.py b/blockstack_zones/parse_zone_file.py index d77f082..f6ea30d 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: @@ -262,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) @@ -301,7 +320,11 @@ def parse_line(parser, record_token, parsed_records): # 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 diff --git a/blockstack_zones/record_processors.py b/blockstack_zones/record_processors.py index 7b0a7c4..d0140d0 100644 --- a/blockstack_zones/record_processors.py +++ b/blockstack_zones/record_processors.py @@ -117,7 +117,15 @@ 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']) ) - record_data.append("IN") + # 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'): + if data[i].get('class'): + record_data.append(str(data[i]['class'])) + else: + raise ValueError("Record must have a CLASS field") + record_data.append(record_type) record_data += [str(data[i][record_key]) for record_key in record_keys] record += " ".join(record_data) + "\n" From d64fa139ba72f1e0dbdc0016729d6a9222115d1c Mon Sep 17 00:00:00 2001 From: Donncha O'Cearbhaill Date: Wed, 31 Aug 2016 15:40:05 +0200 Subject: [PATCH 08/10] Unit tests for CLASS field parsing and zone generation --- test_sample_data.py | 62 +++++++++++++++++++++++---------------------- unit_tests.py | 22 +++++++++++++++- 2 files changed, 53 insertions(+), 31 deletions(-) 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 de51ba7..da5495f 100644 --- a/unit_tests.py +++ b/unit_tests.py @@ -29,7 +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) - self.assertTrue("www IN A 127.0.0.1" 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"] @@ -59,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) From c60f3d5e27605fe81bbe49eb865748be85886c71 Mon Sep 17 00:00:00 2001 From: Donncha O'Cearbhaill Date: Fri, 9 Sep 2016 15:50:29 +0200 Subject: [PATCH 09/10] Remove requirement for CLASS field with make_zone_file --- blockstack_zones/record_processors.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/blockstack_zones/record_processors.py b/blockstack_zones/record_processors.py index d0140d0..a9b2465 100644 --- a/blockstack_zones/record_processors.py +++ b/blockstack_zones/record_processors.py @@ -121,10 +121,7 @@ def process_rr(data, record_type, record_keys, field, template): # 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'): - if data[i].get('class'): - record_data.append(str(data[i]['class'])) - else: - raise ValueError("Record must have a CLASS field") + 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] From fa3f1ea64d55cf33b6760940a1db768817b9fd26 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Thu, 29 Sep 2016 15:36:32 -0400 Subject: [PATCH 10/10] version bump, to 0.14.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e74daf8..2d12f86 100755 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name='blockstack-zones', - version='0.1.6.1', + version='0.14.0', url='https://github.com/blockstack/dns-zone-file-py', license='MIT', author='Blockstack Developers',