Changeset 5520

Show
Ignore:
Timestamp:
06/18/12 17:15:33 (7 years ago)
Author:
sebass63
Message:

Re-created hosts.txt functionality for zenodotus. This version does not support custom SOA or NS records yet,
but it won't take much to get it to that point.

Location:
seattle/trunk/zenodotus
Files:
1 added
2 modified

Legend:

Unmodified
Added
Removed
  • seattle/trunk/zenodotus/zenodotus.repy

    r5511 r5520  
    5858include ntp_time.repy 
    5959 
    60  
    61  
     60# This is the records we currently have cached. Should be iterated through  
     61# on a regular basis, removing expired entries. In addition, we should  
     62# decrement the entries; ttl values on each check. 
     63record_cache = [] 
    6264 
    6365# This is the serial number which zenodotus will advertise in SOA records. 
     
    7779 
    7880 
     81# Searches through the cache and returns all relevant records. 
     82def _search_cache(record_name, record_type): 
     83  answers = [] 
     84 
     85  for record in record_cache: 
     86    if (record["name"] == record_name and record["type"] == record_type): 
     87      answers.append(record) 
     88 
     89  return answers 
     90 
     91 
     92 
    7993def _do_dns_callback(query_dictionary): 
    8094  """ 
     
    8599 
    86100  <Arguments> 
    87     A dictionary describing the DNS packet we've received. It has the  
    88     following format: 
    89  
    90     { 
    91         'raw_data': <long string> 
    92         'communication_id': 'a7' 
    93         'query_response': False 
    94         'operation_code': 0 
    95         'authority_advisory': False 
    96         'truncation': False 
    97         'recursion_desired': True 
    98         'recursion_accepted': False 
    99         'z': False 
    100         'authentic_data': False 
    101         'checking_disabled': False 
    102         'error_code': 0 
    103         'question_count': 1 
    104         'answer_count': 0 
    105         'nscount': 0 
    106         'arcount': 0 
    107         'question_name': "asdf" <--- This is the query, such as a hostname. 
    108         'question_type': 0 
    109         'question_class': 0 
    110         'answers': [] 
    111     } (Numbers are integers) 
     101    A dictionary describing the DNS packet we've received. For format  
     102    specifics, please refer to dnsserver.repy. 
    112103 
    113104  <Exceptions> 
     
    150141    with strings received from the advertise service. 
    151142  """ 
    152   # Record the time at which this method begins. 
    153   method_time = getruntime() 
    154  
    155   # These types are all dealt with in the same way, format-wise. 
    156   simple_answers = ['NS', 'CNAME', 'MD', 'MB', 'MF', 'MG', 'MR', 'MX', 'PTR'] 
    157  
    158   # No doubt the debugger will be interested in a query dump. 
    159   if _verbose: 
    160     print "====================================" 
    161     print "| Callback started with arguments: |" 
    162     print "====================================" 
    163     for key in query_dictionary.keys(): 
    164       print "  " + str(key) + " : " + str(query_dictionary[key]) 
    165     print "====================================\n" 
     143  # Prepare a new dictionary for responding. 
     144  response_dictionary = {} 
    166145 
    167146  try: 
    168     query_dictionary['query_response'] = True 
    169     query_dictionary['authority_advisory'] = True 
    170     query_dictionary['recursion_accepted'] = True 
     147    # IQUERY and STATUS operations not yet implemented. (Worthwhile?) 
     148    if query_dictionary['operation_code'] != 0: 
     149      raise NotImplementedError() 
     150 
     151    response_dictionary['questions'] = query_dictionary['questions'] 
     152    response_dictionary['query_response'] = True      # Always true 
     153    response_dictionary['recursion_desired'] = query_dictionary['recursion_desired'] 
     154    response_dictionary['recursion_accepted'] = True  # We accept regardless 
     155                                                      # of requests. 
     156    response_dictionary['authority_advisory'] = False # Set to true later,  
     157                                                      # if valid. 
    171158 
    172159    # The domain name service imposes an artificial 512 byte limit on all  
     
    175162    # and we have to service requests from IPv6 users as well. As such, we  
    176163    # can only depend on having 512 - 128 (384) bytes available for any  
    177     # given message. If they exceed 512 in total, they will be truncated,  
    178     # and we will start to lose answers in our responses. However,  
    179     # dnsserver currently doesn't allow packets it sends to be too long,  
    180     # so truncation should never come up. 
    181     query_dictionary['truncation'] = False 
    182  
    183     # dnsserver does not currently support inverse queries, so we require  
    184     # opcode to be equal to zero always. 
    185     if query_dictionary['operation_code'] != 0: 
    186       raise NotImplementedError() 
     164    # given message. If this limit is exceeded, the packet may be truncated  
     165    # at some point between server and client. At that time, this bit will  
     166    # be set to true. 
     167    response_dictionary['truncation'] = False 
     168 
     169    # Other entries we already know. 
     170    response_dictionary['operation_code'] = 0 
     171    response_dictionary['communication_id'] = query_dictionary['communication_id'] 
     172    response_dictionary['z'] = 0 
     173    response_dictionary['error_code'] = 0 
     174    response_dictionary['checking_disabled'] = query_dictionary['checking_disabled'] 
     175    response_dictionary['question_count'] = len(response_dictionary['questions']) 
     176    response_dictionary['authentic_data'] = True 
     177 
     178    answers = [] 
    187179 
    188180    # Iterate through the enclosed questions and respond appropriately. 
     
    195187      if question_class != 'IN': 
    196188        raise NotImplementedError("Non-IN query is unacceptable.") 
     189 
     190      cached_values = _search_cache(question_name, question_type) 
     191      if len(cached_values) > 0: 
     192        for i in range(len(cached_values)): 
     193          answers.append(cached_values[i]) 
     194 
     195      if len(answers) == 0: # We found nothing in the cache. 
     196        string_answers = zenodotus_advertise_do_lookup(question_name, question_type) 
     197         
     198    for entry in string_answers: 
    197199      if question_type == 'A': 
    198         if question_name == 'zenodotus.cs.washington.edu': 
    199           query_dictionary['answers'].append({ 
    200                'name': question_name,  
    201                'type': question_type, 
    202                'class': question_class, 
    203                'time_to_live': 0, 
    204                'answer_data': { 'address': _host_ip_address } }) 
    205           query_dictionary['answer_count'] += 1 
    206         elif question_name == 'localhost': 
    207           query_dictionary['answers'].append({ 
    208                'name': question_name, 
    209                'type': question_type, 
    210                'class': question_class, 
    211                'time_to_live': 0, 
    212                'answer_data': { 'address': "127.0.0.1" } }) 
    213           query_dictionary['answer_count'] += 1 
    214         elif question_name == 'localhost.zenodotus.cs.washington.edu': 
    215           query_dictionary['answers'].append({ 
    216                'name': question_name, 
    217                'type': question_type, 
    218                'class': question_class, 
    219                'time_to_live': 0, 
    220                'answer_data': { 'address': "127.0.0.1" } }) 
    221           query_dictionary['answer_count'] += 1 
    222  
    223         # This part involves a lot of compact and complicated string operations. 
    224         # It is slightly horrifying. It handles redirection to additional name servers 
    225         elif question_name.endswith("zenodotus.cs.washington.edu") and len(question_name.split(".")) > 5: 
    226           service_name = question_name[:-28].split(".")[len(question_name[:-28].split(".")) - 1] 
    227           lookup_name = service_name + ".zenodotus.cs.washington.edu" 
    228  
    229           answers = zenodotus_advertise_do_lookup(lookup_name, 'NS') 
    230  
    231           for answer in answers: 
    232             query_dictionary['answers'].append({ 
    233                  'name': lookup_name, 
    234                  'type': 'NS', 
    235                  'class': question_class, 
    236                  'time_to_live': 0, 
    237                  'answer_data': { 'address': answer } }) 
    238  
    239           query_dictionary['answer_count'] += len(answers) 
    240  
    241         else: 
    242           # Begin "stopwatch" for the advertise stuff. 
    243           pre_advertise_time = getruntime() 
    244            
    245           # Get advertise data 
    246           answers = zenodotus_advertise_do_lookup(question_name, question_type) 
    247  
    248           if _verbose: 
    249             advertise_time = getruntime() - pre_advertise_time 
    250             print "::NOTICE:: Advertise lookup took " + str(advertise_time) + " seconds.\n" 
    251  
    252           query_dictionary['answer_count'] += len(answers) 
    253  
    254           for answer in answers: 
    255             query_dictionary['answers'].append({ 
    256                  'name': question_name, 
    257                  'type': question_type, 
    258                  'class': question_class, 
    259                  'time_to_live': 0, 
    260                  'answer_data': { 'address': answer } }) 
    261  
    262       elif question_type == 'NS': 
    263  
    264         if question_name == 'zenodotus.cs.washington.edu': 
    265           query_dictionary['answers'].append({ 
    266                'name': question_name, 
    267                'type': question_type, 
    268                'class': question_class, 
    269                'time_to_live': 0, 
    270                'answer_data': { 'address': 'zenodotus.cs.washington.edu' } }) 
    271  
    272           query_dictionary['answer_count'] += 1 
    273  
    274           query_dictionary['answers'].append({ 
    275                'name': question_name, 
    276                'type': question_type, 
    277                'class': question_class, 
    278                'time_to_live': 0, 
    279                'answer_data': { 'address': 'zenodotus.cs.washington.edu' } }) 
    280  
    281           query_dictionary['additional_record_count'] += 1 
    282  
    283         else: 
    284           pre_advertise_time = getruntime() 
    285  
    286           answers = zenodotus_advertise_do_lookup(question_name, question_type) 
    287  
    288           if _verbose: 
    289             advertise_time = getruntime() - pre_advertise_time 
    290             print "::NOTICE:: Advertise lookup took " + str(advertise_time) + " seconds.\n" 
    291  
    292           for answer in answers: 
    293             query_dictionary['answers'].append({ 
    294                  'name': question_name, 
    295                  'type': question_type, 
    296                  'class': question_class, 
    297                  'time_to_live': 0, 
    298                  'answer_data': { 'address': answer } }) 
    299  
    300           query_dictionary['answer_count'] += len(answers) 
    301  
    302  
    303       elif question_type == 'SOA': 
    304         if question_name != "zenodotus.cs.washington.edu": 
    305           pre_advertise_time = getruntime() 
    306            
    307           answers = zenodotus_advertise_do_lookup(question_name, question_type) 
    308  
    309           if _verbose: 
    310             advertise_time = getruntime() - pre_advertise_time 
    311             print "::NOTICE:: Advertise lookup took " + str(advertise_time) + " seconds.\n" 
    312  
    313           query_dictionary['answer_count'] += len(answers) 
    314  
    315           for answer in answers: 
    316             answer_data = { 
    317                 'mname': answer, 
    318                 'rname': 'autogen.noaddress', 
    319                 'serial': _SOA_SERIAL, 
    320                 'retry': 900, 
    321                 'refresh': 10800, 
    322                 'expire': 3600000, 
    323                 'minimum': 1 } 
    324  
    325             query_dictionary['answers'].append({ 
    326                 'name': question_name, 
    327                 'type': question_type, 
    328                 'class': question_class, 
    329                 'time_to_live': 0, 
    330                 'answer_data': answer_data }) 
    331         else: 
    332           query_dictionary['answer_count'] += 1 
    333           query_dictionary['additional_record_count'] += 1 
    334  
    335           answer_data = { 
    336                 'mname': "testbed-ubuntu.cs.washington.edu", 
    337                 'rname': "sebass63.uw.edu", 
    338                 'serial': _SOA_SERIAL, 
    339                 'retry': 900, 
    340                 'refresh': 10800, 
    341                 'expire': 3600000, 
    342                 'minimum': 1 } 
    343  
    344           query_dictionary['answers'].append({ 
    345                'name': question_name, 
    346                'type': question_type, 
    347                'class': question_class, 
    348                'time_to_live': 0, 
    349                'answer_data': answer_data }) 
    350           query_dictionary['answers'].append({ 
    351                'name': "testbed-ubuntu.cs.washington.edu", 
    352                'type': 'A', 
    353                'class': question_class, 
    354                'time_to_live': 0, 
    355                'answer_data': {'address': _host_ip_address } }) 
    356  
    357       elif question_type in simple_answers: 
    358         pre_advertise_time = getruntime() 
    359         answers = zenodotus_advertise_do_lookup(question_name, question_type) 
    360  
    361         if _verbose: 
    362           advertise_time = getruntime() - pre_advertise_time 
    363           print "::NOTICE:: Advertise lookup took " + str(advertise_time) + " seconds.\n" 
    364  
    365         for answer in answers: 
    366           query_dictionary['answers'].append({ 
    367              'name': question_name, 
    368              'type': question_type, 
    369              'class': question_class, 
    370              'time_to_live': 0, 
    371              'answer_data': { 'address': answer } }) 
    372  
    373         query_dictionary['answer_count'] = len(answers) 
    374  
    375       else: 
    376         raise NotImplementedError("Query type " + str(question_type) + " not implemented!") 
    377  
    378     ############################################ 
    379     #      End TYPE checking/formatting        # 
    380     ############################################ 
    381  
    382     if _verbose: 
    383       print "Processing successful. Returning:" 
    384       print "=================================" 
    385       for key in query_dictionary.keys(): 
    386         print "  " + str(key) + " : " + str(query_dictionary[key]) 
    387  
    388     if _verbose: 
    389       total_time = getruntime() - method_time 
    390       print "::NOTICE:: Method took " + str(total_time) + " seconds.\n" 
    391  
    392     return query_dictionary 
     200        answers.append({'name': question_name, 
     201                    'address': entry, 
     202                    'type': question_type, 
     203                    'time_to_live': 0,      # I still do not have a good idea  
     204                    'class': 'IN' })        # for this. 
     205 
     206    response_dictionary['answers'] = answers 
     207    response_dictionary['answer_count'] = len(answers) 
     208    response_dictionary['additional_record_count'] = 0 
     209 
     210    for record in answers: 
     211      if record["type"] == "NS": 
     212        response_dictionary['additional_record_count'] += 1 
     213 
     214    response_dictionary['authority_record_count'] = len(answers) - response_dictionary['additional_record_count'] 
     215 
     216    return response_dictionary 
     217 
    393218  except NotImplementedError, e: 
    394219    if _verbose: 
     
    549374  print >> debug_stream, "\n==================================================" 
    550375  debug_stream.flush() 
     376 
     377 
     378 
     379 
     380def _evaluate_shorthand_time(expression): 
     381  """ 
     382  <Purpose> 
     383    Helper method to parse time duration shorthand, i.e. 
     384 
     385           f('1h') = 3600 
     386 
     387    This will accept the following formats: 
     388 
     389       xm, xh, xd, xw 
     390 
     391    Minutes, hours, days, weeks respectively. 
     392 
     393  <Arguments> 
     394    expression 
     395      String expression to be evaluated 
     396 
     397  <Exceptions> 
     398     
     399 
     400  <Side Effects> 
     401    ValueErrors possible in the case of malformed input. 
     402 
     403  <Returns> 
     404    An integer number of seconds. 
     405  """ 
     406  # Validate input 
     407  if (type(expression) != type("")): 
     408    raise ValueError("Argument <expression> must be a string.") 
     409  if (len(expression) == 0): 
     410    raise ValueError("Time expression must not be empty.") 
     411  if (expression[:-1].isalpha()): 
     412    raise ValueError("Time expression " + str(expression) + "improperly formatted.") 
    551413   
     414  conversiontable = {'m' : 60, 'h' : 3600, 'd' : 86400, 'w' : 604800 } 
     415 
     416  return conversiontable[expression[-1:]] * int(expression[:-1]) 
     417 
     418 
     419 
     420 
     421def _parse_zonefile(zonefile, maxcache=100): 
     422  """ 
     423  <Purpose> 
     424    Parses the given zone file and returns records which should be  
     425    initially cached. There's nothing clever here, it just uses the  
     426    first records it reads and caches those. 
     427 
     428  <Arguments> 
     429    zonefile 
     430      The handle of the file to be parsed. 
     431    maxcache 
     432      The maximum number of records to be cached after reading. 
     433 
     434  <Exceptions> 
     435    TODO: Populate 
     436 
     437  <Side Effects> 
     438    None 
     439 
     440  <Returns> 
     441    A list of records in dictionary form. These have the same format  
     442    as the dictionaries in the answers[] list in the packet dictionary  
     443    specification. 
     444  """ 
     445  # Break the file in to individual lines. 
     446  lines = zonefile.readlines() 
     447 
     448  # If a semicolon is present, remove it and everything following. 
     449  # While we're at it, remove all leading and trailing whitespace. 
     450  for i in range(len(lines)): 
     451    if ';' in lines[i]: 
     452      lines[i] = lines[i][0:lines[i].index(';')] # Repy should have regex! 
     453    lines[i] = lines[i].strip() 
     454 
     455  # Validate formatting of the first line. 
     456  args = _splitstrip(lines[0]) 
     457  if len(args) != 2: 
     458    raise ValueError("Malformed zone file (line 1)") 
     459  if args[0] != "$ORIGIN": 
     460    raise ValueError("Malformed zone file (line 1)") 
     461 
     462  # Accept naively that the $origin arument is correctly formatted,  
     463  # because we never seem to use it. 
     464  origin = args[1] 
     465  del lines[0] 
     466 
     467  # Validate formatting of the second line. 
     468  args = _splitstrip(lines[0]) 
     469  if len(args) != 2: 
     470    raise ValueError("Malformed zone file (line 2)") 
     471  if args[0] != "$TTL": 
     472    raise ValueError("Malformed zone file (line 2)") 
     473   
     474  ttl = _evaluate_shorthand_time(args[1]) 
     475  del lines[0] 
     476 
     477  records = [] 
     478 
     479  # Iterate through the remainder of the file, populating the records  
     480  # list as we go. 
     481  i = 0 
     482  while i < len(lines): 
     483    line = lines[i] 
     484 
     485    # Create default dictionary 
     486    record = {} 
     487    record["class"] = "IN" 
     488    # Later, line-item assignment of ttl should be possible. For now, default. 
     489    record["time_to_live"] = ttl 
     490 
     491    # Add specified information. 
     492    args = _splitstrip(line) 
     493    if '(' in args: 
     494      while not ')' in args: 
     495        i += 1 
     496        if not i in range(len(lines)): 
     497          raise Exception("Unexpected EOF when parsing zone file.") 
     498        line = lines[i] 
     499        args += _splitstrip(lines[i]) 
     500      args.remove('(') 
     501      args.remove(')') 
     502 
     503    # TODO: Format checking for these fields. 
     504    record["name"] = args[0] 
     505    record["type"] = args[1] 
     506     
     507    if record["type"] == "SOA": 
     508      record["mname"] = args[2] 
     509      record["rname"] = args[3] 
     510      record["serial"] = int(args[4]) 
     511      record["refresh"] = _evaluate_shorthand_time(args[5]) 
     512      record["retry"] = _evaluate_shorthand_time(args[6]) 
     513      record["expire"] = _evaluate_shorthand_time(args[7]) 
     514      record["minimum"] = _evaluate_shorthand_time(args[8]) 
     515    elif record["type"] == "A": 
     516      if args[2] == "getmyip": 
     517        record["address"] = getmyip() 
     518      else: 
     519        record["address"] = args[2] 
     520    elif record["type"] == "NS": 
     521      record["address"] = args[2] 
     522    #TODO: Add other queries 
     523 
     524    records.append(record) 
     525 
     526    i += 1 
     527 
     528  return records 
     529 
     530 
     531 
     532# Helper function for parsing 
     533def _splitstrip(expression): 
     534  args = expression.split(' ') 
     535  for arg in args: 
     536    arg = arg.strip() 
     537  while '' in args: 
     538    args.remove('') 
     539 
     540  return args 
     541 
    552542 
    553543 
     
    674664    # Update NTP Time on the server, so that we can use time_gettime() for  
    675665    # timestamping purposes. This should incorporate a way to run zenodotus  
    676     # without timestamping, but that's not a very high priority right now. 
     666    # without timestamping eventually. 
    677667    if ntp_port_set: 
    678668      try: 
     
    686676        exitall() 
    687677 
     678    print "Attempting to load hosts.txt data . . . " 
     679    file_hwnd = open("hosts.txt", "r") 
     680    record_cache = _parse_zonefile(file_hwnd) 
     681    print "Loaded successfully. Cached " + str(len(record_cache)) + " permanent records." 
     682 
    688683    dnsserver_registercallback(listen_port, listen_ip, _do_dns_callback) 
    689684    print "DNS callback registered." 
  • seattle/trunk/zenodotus/zenodotus_advertise.repy

    r5511 r5520  
    3838 
    3939 
    40 include centralizedadvertise.repy 
     40include centralizedadvertise_v2.repy 
    4141 
    4242 
     
    105105        if _verbose: 
    106106          print "Begin lookup attempt on central service." 
    107         simple_results = centralizedadvertise_lookup(key_string, maxvals) 
     107        simple_results = v2centralizedadvertise_lookup(key_string, maxvals) 
    108108        if _verbose: 
    109109          print "Lookup success. Data received:" 
     
    118118        if _verbose: 
    119119          print "Begin lookup attempt on central service." 
    120         results = centralizedadvertise_lookup(adjusted_name, maxvals) 
     120        results = v2centralizedadvertise_lookup(adjusted_name, maxvals) 
    121121        if _verbose: 
    122122          print "Lookup success. Data received:"