├── .gitignore ├── LICENSE ├── README.md ├── __init__.py ├── bin └── zonefile ├── blockstack_zones ├── __init__.py ├── configs.py ├── exceptions.py ├── make_zone_file.py ├── parse_zone_file.py └── record_processors.py ├── setup.py ├── test_sample_data.py ├── tests ├── zonefile_forward.json ├── zonefile_forward.txt ├── zonefile_reverse.json ├── zonefile_reverse.txt ├── zonefile_reverse_ipv6.json └── zonefile_reverse_ipv6.txt └── unit_tests.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | 64 | unused -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Blockstack 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DNS Zone File Library 2 | 3 | [![CircleCI](https://img.shields.io/circleci/project/blockstack/dns-zone-file-py/master.svg)](https://circleci.com/gh/blockstack/dns-zone-file-py/tree/master) 4 | [![PyPI](https://img.shields.io/pypi/v/blockstack-zones.svg)](https://pypi.python.org/pypi/blockstack-zones/) 5 | [![PyPI](https://img.shields.io/pypi/dm/blockstack-zones.svg)](https://pypi.python.org/pypi/blockstack-zones/) 6 | [![PyPI](https://img.shields.io/pypi/l/blockstack-zones.svg)](https://pypi.python.org/pypi/blockstack-zones/) 7 | [![Slack](http://slack.blockstack.org/badge.svg)](http://slack.blockstack.org/) 8 | 9 | *A library for creating and parsing DNS zone files* 10 | 11 | #### Zone File Example 12 | 13 | ``` 14 | $ORIGIN example.com 15 | $TTL 86400 16 | 17 | server1 IN A 10.0.1.5 18 | server2 IN A 10.0.1.7 19 | dns1 IN A 10.0.1.2 20 | dns2 IN A 10.0.1.3 21 | 22 | ftp IN CNAME server1 23 | mail IN CNAME server1 24 | mail2 IN CNAME server2 25 | www IN CNAME server2 26 | ``` 27 | 28 | #### Parsing Zone Files 29 | 30 | ```python 31 | >>> zone_file_object = parse_zone_file(zone_file) 32 | >>> print json.dumps(zone_file_object, indent=4, sort_keys=True) 33 | { 34 | "$origin": "EXAMPLE.COM", 35 | "$ttl": 86400, 36 | "a": [ 37 | { 38 | "ip": "10.0.1.5", 39 | "name": "SERVER1" 40 | }, 41 | { 42 | "ip": "10.0.1.7", 43 | "name": "SERVER2" 44 | }, 45 | { 46 | "ip": "10.0.1.2", 47 | "name": "DNS1" 48 | }, 49 | { 50 | "ip": "10.0.1.3", 51 | "name": "DNS2" 52 | } 53 | ], 54 | "cname": [ 55 | { 56 | "alias": "SERVER1", 57 | "name": "FTP" 58 | }, 59 | { 60 | "alias": "SERVER1", 61 | "name": "MAIL" 62 | }, 63 | { 64 | "alias": "SERVER2", 65 | "name": "MAIL2" 66 | }, 67 | { 68 | "alias": "SERVER2", 69 | "name": "WWW" 70 | } 71 | ] 72 | } 73 | ``` 74 | 75 | #### Making Zone Files 76 | 77 | ```python 78 | >>> records = {'uri': [{'priority': 1, 'target': 'https://mq9.s3.amazonaws.com/naval.id/profile.json', 'name': '@', 'weight': 10, 'ttl': '1D'}]} 79 | >>> zone_file = make_zone_file(records, origin="ryan.id", ttl="3600") 80 | >>> print zone_file 81 | ``` 82 | 83 | ``` 84 | $ORIGIN ryan.id 85 | $TTL 3600 86 | @ 1D URI 1 10 "https://mq9.s3.amazonaws.com/naval.id/profile.json" 87 | ``` 88 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stacks-archive/zone-file-py/c1078c8c3c28f0881bc9a3af53d4972c4a6862d0/__init__.py -------------------------------------------------------------------------------- /bin/zonefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import blockstack_zones 4 | import sys 5 | import os 6 | import json 7 | import traceback 8 | 9 | if __name__ == "__main__": 10 | if len(sys.argv) < 2: 11 | print >> sys.stderr, "Usage: %s [txt or json file] [origin] [ttl]" % sys.argv[0] 12 | sys.exit(1) 13 | 14 | origin = None 15 | ttl = None 16 | 17 | if len(sys.argv) >= 3: 18 | origin = sys.argv[2] 19 | 20 | if len(sys.argv) >= 4: 21 | ttl = sys.argv[3] 22 | 23 | dat = None 24 | with open(sys.argv[1], "r") as f: 25 | dat = f.read() 26 | 27 | try: 28 | # maybe it's a JSON file? 29 | dat = json.loads(dat) 30 | zf = zone_file.make_zone_file( dat, origin=origin, ttl=ttl ) 31 | print zf 32 | 33 | except ValueError: 34 | # maybe it's a zone file? 35 | try: 36 | zfj = zone_file.parse_zone_file( dat ) 37 | print json.dumps(zfj, indent=4, sort_keys=True) 38 | except zone_file.InvalidLineException, e: 39 | print >> sys.stderr, "WARN: Invalid line: %s" % str(e) 40 | print >> sys.stderr, "Trying again, while ignoring invalid lines" 41 | try: 42 | zfj = zone_file.parse_zone_file( dat, ignore_invalid=True ) 43 | print json.dumps(zfj, indent=4, sort_keys=True) 44 | except: 45 | traceback.print_exc() 46 | sys.exit(1) 47 | 48 | except Exception, e: 49 | traceback.print_exc() 50 | sys.exit(1) 51 | 52 | -------------------------------------------------------------------------------- /blockstack_zones/__init__.py: -------------------------------------------------------------------------------- 1 | from parse_zone_file import parse_zone_file 2 | from make_zone_file import make_zone_file 3 | from exceptions import InvalidLineException 4 | -------------------------------------------------------------------------------- /blockstack_zones/configs.py: -------------------------------------------------------------------------------- 1 | SUPPORTED_RECORDS = [ 2 | '$ORIGIN', '$TTL', 'SOA', 'NS', 'A', 'AAAA', 'CNAME', 'ALIAS', 'MX', 3 | 'PTR', 'TXT', 'SRV', 'SPF', 'URI', 4 | ] 5 | 6 | DEFAULT_TEMPLATE = """ 7 | {$origin}\n\ 8 | {$ttl}\n\ 9 | \n\ 10 | {soa} 11 | \n\ 12 | {ns}\n\ 13 | \n\ 14 | {mx}\n\ 15 | \n\ 16 | {a}\n\ 17 | \n\ 18 | {aaaa}\n\ 19 | \n\ 20 | {cname}\n\ 21 | \n\ 22 | {alias}\n\ 23 | \n\ 24 | {ptr}\n\ 25 | \n\ 26 | {txt}\n\ 27 | \n\ 28 | {srv}\n\ 29 | \n\ 30 | {spf}\n\ 31 | \n\ 32 | {uri}\n\ 33 | """ -------------------------------------------------------------------------------- /blockstack_zones/exceptions.py: -------------------------------------------------------------------------------- 1 | class InvalidLineException(Exception): 2 | pass -------------------------------------------------------------------------------- /blockstack_zones/make_zone_file.py: -------------------------------------------------------------------------------- 1 | from .record_processors import ( 2 | process_origin, process_ttl, process_soa, process_ns, process_a, 3 | process_aaaa, process_cname, process_alias, process_mx, process_ptr, 4 | process_txt, process_srv, process_spf, process_uri 5 | ) 6 | from .configs import DEFAULT_TEMPLATE 7 | import copy 8 | 9 | 10 | def make_zone_file(json_zone_file_input, origin=None, ttl=None, template=None): 11 | """ 12 | Generate the DNS zonefile, given a json-encoded description of the 13 | zone file (@json_zone_file) and the template to fill in (@template) 14 | 15 | json_zone_file = { 16 | "$origin": origin server, 17 | "$ttl": default time-to-live, 18 | "soa": [ soa records ], 19 | "ns": [ ns records ], 20 | "a": [ a records ], 21 | "aaaa": [ aaaa records ] 22 | "cname": [ cname records ] 23 | "alias": [ alias records ] 24 | "mx": [ mx records ] 25 | "ptr": [ ptr records ] 26 | "txt": [ txt records ] 27 | "srv": [ srv records ] 28 | "spf": [ spf records ] 29 | "uri": [ uri records ] 30 | } 31 | """ 32 | 33 | if template is None: 34 | template = DEFAULT_TEMPLATE[:] 35 | 36 | # careful... 37 | json_zone_file = copy.deepcopy(json_zone_file_input) 38 | if origin is not None: 39 | json_zone_file['$origin'] = origin 40 | 41 | if ttl is not None: 42 | json_zone_file['$ttl'] = ttl 43 | 44 | soa_records = [json_zone_file.get('soa')] if json_zone_file.get('soa') else None 45 | 46 | zone_file = template 47 | zone_file = process_origin(json_zone_file.get('$origin', None), zone_file) 48 | zone_file = process_ttl(json_zone_file.get('$ttl', None), zone_file) 49 | zone_file = process_soa(soa_records, zone_file) 50 | zone_file = process_ns(json_zone_file.get('ns', None), zone_file) 51 | zone_file = process_a(json_zone_file.get('a', None), zone_file) 52 | zone_file = process_aaaa(json_zone_file.get('aaaa', None), zone_file) 53 | zone_file = process_cname(json_zone_file.get('cname', None), zone_file) 54 | zone_file = process_alias(json_zone_file.get('alias', None), zone_file) 55 | zone_file = process_mx(json_zone_file.get('mx', None), zone_file) 56 | zone_file = process_ptr(json_zone_file.get('ptr', None), zone_file) 57 | zone_file = process_txt(json_zone_file.get('txt', None), zone_file) 58 | zone_file = process_srv(json_zone_file.get('srv', None), zone_file) 59 | zone_file = process_spf(json_zone_file.get('spf', None), zone_file) 60 | zone_file = process_uri(json_zone_file.get('uri', None), zone_file) 61 | 62 | # remove newlines, but terminate with one 63 | zone_file = "\n".join( 64 | filter( 65 | lambda l: len(l.strip()) > 0, [tl.strip() for tl in zone_file.split("\n")] 66 | ) 67 | ) + "\n" 68 | 69 | return zone_file 70 | -------------------------------------------------------------------------------- /blockstack_zones/parse_zone_file.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | Known limitations: 5 | * only one $ORIGIN and one $TTL are supported 6 | * only the IN class is supported 7 | * PTR records must have a non-empty name 8 | * currently only supports the following: 9 | '$ORIGIN', '$TTL', 'SOA', 'NS', 'A', 'AAAA', 'CNAME', 'MX', 'PTR', 10 | 'TXT', 'SRV', 'SPF', 'URI' 11 | """ 12 | 13 | import os 14 | import copy 15 | import datetime 16 | import time 17 | import argparse 18 | from collections import defaultdict 19 | 20 | from .configs import SUPPORTED_RECORDS, DEFAULT_TEMPLATE 21 | from .exceptions import InvalidLineException 22 | 23 | 24 | class ZonefileLineParser(argparse.ArgumentParser): 25 | def error(self, message): 26 | """ 27 | Silent error message 28 | """ 29 | raise InvalidLineException(message) 30 | 31 | 32 | def make_rr_subparser(subparsers, rec_type, args_and_types): 33 | """ 34 | Make a subparser for a given type of DNS record 35 | """ 36 | sp = subparsers.add_parser(rec_type) 37 | 38 | sp.add_argument("name", type=str) 39 | sp.add_argument("ttl", type=int, nargs='?') 40 | sp.add_argument(rec_type, type=str) 41 | 42 | for my_spec in args_and_types: 43 | (argname, argtype) = my_spec[:2] 44 | if len(my_spec) > 2: 45 | nargs = my_spec[2] 46 | sp.add_argument(argname, type=argtype, nargs=nargs) 47 | else: 48 | sp.add_argument(argname, type=argtype) 49 | return sp 50 | 51 | def make_txt_subparser(subparsers): 52 | sp = subparsers.add_parser("TXT") 53 | 54 | sp.add_argument("name", type=str) 55 | sp.add_argument("--ttl", type=int) 56 | sp.add_argument("TXT", type=str) 57 | sp.add_argument("txt", type=str, nargs='+') 58 | return sp 59 | 60 | def make_parser(): 61 | """ 62 | Make an ArgumentParser that accepts DNS RRs 63 | """ 64 | line_parser = ZonefileLineParser() 65 | subparsers = line_parser.add_subparsers() 66 | 67 | # parse $ORIGIN 68 | sp = subparsers.add_parser("$ORIGIN") 69 | sp.add_argument("$ORIGIN", type=str) 70 | 71 | # parse $TTL 72 | sp = subparsers.add_parser("$TTL") 73 | sp.add_argument("$TTL", type=int) 74 | 75 | # parse each RR 76 | args_and_types = [ 77 | ("mname", str), ("rname", str), ("serial", int), ("refresh", int), 78 | ("retry", int), ("expire", int), ("minimum", int) 79 | ] 80 | make_rr_subparser(subparsers, "SOA", args_and_types) 81 | 82 | make_rr_subparser(subparsers, "NS", [("host", str)]) 83 | make_rr_subparser(subparsers, "A", [("ip", str)]) 84 | make_rr_subparser(subparsers, "AAAA", [("ip", str)]) 85 | make_rr_subparser(subparsers, "CNAME", [("alias", str)]) 86 | make_rr_subparser(subparsers, "ALIAS", [("host", str)]) 87 | make_rr_subparser(subparsers, "MX", [("preference", str), ("host", str)]) 88 | make_txt_subparser(subparsers) 89 | make_rr_subparser(subparsers, "PTR", [("host", str)]) 90 | make_rr_subparser(subparsers, "SRV", [("priority", int), ("weight", int), ("port", int), ("target", str)]) 91 | make_rr_subparser(subparsers, "SPF", [("data", str)]) 92 | make_rr_subparser(subparsers, "URI", [("priority", int), ("weight", int), ("target", str)]) 93 | 94 | return line_parser 95 | 96 | 97 | def tokenize_line(line): 98 | """ 99 | Tokenize a line: 100 | * split tokens on whitespace 101 | * treat quoted strings as a single token 102 | * drop comments 103 | * handle escaped spaces and comment delimiters 104 | """ 105 | ret = [] 106 | escape = False 107 | quote = False 108 | tokbuf = "" 109 | ll = list(line) 110 | while len(ll) > 0: 111 | c = ll.pop(0) 112 | if c.isspace(): 113 | if not quote and not escape: 114 | # end of token 115 | if len(tokbuf) > 0: 116 | ret.append(tokbuf) 117 | 118 | tokbuf = "" 119 | elif quote: 120 | # in quotes 121 | tokbuf += c 122 | elif escape: 123 | # escaped space 124 | tokbuf += c 125 | escape = False 126 | else: 127 | tokbuf = "" 128 | 129 | continue 130 | 131 | if c == '\\': 132 | escape = True 133 | continue 134 | elif c == '"': 135 | if not escape: 136 | if quote: 137 | # end of quote 138 | ret.append(tokbuf) 139 | tokbuf = "" 140 | quote = False 141 | continue 142 | else: 143 | # beginning of quote 144 | quote = True 145 | continue 146 | elif c == ';': 147 | if not escape: 148 | # comment 149 | ret.append(tokbuf) 150 | tokbuf = "" 151 | break 152 | 153 | # normal character 154 | tokbuf += c 155 | escape = False 156 | 157 | if len(tokbuf.strip(" ").strip("\n")) > 0: 158 | ret.append(tokbuf) 159 | 160 | return ret 161 | 162 | 163 | def serialize(tokens): 164 | """ 165 | Serialize tokens: 166 | * quote whitespace-containing tokens 167 | * escape semicolons 168 | """ 169 | ret = [] 170 | for tok in tokens: 171 | if " " in tok: 172 | tok = '"%s"' % tok 173 | 174 | if ";" in tok: 175 | tok = tok.replace(";", "\;") 176 | 177 | ret.append(tok) 178 | 179 | return " ".join(ret) 180 | 181 | 182 | def remove_comments(text): 183 | """ 184 | Remove comments from a zonefile 185 | """ 186 | ret = [] 187 | lines = text.split("\n") 188 | for line in lines: 189 | if len(line) == 0: 190 | continue 191 | 192 | line = serialize(tokenize_line(line)) 193 | ret.append(line) 194 | 195 | return "\n".join(ret) 196 | 197 | 198 | def flatten(text): 199 | """ 200 | Flatten the text: 201 | * make sure each record is on one line. 202 | * remove parenthesis 203 | """ 204 | lines = text.split("\n") 205 | 206 | # tokens: sequence of non-whitespace separated by '' where a newline was 207 | tokens = [] 208 | for l in lines: 209 | if len(l) == 0: 210 | continue 211 | 212 | l = l.replace("\t", " ") 213 | tokens += filter(lambda x: len(x) > 0, l.split(" ")) + [''] 214 | 215 | # find (...) and turn it into a single line ("capture" it) 216 | capturing = False 217 | captured = [] 218 | 219 | flattened = [] 220 | while len(tokens) > 0: 221 | tok = tokens.pop(0) 222 | if not capturing and len(tok) == 0: 223 | # normal end-of-line 224 | if len(captured) > 0: 225 | flattened.append(" ".join(captured)) 226 | captured = [] 227 | continue 228 | 229 | if tok.startswith("("): 230 | # begin grouping 231 | tok = tok.lstrip("(") 232 | capturing = True 233 | 234 | if capturing and tok.endswith(")"): 235 | # end grouping. next end-of-line will turn this sequence into a flat line 236 | tok = tok.rstrip(")") 237 | capturing = False 238 | 239 | captured.append(tok) 240 | 241 | return "\n".join(flattened) 242 | 243 | 244 | def remove_class(text): 245 | """ 246 | Remove the CLASS from each DNS record, if present. 247 | The only class that gets used today (for all intents 248 | and purposes) is 'IN'. 249 | """ 250 | 251 | # see RFC 1035 for list of classes 252 | lines = text.split("\n") 253 | ret = [] 254 | for line in lines: 255 | tokens = tokenize_line(line) 256 | tokens_upper = [t.upper() for t in tokens] 257 | 258 | if "IN" in tokens_upper: 259 | tokens.remove("IN") 260 | elif "CS" in tokens_upper: 261 | tokens.remove("CS") 262 | elif "CH" in tokens_upper: 263 | tokens.remove("CH") 264 | elif "HS" in tokens_upper: 265 | tokens.remove("HS") 266 | 267 | ret.append(serialize(tokens)) 268 | 269 | return "\n".join(ret) 270 | 271 | 272 | def add_default_name(text): 273 | """ 274 | Go through each line of the text and ensure that 275 | a name is defined. Use '@' if there is none. 276 | """ 277 | global SUPPORTED_RECORDS 278 | 279 | lines = text.split("\n") 280 | ret = [] 281 | for line in lines: 282 | tokens = tokenize_line(line) 283 | if len(tokens) == 0: 284 | continue 285 | 286 | if tokens[0] in SUPPORTED_RECORDS and not tokens[0].startswith("$"): 287 | # add back the name 288 | tokens = ['@'] + tokens 289 | 290 | ret.append(serialize(tokens)) 291 | 292 | return "\n".join(ret) 293 | 294 | 295 | def parse_line(parser, record_token, parsed_records): 296 | """ 297 | Given the parser, capitalized list of a line's tokens, and the current set of records 298 | parsed so far, parse it into a dictionary. 299 | 300 | Return the new set of parsed records. 301 | Raise an exception on error. 302 | """ 303 | 304 | global SUPPORTED_RECORDS 305 | 306 | line = " ".join(record_token) 307 | 308 | # match parser to record type 309 | if len(record_token) >= 2 and record_token[1] in SUPPORTED_RECORDS: 310 | # with no ttl 311 | record_token = [record_token[1]] + record_token 312 | elif len(record_token) >= 3 and record_token[2] in SUPPORTED_RECORDS: 313 | # with ttl 314 | record_token = [record_token[2]] + record_token 315 | if record_token[0] == "TXT": 316 | record_token = record_token[:2] + ["--ttl"] + record_token[2:] 317 | try: 318 | rr, unmatched = parser.parse_known_args(record_token) 319 | assert len(unmatched) == 0, "Unmatched fields: %s" % unmatched 320 | except (SystemExit, AssertionError, InvalidLineException): 321 | # invalid argument 322 | raise InvalidLineException(line) 323 | 324 | record_dict = rr.__dict__ 325 | if record_token[0] == "TXT" and len(record_dict['txt']) == 1: 326 | record_dict['txt'] = record_dict['txt'][0] 327 | 328 | # what kind of record? including origin and ttl 329 | record_type = None 330 | for key in record_dict.keys(): 331 | if key in SUPPORTED_RECORDS and (key.startswith("$") or record_dict[key] == key): 332 | record_type = key 333 | if record_dict[key] == key: 334 | del record_dict[key] 335 | break 336 | 337 | assert record_type is not None, "Unknown record type in %s" % rr 338 | 339 | # clean fields 340 | for field in record_dict.keys(): 341 | if record_dict[field] is None: 342 | del record_dict[field] 343 | 344 | current_origin = record_dict.get('$ORIGIN', parsed_records.get('$ORIGIN', None)) 345 | 346 | # special record-specific fix-ups 347 | if record_type == 'PTR': 348 | record_dict['fullname'] = record_dict['name'] + '.' + current_origin 349 | 350 | if len(record_dict) > 0: 351 | if record_type.startswith("$"): 352 | # put the value directly 353 | record_dict_key = record_type.lower() 354 | parsed_records[record_dict_key] = record_dict[record_type] 355 | else: 356 | record_dict_key = record_type.lower() 357 | parsed_records[record_dict_key].append(record_dict) 358 | 359 | return parsed_records 360 | 361 | 362 | def parse_lines(text, ignore_invalid=False): 363 | """ 364 | Parse a zonefile into a dict. 365 | @text must be flattened--each record must be on one line. 366 | Also, all comments must be removed. 367 | """ 368 | json_zone_file = defaultdict(list) 369 | record_lines = text.split("\n") 370 | parser = make_parser() 371 | 372 | for record_line in record_lines: 373 | record_token = tokenize_line(record_line) 374 | try: 375 | json_zone_file = parse_line(parser, record_token, json_zone_file) 376 | except InvalidLineException: 377 | if ignore_invalid: 378 | continue 379 | else: 380 | raise 381 | 382 | return json_zone_file 383 | 384 | 385 | def parse_zone_file(text, ignore_invalid=False): 386 | """ 387 | Parse a zonefile into a dict 388 | """ 389 | text = remove_comments(text) 390 | text = flatten(text) 391 | text = remove_class(text) 392 | text = add_default_name(text) 393 | json_zone_file = parse_lines(text, ignore_invalid=ignore_invalid) 394 | return json_zone_file 395 | -------------------------------------------------------------------------------- /blockstack_zones/record_processors.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | 4 | def process_origin(data, template): 5 | """ 6 | Replace {$origin} in template with a serialized $ORIGIN record 7 | """ 8 | record = "" 9 | if data is not None: 10 | record += "$ORIGIN %s" % data 11 | 12 | return template.replace("{$origin}", record) 13 | 14 | 15 | def process_ttl(data, template): 16 | """ 17 | Replace {$ttl} in template with a serialized $TTL record 18 | """ 19 | record = "" 20 | if data is not None: 21 | record += "$TTL %s" % data 22 | 23 | return template.replace("{$ttl}", record) 24 | 25 | 26 | def process_soa(data, template): 27 | """ 28 | Replace {SOA} in template with a set of serialized SOA records 29 | """ 30 | record = template[:] 31 | 32 | if data is not None: 33 | 34 | assert len(data) == 1, "Only support one SOA RR at this time" 35 | data = data[0] 36 | 37 | soadat = [] 38 | domain_fields = ['mname', 'rname'] 39 | param_fields = ['serial', 'refresh', 'retry', 'expire', 'minimum'] 40 | 41 | for f in domain_fields + param_fields: 42 | assert f in data.keys(), "Missing '%s' (%s)" % (f, data) 43 | 44 | data_name = str(data.get('name', '@')) 45 | soadat.append(data_name) 46 | 47 | if data.get('ttl') is not None: 48 | soadat.append( str(data['ttl']) ) 49 | 50 | soadat.append("IN") 51 | soadat.append("SOA") 52 | 53 | for key in domain_fields: 54 | value = str(data[key]) 55 | soadat.append(value) 56 | 57 | soadat.append("(") 58 | 59 | for key in param_fields: 60 | value = str(data[key]) 61 | soadat.append(value) 62 | 63 | soadat.append(")") 64 | 65 | soa_txt = " ".join(soadat) 66 | record = record.replace("{soa}", soa_txt) 67 | 68 | else: 69 | # clear all SOA fields 70 | record = record.replace("{soa}", "") 71 | 72 | return record 73 | 74 | 75 | def quote_field(data, field): 76 | """ 77 | Quote a field in a list of DNS records. 78 | Return the new data records. 79 | """ 80 | if data is None: 81 | return None 82 | 83 | data_dup = copy.deepcopy(data) 84 | for i in xrange(0, len(data_dup)): 85 | data_dup[i][field] = '"%s"' % data_dup[i][field] 86 | data_dup[i][field] = data_dup[i][field].replace(";", "\;") 87 | 88 | return data_dup 89 | 90 | 91 | def process_rr(data, record_type, record_keys, field, template): 92 | """ 93 | Meta method: 94 | Replace $field in template with the serialized $record_type records, 95 | using @record_key from each datum. 96 | """ 97 | if data is None: 98 | return template.replace(field, "") 99 | 100 | if type(record_keys) == list: 101 | pass 102 | elif type(record_keys) == str: 103 | record_keys = [record_keys] 104 | else: 105 | raise ValueError("Invalid record keys") 106 | 107 | assert type(data) == list, "Data must be a list" 108 | 109 | record = "" 110 | for i in xrange(0, len(data)): 111 | 112 | for record_key in record_keys: 113 | assert record_key in data[i].keys(), "Missing '%s'" % record_key 114 | 115 | record_data = [] 116 | record_data.append( str(data[i].get('name', '@')) ) 117 | if data[i].get('ttl') is not None: 118 | record_data.append( str(data[i]['ttl']) ) 119 | 120 | record_data.append(record_type) 121 | record_data += [str(data[i][record_key]) for record_key in record_keys] 122 | record += " ".join(record_data) + "\n" 123 | 124 | return template.replace(field, record) 125 | 126 | 127 | def process_ns(data, template): 128 | """ 129 | Replace {ns} in template with the serialized NS records 130 | """ 131 | return process_rr(data, "NS", "host", "{ns}", template) 132 | 133 | 134 | def process_a(data, template): 135 | """ 136 | Replace {a} in template with the serialized A records 137 | """ 138 | return process_rr(data, "A", "ip", "{a}", template) 139 | 140 | 141 | def process_aaaa(data, template): 142 | """ 143 | Replace {aaaa} in template with the serialized A records 144 | """ 145 | return process_rr(data, "AAAA", "ip", "{aaaa}", template) 146 | 147 | 148 | def process_cname(data, template): 149 | """ 150 | Replace {cname} in template with the serialized CNAME records 151 | """ 152 | return process_rr(data, "CNAME", "alias", "{cname}", template) 153 | 154 | 155 | def process_alias(data, template): 156 | """ 157 | Replace {alias} in template with the serialized ALIAS records 158 | """ 159 | return process_rr(data, "ALIAS", "host", "{alias}", template) 160 | 161 | 162 | def process_mx(data, template): 163 | """ 164 | Replace {mx} in template with the serialized MX records 165 | """ 166 | return process_rr(data, "MX", ["preference", "host"], "{mx}", template) 167 | 168 | 169 | def process_ptr(data, template): 170 | """ 171 | Replace {ptr} in template with the serialized PTR records 172 | """ 173 | return process_rr(data, "PTR", "host", "{ptr}", template) 174 | 175 | 176 | def process_txt(data, template): 177 | """ 178 | Replace {txt} in template with the serialized TXT records 179 | """ 180 | if data is None: 181 | to_process = None 182 | else: 183 | # quote txt 184 | to_process = copy.deepcopy(data) 185 | for datum in to_process: 186 | if isinstance(datum["txt"], list): 187 | datum["txt"] = " ".join(['"%s"' % entry.replace(";", "\;") 188 | for entry in datum["txt"]]) 189 | else: 190 | datum["txt"] = '"%s"' % datum["txt"].replace(";", "\;") 191 | return process_rr(to_process, "TXT", "txt", "{txt}", template) 192 | 193 | 194 | def process_srv(data, template): 195 | """ 196 | Replace {srv} in template with the serialized SRV records 197 | """ 198 | return process_rr(data, "SRV", ["priority", "weight", "port", "target"], "{srv}", template) 199 | 200 | 201 | def process_spf(data, template): 202 | """ 203 | Replace {spf} in template with the serialized SPF records 204 | """ 205 | return process_rr(data, "SPF", "data", "{spf}", template) 206 | 207 | 208 | def process_uri(data, template): 209 | """ 210 | Replace {uri} in templtae with the serialized URI records 211 | """ 212 | # quote target 213 | data_dup = quote_field(data, "target") 214 | return process_rr(data_dup, "URI", ["priority", "weight", "target"], "{uri}", template) 215 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | DNS Zone File 4 | ============== 5 | 6 | """ 7 | 8 | from setuptools import setup, find_packages 9 | 10 | setup( 11 | name='blockstack-zones', 12 | version='0.19.0.0', 13 | url='https://github.com/blockstack/dns-zone-file-py', 14 | license='MIT', 15 | author='Blockstack Developers', 16 | author_email='hello@onename.com', 17 | description=("Library for creating and parsing DNS zone files"), 18 | keywords='dns zone file zonefile parse create', 19 | packages=find_packages(), 20 | zip_safe=False, 21 | install_requires=[ 22 | ], 23 | classifiers=[ 24 | 'Intended Audience :: Developers', 25 | 'License :: OSI Approved :: MIT License', 26 | 'Operating System :: OS Independent', 27 | 'Programming Language :: Python', 28 | 'Topic :: Internet', 29 | 'Topic :: Software Development :: Libraries :: Python Modules', 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /test_sample_data.py: -------------------------------------------------------------------------------- 1 | zone_files = { 2 | "sample_1": """ 3 | $ORIGIN example.com 4 | $TTL 86400 5 | @ 10800 IN A 217.70.184.38 6 | blog 10800 IN CNAME blogs.vip.gandi.net. 7 | imap 10800 IN CNAME access.mail.gandi.net. 8 | pop 10800 IN CNAME access.mail.gandi.net. 9 | smtp 10800 IN CNAME relay.mail.gandi.net. 10 | webmail 10800 IN CNAME webmail.gandi.net. 11 | www 10800 IN CNAME webredir.vip.gandi.net. 12 | aname 10800 IN ALIAS otherdomain.com. 13 | @ 10800 IN MX 50 fb.mail.gandi.net. 14 | @ 10800 IN MX 10 spool.mail.gandi.net.""", 15 | "sample_2": """ 16 | $ORIGIN example.com 17 | $TTL 86400 18 | 19 | server1 IN A 10.0.1.5 20 | server2 IN A 10.0.1.7 21 | dns1 IN A 10.0.1.2 22 | dns2 IN A 10.0.1.3 23 | 24 | ftp IN CNAME server1 25 | mail IN CNAME server1 26 | mail2 IN CNAME server2 27 | www IN CNAME server2 28 | 29 | aname IN ALIAS otherdomain.com""", 30 | "sample_3": """$ORIGIN example.com 31 | $TTL 86400 32 | @ IN SOA dns1.example.com. hostmaster.example.com. ( 33 | 2001062501 ; serial 34 | 21600 ; refresh after 6 hours 35 | 3600 ; retry after 1 hour 36 | 604800 ; expire after 1 week 37 | 86400 ) ; minimum TTL of 1 day 38 | 39 | IN NS dns1.example.com. 40 | IN NS dns2.example.com. 41 | 42 | IN MX 10 mail.example.com. 43 | IN MX 20 mail2.example.com. 44 | 45 | IN A 10.0.1.5 46 | 47 | server1 IN A 10.0.1.5 48 | server2 IN A 10.0.1.7 49 | dns1 IN A 10.0.1.2 50 | dns2 IN A 10.0.1.3 51 | 52 | ftp IN CNAME server1 53 | mail IN CNAME server1 54 | mail2 IN CNAME server2 55 | www IN CNAME server2 56 | 57 | aname IN ALIAS otherdomain.com""", 58 | "sample_txt_1": """$ORIGIN example.com 59 | $TTL 86400 60 | @ IN SOA dns1.example.com. hostmaster.example.com. ( 61 | 2001062501 ; serial 62 | 21600 ; refresh after 6 hours 63 | 3600 ; retry after 1 hour 64 | 604800 ; expire after 1 week 65 | 86400 ) ; minimum TTL of 1 day 66 | 67 | IN NS dns1.example.com. 68 | IN NS dns2.example.com. 69 | 70 | IN MX 10 mail.example.com. 71 | IN MX 20 mail2.example.com. 72 | 73 | IN A 10.0.1.5 74 | 75 | server1 IN A 10.0.1.5 76 | server2 IN A 10.0.1.7 77 | dns1 IN A 10.0.1.2 78 | dns2 IN A 10.0.1.3 79 | 80 | ftp IN CNAME server1 81 | mail IN CNAME server1 82 | mail2 IN CNAME server2 83 | www IN CNAME server2 84 | 85 | single IN TXT "everything I do" 86 | singleTTL 100 IN TXT "everything I do" 87 | multi IN TXT "everything I do" "I do for you" 88 | multiTTL 100 IN TXT "everything I do" "I do for you" 89 | """ 90 | } 91 | 92 | zone_file_objects = { 93 | "sample_1": { 94 | "$origin": "naval.id", 95 | "$ttl": "3600", 96 | "uri": [{ 97 | "name": "@", 98 | "ttl": "1D", 99 | "priority": 1, 100 | "weight": 10, 101 | "target": "https://mq9.s3.amazonaws.com/naval.id/profile.json" 102 | }] 103 | }, 104 | "sample_2": { 105 | "$origin": "MYDOMAIN.COM.", 106 | "$ttl": 3600, 107 | "soa": { 108 | "mname": "NS1.NAMESERVER.NET.", 109 | "rname": "HOSTMASTER.MYDOMAIN.COM.", 110 | "serial": "{time}", 111 | "refresh": 3600, 112 | "retry": 600, 113 | "expire": 604800, 114 | "minimum": 86400 115 | }, 116 | "ns": [ 117 | { "host": "NS1.NAMESERVER.NET." }, 118 | { "host": "NS2.NAMESERVER.NET." } 119 | ], 120 | "a": [ 121 | { "name": "@", "ip": "127.0.0.1" }, 122 | { "name": "www", "ip": "127.0.0.1" }, 123 | { "name": "mail", "ip": "127.0.0.1" } 124 | ], 125 | "aaaa": [ 126 | { "ip": "::1" }, 127 | { "name": "mail", "ip": "2001:db8::1" } 128 | ], 129 | "cname": [ 130 | { "name": "mail1", "alias": "mail" }, 131 | { "name": "mail2", "alias": "mail" } 132 | ], 133 | "alias":[ 134 | { "name": "aname", "host": "otherdomain.com"} 135 | ], 136 | "mx": [ 137 | { "preference": 0, "host": "mail1" }, 138 | { "preference": 10, "host": "mail2" } 139 | ], 140 | "txt": [ 141 | { "name": "txt1", "txt": "hello" }, 142 | { "name": "txt2", "txt": "world" } 143 | ], 144 | "srv": [ 145 | { "name": "_xmpp-client._tcp", "target": "jabber", "priority": 10, "weight": 0, "port": 5222 }, 146 | { "name": "_xmpp-server._tcp", "target": "jabber", "priority": 10, "weight": 0, "port": 5269 } 147 | ] 148 | }, 149 | "sample_3": { 150 | "$origin": "MYDOMAIN.COM.", 151 | "$ttl": 3600, 152 | "soa": { 153 | "mname": "NS1.NAMESERVER.NET.", 154 | "rname": "HOSTMASTER.MYDOMAIN.COM.", 155 | "serial": "{time}", 156 | "refresh": 3600, 157 | "retry": 600, 158 | "expire": 604800, 159 | "minimum": 86400 160 | }, 161 | "ns": [ 162 | { "host": "NS1.NAMESERVER.NET." }, 163 | { "host": "NS2.NAMESERVER.NET." } 164 | ], 165 | "a": [ 166 | { "name": "@", "ip": "127.0.0.1" }, 167 | { "name": "www", "ip": "127.0.0.1" }, 168 | { "name": "mail", "ip": "127.0.0.1" } 169 | ], 170 | "aaaa": [ 171 | { "ip": "::1" }, 172 | { "name": "mail", "ip": "2001:db8::1" } 173 | ], 174 | "cname":[ 175 | { "name": "mail1", "alias": "mail" }, 176 | { "name": "mail2", "alias": "mail" } 177 | ], 178 | "alias":[ 179 | { "name": "aname", "host": "otherdomain.com"} 180 | ], 181 | "mx":[ 182 | { "preference": 0, "host": "mail1" }, 183 | { "preference": 10, "host": "mail2" } 184 | ] 185 | }, 186 | "sample_txt_1": { 187 | "$origin": "MYDOMAIN.COM.", 188 | "$ttl": 3600, 189 | "soa": { 190 | "mname": "NS1.NAMESERVER.NET.", 191 | "rname": "HOSTMASTER.MYDOMAIN.COM.", 192 | "serial": "4000", 193 | "refresh": 3600, 194 | "retry": 600, 195 | "expire": 604800, 196 | "minimum": 86400 197 | }, 198 | "ns": [ 199 | { "host": "NS1.NAMESERVER.NET." }, 200 | { "host": "NS2.NAMESERVER.NET." } 201 | ], 202 | "a": [ 203 | { "name": "@", "ip": "127.0.0.1" }, 204 | { "name": "www", "ip": "127.0.0.1" }, 205 | { "name": "mail", "ip": "127.0.0.1" } 206 | ], 207 | "aaaa": [ 208 | { "ip": "::1" }, 209 | { "name": "mail", "ip": "2001:db8::1" } 210 | ], 211 | "cname":[ 212 | { "name": "mail1", "alias": "mail" }, 213 | { "name": "mail2", "alias": "mail" } 214 | ], 215 | "mx":[ 216 | { "preference": 0, "host": "mail1" }, 217 | { "preference": 10, "host": "mail2" } 218 | ], 219 | "txt":[ 220 | {'name' : 'single', 'txt': 'everything I do'}, 221 | {'name' : 'singleTTL', 'ttl': 100, 'txt': 'everything I do'}, 222 | {'name' : 'multi', 'txt': ['everything I do', 'I do for you']}, 223 | {'name' : 'multiTTL', 'ttl': 100, 'txt': ['everything I do', 'I do for you']} 224 | ] 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /tests/zonefile_forward.json: -------------------------------------------------------------------------------- 1 | { 2 | "$ORIGIN": "MYDOMAIN.COM.", 3 | "$TTL": 3600, 4 | "SOA": { 5 | "mname": "NS1.NAMESERVER.NET.", 6 | "rname": "HOSTMASTER.MYDOMAIN.COM.", 7 | "serial": "{time}", 8 | "refresh": 3600, 9 | "retry": 600, 10 | "expire": 604800, 11 | "minimum": 86400 12 | }, 13 | "NS": [ 14 | { "host": "NS1.NAMESERVER.NET." }, 15 | { "host": "NS2.NAMESERVER.NET." } 16 | ], 17 | "A": [ 18 | { "name": "@", "ip": "127.0.0.1" }, 19 | { "name": "www", "ip": "127.0.0.1" }, 20 | { "name": "mail", "ip": "127.0.0.1" } 21 | ], 22 | "AAAA": [ 23 | { "ip": "::1" }, 24 | { "name": "mail", "ip": "2001:db8::1" } 25 | ], 26 | "CNAME":[ 27 | { "name": "mail1", "alias": "mail" }, 28 | { "name": "mail2", "alias": "mail" } 29 | ], 30 | "ALIAS":[ 31 | { "name": "aname", "alias": "otherdomain.com"} 32 | ], 33 | "MX":[ 34 | { "preference": 0, "host": "mail1" }, 35 | { "preference": 10, "host": "mail2" } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /tests/zonefile_forward.txt: -------------------------------------------------------------------------------- 1 | $ORIGIN MYDOMAIN.COM. 2 | $TTL 3600 3 | @ IN SOA NS1.NAMESERVER.NET. HOSTMASTER.MYDOMAIN.COM. ( 4 | 1406291485 ;serial 5 | 3600 ;refresh 6 | 600 ;retry 7 | 604800 ;expire 8 | 86400 ;minimum ttl 9 | ) 10 | 11 | @ NS NS1.NAMESERVER.NET. 12 | @ NS NS2.NAMESERVER.NET. 13 | 14 | @ MX 0 mail1 15 | @ MX 10 mail2 16 | 17 | A 1.1.1.1 18 | @ A 127.0.0.1 19 | www A 127.0.0.1 20 | mail A 127.0.0.1 21 | A 1.2.3.4 22 | tst 300 IN A 101.228.10.127;this is a comment 23 | 24 | @ AAAA ::1 25 | mail AAAA 2001:db8::1 26 | 27 | mail1 CNAME mail 28 | mail2 CNAME mail 29 | 30 | treefrog.ca. IN TXT "v=spf1 a mx a:mail.treefrog.ca a:webmail.treefrog.ca ip4:76.75.250.33 ?all" 31 | treemonkey.ca. IN TXT "v=DKIM1\; k=rsa\; p=MIGf..." 32 | -------------------------------------------------------------------------------- /tests/zonefile_reverse.json: -------------------------------------------------------------------------------- 1 | { 2 | "$ORIGIN": "0.168.192.IN-ADDR.ARPA.", 3 | "$TTL": 3600, 4 | "SOA": { 5 | "mname": "NS1.NAMESERVER.NET.", 6 | "rname": "HOSTMASTER.MYDOMAIN.COM.", 7 | "serial": 1406291485, 8 | "refresh": 3600, 9 | "retry": 600, 10 | "expire": 604800, 11 | "minimum": 86400 12 | }, 13 | "NS": [ 14 | { "host": "NS1.NAMESERVER.NET." }, 15 | { "host": "NS2.NAMESERVER.NET." } 16 | ], 17 | "PTR":[ 18 | { "name": 1, "host": "HOST1.MYDOMAIN.COM." }, 19 | { "name": 2, "host": "HOST2.MYDOMAIN.COM." } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /tests/zonefile_reverse.txt: -------------------------------------------------------------------------------- 1 | $ORIGIN 0.168.192.IN-ADDR.ARPA. 2 | $TTL 3600 3 | @ IN SOA NS1.NAMESERVER.NET. HOSTMASTER.MYDOMAIN.COM. ( 4 | 1406291485 ;serial 5 | 3600 ;refresh 6 | 600 ;retry 7 | 604800 ;expire 8 | 86400 ;minimum ttl 9 | ) 10 | 11 | @ NS NS1.NAMESERVER.NET. 12 | @ NS NS2.NAMESERVER.NET. 13 | 14 | 1 PTR HOST1.MYDOMAIN.COM. 15 | 2 PTR HOST2.MYDOMAIN.COM. 16 | 17 | $ORIGIN 30.168.192.in-addr.arpa. 18 | 3 PTR HOST3.MYDOMAIN.COM. 19 | 4 PTR HOST4.MYDOMAIN.COM. 20 | PTR HOST5.MYDOMAIN.COM. 21 | 22 | $ORIGIN 168.192.in-addr.arpa. 23 | 10.3 PTR HOST3.MYDOMAIN.COM. 24 | 10.4 PTR HOST4.MYDOMAIN.COM. 25 | -------------------------------------------------------------------------------- /tests/zonefile_reverse_ipv6.json: -------------------------------------------------------------------------------- 1 | { 2 | "$ORIGIN": "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.", 3 | "$TTL": 3600, 4 | "SOA": { 5 | "mname": "NS1.NAMESERVER.NET.", 6 | "rname": "HOSTMASTER.MYDOMAIN.COM.", 7 | "serial": "{time}", 8 | "refresh": 3600, 9 | "retry": 600, 10 | "expire": 604800, 11 | "minimum": 86400 12 | }, 13 | "NS": [ 14 | { "host": "NS1.NAMESERVER.NET." }, 15 | { "host": "NS2.NAMESERVER.NET." } 16 | ], 17 | "PTR":[ 18 | { "name": 1, "host": "HOST1.MYDOMAIN.COM." }, 19 | { "name": 2, "host": "HOST2.MYDOMAIN.COM." } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /tests/zonefile_reverse_ipv6.txt: -------------------------------------------------------------------------------- 1 | $ORIGIN 0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa. 2 | $TTL 3600 3 | @ IN SOA NS1.NAMESERVER.NET. HOSTMASTER.MYDOMAIN.COM. ( 4 | 1406291485 ;serial 5 | 3600 ;refresh 6 | 600 ;retry 7 | 604800 ;expire 8 | 86400 ;minimum ttl 9 | ) 10 | 11 | @ NS NS1.NAMESERVER.NET. 12 | @ NS NS2.NAMESERVER.NET. 13 | 14 | 1 PTR HOST1.MYDOMAIN.COM. 15 | 2 PTR HOST2.MYDOMAIN.COM. 16 | -------------------------------------------------------------------------------- /unit_tests.py: -------------------------------------------------------------------------------- 1 | import json 2 | import traceback 3 | import unittest 4 | from test import test_support 5 | from blockstack_zones import make_zone_file, parse_zone_file 6 | from test_sample_data import zone_files, zone_file_objects 7 | 8 | class ZoneFileTests(unittest.TestCase): 9 | def setUp(self): 10 | pass 11 | 12 | def tearDown(self): 13 | pass 14 | 15 | def test_zone_file_parsing_txt(self): 16 | zone_file = parse_zone_file(zone_files["sample_txt_1"]) 17 | self.assertTrue(isinstance(zone_file, dict)) 18 | self.assertTrue("soa" in zone_file) 19 | self.assertTrue("mx" in zone_file) 20 | self.assertTrue("ns" in zone_file) 21 | self.assertTrue("a" in zone_file) 22 | self.assertTrue("cname" in zone_file) 23 | self.assertTrue("$ttl" in zone_file) 24 | self.assertTrue("$origin" in zone_file) 25 | self.assertTrue("txt" in zone_file) 26 | self.assertEqual(zone_file["txt"][0]["name"], "single") 27 | self.assertEqual(zone_file["txt"][0]["txt"], "everything I do") 28 | self.assertEqual(zone_file["txt"][1]["name"], "singleTTL") 29 | self.assertEqual(zone_file["txt"][1]["ttl"], 100) 30 | self.assertEqual(zone_file["txt"][1]["txt"], "everything I do") 31 | self.assertEqual(zone_file["txt"][2]["name"], "multi") 32 | self.assertEqual(zone_file["txt"][2]["txt"], 33 | ["everything I do", "I do for you"]) 34 | self.assertEqual(zone_file["txt"][3]["name"], "multiTTL") 35 | self.assertEqual(zone_file["txt"][3]["ttl"], 100) 36 | self.assertEqual(zone_file["txt"][3]["txt"], 37 | ["everything I do", "I do for you"]) 38 | 39 | def test_zone_file_creation_txt(self): 40 | json_zone_file = zone_file_objects["sample_txt_1"] 41 | zone_file = make_zone_file(json_zone_file) 42 | print zone_file 43 | self.assertTrue(isinstance(zone_file, (unicode, str))) 44 | self.assertTrue("$ORIGIN" in zone_file) 45 | self.assertTrue("$TTL" in zone_file) 46 | self.assertTrue("@ IN SOA" in zone_file) 47 | 48 | zone_file = parse_zone_file(zone_file) 49 | self.assertTrue(isinstance(zone_file, dict)) 50 | self.assertTrue("soa" in zone_file) 51 | self.assertTrue("mx" in zone_file) 52 | self.assertTrue("ns" in zone_file) 53 | self.assertTrue("a" in zone_file) 54 | self.assertTrue("cname" in zone_file) 55 | self.assertTrue("$ttl" in zone_file) 56 | self.assertTrue("$origin" in zone_file) 57 | self.assertTrue("txt" in zone_file) 58 | self.assertEqual(zone_file["txt"][0]["name"], "single") 59 | self.assertEqual(zone_file["txt"][0]["txt"], "everything I do") 60 | self.assertEqual(zone_file["txt"][1]["name"], "singleTTL") 61 | self.assertEqual(zone_file["txt"][1]["ttl"], 100) 62 | self.assertEqual(zone_file["txt"][1]["txt"], "everything I do") 63 | self.assertEqual(zone_file["txt"][2]["name"], "multi") 64 | self.assertEqual(zone_file["txt"][2]["txt"], 65 | ["everything I do", "I do for you"]) 66 | self.assertEqual(zone_file["txt"][3]["name"], "multiTTL") 67 | self.assertEqual(zone_file["txt"][3]["ttl"], 100) 68 | self.assertEqual(zone_file["txt"][3]["txt"], 69 | ["everything I do", "I do for you"]) 70 | 71 | def test_zone_file_creation_1(self): 72 | json_zone_file = zone_file_objects["sample_1"] 73 | zone_file = make_zone_file(json_zone_file) 74 | print zone_file 75 | self.assertTrue(isinstance(zone_file, (unicode, str))) 76 | self.assertTrue("$ORIGIN" in zone_file) 77 | self.assertTrue("$TTL" in zone_file) 78 | self.assertTrue("@ 1D URI" in zone_file) 79 | 80 | def test_zone_file_creation_2(self): 81 | json_zone_file = zone_file_objects["sample_2"] 82 | zone_file = make_zone_file(json_zone_file) 83 | print zone_file 84 | self.assertTrue(isinstance(zone_file, (unicode, str))) 85 | self.assertTrue("$ORIGIN" in zone_file) 86 | self.assertTrue("$TTL" in zone_file) 87 | self.assertTrue("@ IN SOA" in zone_file) 88 | 89 | def test_zone_file_creation_3(self): 90 | json_zone_file = zone_file_objects["sample_3"] 91 | zone_file = make_zone_file(json_zone_file) 92 | print zone_file 93 | self.assertTrue(isinstance(zone_file, (unicode, str))) 94 | self.assertTrue("$ORIGIN" in zone_file) 95 | self.assertTrue("$TTL" in zone_file) 96 | self.assertTrue("@ IN SOA" in zone_file) 97 | 98 | def test_zone_file_parsing_1(self): 99 | zone_file = parse_zone_file(zone_files["sample_1"]) 100 | print json.dumps(zone_file, indent=2) 101 | self.assertTrue(isinstance(zone_file, dict)) 102 | self.assertTrue("a" in zone_file) 103 | self.assertTrue("cname" in zone_file) 104 | self.assertTrue("alias" in zone_file) 105 | self.assertTrue("mx" in zone_file) 106 | self.assertTrue("$ttl" in zone_file) 107 | self.assertTrue("$origin" in zone_file) 108 | 109 | def test_zone_file_parsing_2(self): 110 | zone_file = parse_zone_file(zone_files["sample_2"]) 111 | #print json.dumps(zone_file, indent=2) 112 | self.assertTrue(isinstance(zone_file, dict)) 113 | self.assertTrue("a" in zone_file) 114 | self.assertTrue("cname" in zone_file) 115 | self.assertTrue("alias" in zone_file) 116 | self.assertTrue("$ttl" in zone_file) 117 | self.assertTrue("$origin" in zone_file) 118 | 119 | def test_zone_file_parsing_3(self): 120 | zone_file = parse_zone_file(zone_files["sample_3"]) 121 | #print json.dumps(zone_file, indent=2) 122 | self.assertTrue(isinstance(zone_file, dict)) 123 | self.assertTrue("soa" in zone_file) 124 | self.assertTrue("mx" in zone_file) 125 | self.assertTrue("ns" in zone_file) 126 | self.assertTrue("a" in zone_file) 127 | self.assertTrue("cname" in zone_file) 128 | self.assertTrue("alias" in zone_file) 129 | self.assertTrue("$ttl" in zone_file) 130 | self.assertTrue("$origin" in zone_file) 131 | 132 | def test_main(): 133 | test_support.run_unittest( 134 | ZoneFileTests 135 | ) 136 | 137 | 138 | if __name__ == '__main__': 139 | test_main() 140 | --------------------------------------------------------------------------------