Skip to content
This repository
Browse code

Initial commit

  • Loading branch information...
commit e4fad708274a8ce67ca3876cfeea0c185a7985ba 0 parents
authored June 24, 2012
8  DNSLG/Answer.py
... ...
@@ -0,0 +1,8 @@
  1
+import dns.resolver
  2
+
  3
+class ExtendedAnswer(dns.resolver.Answer):
  4
+    def __init__(self, initial_answer):
  5
+        self.qname = initial_answer.qname
  6
+        self.rrsets = [initial_answer.rrset,]
  7
+        self.owner_name = initial_answer.rrset.name
  8
+
973  DNSLG/Formatter.py
... ...
@@ -0,0 +1,973 @@
  1
+#!/usr/bin/env python
  2
+
  3
+import dns
  4
+import dns.version as dnspythonversion
  5
+import base64
  6
+import platform
  7
+import pkg_resources 
  8
+import time
  9
+
  10
+# TODO: Accept explicit requests for CNAME and DNAME?
  11
+# TODO: DANE/TLSA record type. Not yet in DNS Python so not easy...
  12
+
  13
+import Answer
  14
+
  15
+def to_hexstring(str):
  16
+    result = ""
  17
+    for char in str:
  18
+        result += ("%x" % ord(char))
  19
+    return result.upper()
  20
+
  21
+class Formatter():
  22
+    """ This ia the base class for the various Formatters. A formatter
  23
+    takes a "DNS answer" object and format it for a given output
  24
+    format (JSON, XML, etc). Implementing a new format means deriving
  25
+    this class and providing the required methods."""
  26
+    def __init__(self, domain):
  27
+        try:
  28
+            self.myversion = pkg_resources.require("DNS-LG")[0].version
  29
+        except pkg_resources.DistributionNotFound:
  30
+            self.myversion = "VERSION UNKNOWN"
  31
+        self.domain = domain
  32
+    
  33
+    def format(self, answer, qtype, flags, querier):
  34
+        """ Parameter "answer" must be of type
  35
+        Answer.ExtendedAnswer. "qtype" is a string, flags an integer
  36
+        and querier a DNSLG.Querier. This method changes the internal
  37
+        state of the Formatter, it returns nothing."""
  38
+        pass
  39
+
  40
+    def result(self, querier):
  41
+        """ Returns the state of the Formatter, to be sent to the client."""
  42
+        return "NOT IMPLEMENTED IN THE BASE CLASS"
  43
+
  44
+# TEXT
  45
+class TextFormatter(Formatter):
  46
+
  47
+    def format(self, answer, qtype, flags, querier):
  48
+        # TODO: it would be nice one day to have a short format to
  49
+        # have only data, not headers. Or may be several short
  50
+        # formats, suitable for typical Unix text parsing tools. In
  51
+        # the mean time, use "zone" for that.
  52
+        self.output = ""
  53
+        self.output += "Query for: %s, type %s\n" % (self.domain.encode(querier.encoding),
  54
+                                                   qtype)
  55
+        if answer is not None and (str(answer.owner_name) != self.domain):
  56
+            self.output += "Result name: %s\n" % \
  57
+                           str(answer.owner_name).encode(querier.encoding)
  58
+        str_flags = ""
  59
+        if flags & dns.flags.AD:
  60
+            str_flags += "/ Authentic Data "
  61
+        if flags & dns.flags.AA:
  62
+            str_flags += "/ Authoritative Answer "
  63
+        if flags & dns.flags.TC:
  64
+            str_flags += "/ Truncated Answer "
  65
+        self.output += "Flags: %s\n" % str_flags
  66
+        if answer is None:
  67
+            self.output += "[No data for this query type]\n"
  68
+        else:
  69
+            for rrset in answer.rrsets:
  70
+                for rdata in rrset:
  71
+                    if rdata.rdtype == dns.rdatatype.A or rdata.rdtype == dns.rdatatype.AAAA:
  72
+                        self.output += "IP address: %s\n" % rdata.address
  73
+                    elif rdata.rdtype == dns.rdatatype.MX:
  74
+                        self.output += "Mail exchanger: %s (preference %i)\n" % \
  75
+                                       (rdata.exchange, rdata.preference)
  76
+                    elif rdata.rdtype == dns.rdatatype.TXT:
  77
+                        self.output += "Text: %s\n" % " ".join(rdata.strings)
  78
+                    elif rdata.rdtype == dns.rdatatype.SPF:
  79
+                        self.output += "SPF policy: %s\n" % " ".join(rdata.strings)
  80
+                    elif rdata.rdtype == dns.rdatatype.SOA:
  81
+                        self.output += "Start of zone authority: serial number %i, zone administrator %s, master nameserver %s\n" % \
  82
+                                       (rdata.serial, rdata.rname, rdata.mname)
  83
+                    elif rdata.rdtype == dns.rdatatype.NS:
  84
+                        self.output += "Name server: %s\n" % rdata.target
  85
+                    elif rdata.rdtype == dns.rdatatype.DS:
  86
+                        self.output += "Delegation of signature: key %i, hash type %i\n" % \
  87
+                                       (rdata.key_tag, rdata.digest_type)
  88
+                        # TODO: display the digest with to_hexstring
  89
+                    elif rdata.rdtype == dns.rdatatype.DLV:
  90
+                        self.output += "Delegation of signature: key %i, hash type %i\n" % \
  91
+                                       (rdata.key_tag, rdata.digest_type)
  92
+                    elif rdata.rdtype == dns.rdatatype.RRSIG:
  93
+                        pass # Should we show signatures?
  94
+                    elif rdata.rdtype == dns.rdatatype.LOC:
  95
+                        self.output += "Location: longitude %i degrees %i' %i\" latitude %i degrees %i' %i\" altitude %f\n" % \
  96
+                                       (rdata.longitude[0], rdata.longitude[1], rdata.longitude[2],
  97
+                                        rdata.latitude[0], rdata.latitude[1], rdata.latitude[2],
  98
+                                        rdata.altitude)
  99
+                    elif rdata.rdtype == dns.rdatatype.SRV:
  100
+                        self.output += "Service location: server %s, port %i, priority %i, weight %i\n" % \
  101
+                                       (rdata.target, rdata.port, rdata.priority, rdata.weight)
  102
+                    elif rdata.rdtype == dns.rdatatype.PTR:
  103
+                        self.output += "Target: %s\n" % rdata.target
  104
+                    elif rdata.rdtype == dns.rdatatype.DNSKEY:
  105
+                        self.output += "DNSSEC key: "
  106
+                        try:
  107
+                            key_tag = dns.dnssec.key_id(rdata)
  108
+                            self.output += "tag %i " % key_tag
  109
+                        except AttributeError:
  110
+                            # key_id appeared only in dnspython 1.9. Not
  111
+                            # always available on 2012-05-17
  112
+                            pass
  113
+                        self.output += "algorithm %i, flags %i\n" % (rdata.algorithm, rdata.flags)
  114
+                    elif rdata.rdtype == dns.rdatatype.SSHFP:
  115
+                        self.output += "SSH fingerprint: algorithm %i, digest type %i, fingerprint %s\n" % \
  116
+                                       (rdata.algorithm, rdata.fp_type, to_hexstring(rdata.fingerprint))
  117
+                    elif rdata.rdtype == dns.rdatatype.NAPTR:
  118
+                        self.output += ("Naming Authority Pointer: flags \"%s\", order %i, " + \
  119
+                                       "preference %i, rexegp \"%s\" -> replacement \"%s\", " + \
  120
+                                       "services \"%s\"\n") % \
  121
+                                       (rdata.flags, rdata.order, rdata.preference,
  122
+                                       rdata.regexp, str(rdata.replacement), rdata.service)
  123
+                    else:
  124
+                        self.output += "Unknown record type %i: (DATA)\n" % rdata.rdtype
  125
+                self.output += "TTL: %i\n" % rrset.ttl
  126
+        self.output += "Resolver queried: %s\n" % querier.resolver.nameservers[0]
  127
+        self.output += "Query done at: %s\n" % time.strftime("%Y-%m-%d %H:%M:%SZ",
  128
+                                                             time.gmtime(time.time()))
  129
+        self.output += "Query duration: %s\n" % querier.delay
  130
+        if querier.description:
  131
+            self.output += "Service description: %s\n" % querier.description
  132
+        self.output += "DNS Looking Glass %s, DNSpython version %s, Python version %s %s on %s\n" % \
  133
+                       (self.myversion,
  134
+                        dnspythonversion.version, platform.python_implementation(),
  135
+                        platform.python_version(), platform.system())
  136
+        
  137
+    def result(self, querier):
  138
+        return self.output
  139
+
  140
+
  141
+# ZONE FILE
  142
+class ZoneFormatter(Formatter):
  143
+
  144
+    def format(self, answer, qtype, flags, querier):
  145
+        self.output = ""
  146
+        self.output += "; Question: %s, type %s\n" % (self.domain.encode(querier.encoding),
  147
+                                                      qtype)
  148
+        str_flags = ""
  149
+        if flags & dns.flags.AD:
  150
+            str_flags += " ad "
  151
+        if flags & dns.flags.AA:
  152
+            str_flags += " aa  "
  153
+        if flags & dns.flags.TC:
  154
+            str_flags += " tc "
  155
+        if str_flags != "":
  156
+            self.output += "; Flags:" + str_flags + "\n"
  157
+        self.output += "\n"
  158
+        if answer is None:
  159
+            self.output += "; No data for this type\n"
  160
+        else:
  161
+            for rrset in answer.rrsets:
  162
+                for rdata in rrset:
  163
+                    # TODO: do not hardwire the class
  164
+                    self.output += "%s\tIN\t" % answer.owner_name # TODO: do not repeat the name if there is a RRset
  165
+                    # TODO: it could use some refactoring: most (but _not all_) of types
  166
+                    # use the same code.
  167
+                    if rdata.rdtype == dns.rdatatype.A:
  168
+                        self.output += "A\t%s\n" % rdata.to_text()
  169
+                    elif rdata.rdtype == dns.rdatatype.AAAA:
  170
+                        self.output += "AAAA\t%s\n" % rdata.to_text()
  171
+                    elif rdata.rdtype == dns.rdatatype.MX:
  172
+                        self.output += "MX\t%s\n" % rdata.to_text()
  173
+                    elif rdata.rdtype == dns.rdatatype.SPF:
  174
+                        self.output += "SPF\t%s\n" % rdata.to_text()
  175
+                    elif rdata.rdtype == dns.rdatatype.TXT:
  176
+                        self.output += "TXT\t%s\n" % rdata.to_text()
  177
+                    elif rdata.rdtype == dns.rdatatype.SOA:
  178
+                        self.output += "SOA\t%s\n" % rdata.to_text()
  179
+                    elif rdata.rdtype == dns.rdatatype.NS:
  180
+                        self.output += "NS\t%s\n" % rdata.to_text()
  181
+                    elif rdata.rdtype == dns.rdatatype.PTR:
  182
+                        self.output += "PTR\t%s\n" % rdata.to_text()
  183
+                    elif rdata.rdtype == dns.rdatatype.LOC:
  184
+                        self.output += "LOC\t%s\n" % rdata.to_text()
  185
+                    elif rdata.rdtype == dns.rdatatype.DNSKEY:
  186
+                        self.output += "DNSKEY\t%s" % rdata.to_text()
  187
+                        try:
  188
+                            key_tag = dns.dnssec.key_id(rdata)
  189
+                            self.output += "; key ID = %i\n" % key_tag
  190
+                        except AttributeError:
  191
+                            # key_id appeared only in dnspython 1.9. Not
  192
+                            # always available on 2012-05-17
  193
+                            self.output += "\n"
  194
+                    elif rdata.rdtype == dns.rdatatype.DS:
  195
+                        self.output += "DS\t%s\n" % rdata.to_text()
  196
+                    elif rdata.rdtype == dns.rdatatype.DLV:
  197
+                        self.output += "DLV\t%s\n" % rdata.to_text()
  198
+                    elif rdata.rdtype == dns.rdatatype.SSHFP:
  199
+                        self.output += "SSHFP\t%s\n" % rdata.to_text()
  200
+                    elif rdata.rdtype == dns.rdatatype.NAPTR:
  201
+                        self.output += "NAPTR\t%s\n" % rdata.to_text()
  202
+                    elif rdata.rdtype == dns.rdatatype.RRSIG:
  203
+                        pass # Should we show signatures?
  204
+                    elif rdata.rdtype == dns.rdatatype.SRV:
  205
+                        self.output += "SRV\t%s\n" % rdata.to_text()
  206
+                    else:
  207
+                        # dnspython dumps the types it knows. TODO: uses that?
  208
+                        self.output += "TYPE%i ; DATA %s\n" % (rdata.rdtype, rdata.to_text())
  209
+                self.output += "; TTL: %i\n" % rrset.ttl
  210
+        self.output += "\n; Server: %s\n" % querier.resolver.nameservers[0]
  211
+        self.output += "; When: %s\n" % time.strftime("%Y-%m-%d %H:%M:%SZ",
  212
+                                                             time.gmtime(time.time()))
  213
+        self.output += "; Query duration: %s\n" % querier.delay
  214
+        if querier.description:
  215
+            self.output += "; Service description: %s\n" % querier.description
  216
+        self.output += "; DNS Looking Glass %s, DNSpython version %s, Python version %s %s on %s\n" % \
  217
+                       (self.myversion, dnspythonversion.version,
  218
+                        platform.python_implementation(),
  219
+                        platform.python_version(), platform.system())
  220
+                
  221
+    def result(self, querier):
  222
+        return self.output
  223
+
  224
+
  225
+# JSON
  226
+import json
  227
+# http://docs.python.org/library/json.html
  228
+class JsonFormatter(Formatter):
  229
+
  230
+    def format(self, answer, qtype, flags, querier):
  231
+        self.object = {}
  232
+        self.object['ReturnCode'] = "NOERROR"
  233
+        self.object['QuestionSection'] = {'Qname': self.domain, 'Qtype': qtype}
  234
+        if flags & dns.flags.AD:
  235
+            self.object['AD'] = True
  236
+        if flags & dns.flags.AA:
  237
+            self.object['AA'] = True
  238
+        if flags & dns.flags.TC:
  239
+            self.object['TC'] = True
  240
+        self.object['AnswerSection'] = []
  241
+        if answer is not None:
  242
+            for rrset in answer.rrsets:
  243
+                for rdata in rrset: # TODO: sort them? For instance by preference for MX?
  244
+                    if rdata.rdtype == dns.rdatatype.A:
  245
+                        self.object['AnswerSection'].append({'Type': 'A', 'Address': rdata.address})
  246
+                    elif  rdata.rdtype == dns.rdatatype.AAAA:
  247
+                        self.object['AnswerSection'].append({'Type': 'AAAA', 'Address': rdata.address})
  248
+                    elif rdata.rdtype == dns.rdatatype.LOC:
  249
+                        self.object['AnswerSection'].append({'Type': 'LOC',
  250
+                                                             'Longitude': '%f' % rdata.float_longitude,
  251
+                                                             'Latitude': '%f' % rdata.float_latitude,
  252
+                                                             'Altitude': '%f' % rdata.altitude})
  253
+                    elif rdata.rdtype == dns.rdatatype.PTR:
  254
+                        self.object['AnswerSection'].append({'Type': 'PTR',
  255
+                                                             'Target': str(rdata.target)})
  256
+                    elif rdata.rdtype == dns.rdatatype.MX:
  257
+                        self.object['AnswerSection'].append({'Type': 'MX', 
  258
+                                                             'MailExchanger': str(rdata.exchange),
  259
+                                                             'Preference': rdata.preference})
  260
+                    elif rdata.rdtype == dns.rdatatype.TXT:
  261
+                        self.object['AnswerSection'].append({'Type': 'TXT', 'Text': " ".join(rdata.strings)})
  262
+                    elif rdata.rdtype == dns.rdatatype.SPF:
  263
+                        self.object['AnswerSection'].append({'Type': 'SPF', 'Text': " ".join(rdata.strings)})
  264
+                    elif rdata.rdtype == dns.rdatatype.SOA:
  265
+                        self.object['AnswerSection'].append({'Type': 'SOA', 'Serial': rdata.serial,
  266
+                                                             'MasterServerName': str(rdata.mname),
  267
+                                                             'MaintainerName': str(rdata.rname),
  268
+                                                             'Refresh': rdata.refresh,
  269
+                                                             'Retry': rdata.retry,
  270
+                                                             'Expire': rdata.expire,
  271
+                                                             'Minimum': rdata.minimum,
  272
+                                                             })
  273
+                    elif rdata.rdtype == dns.rdatatype.NS:
  274
+                        self.object['AnswerSection'].append({'Type': 'NS', 'Name': str(rdata.target)})
  275
+                    elif rdata.rdtype == dns.rdatatype.DNSKEY:
  276
+                        returned_object = {'Type': 'DNSKEY',
  277
+                                          'Algorithm': rdata.algorithm,
  278
+                                          'Flags': rdata.flags}
  279
+                        try:
  280
+                            key_tag = dns.dnssec.key_id(rdata)
  281
+                            returned_object['Tag'] = key_tag
  282
+                        except AttributeError:
  283
+                            # key_id appeared only in dnspython 1.9. Not
  284
+                            # always available on 2012-05-17
  285
+                            pass
  286
+                        self.object['AnswerSection'].append(returned_object)
  287
+                    elif rdata.rdtype == dns.rdatatype.DS:
  288
+                        self.object['AnswerSection'].append({'Type': 'DS', 'DelegationKey': rdata.key_tag,
  289
+                                                             'DigestType': rdata.digest_type})
  290
+                    elif rdata.rdtype == dns.rdatatype.DLV:
  291
+                        self.object['AnswerSection'].append({'Type': 'DLV', 'DelegationKey': rdata.key_tag,
  292
+                                                             'DigestType': rdata.digest_type})
  293
+                    elif rdata.rdtype == dns.rdatatype.RRSIG:
  294
+                        pass # Should we show signatures?
  295
+                    elif rdata.rdtype == dns.rdatatype.SSHFP:
  296
+                        self.object['AnswerSection'].append({'Type': 'SSHFP',
  297
+                                                             'Algorithm': rdata.algorithm,
  298
+                                                             'DigestType': rdata.fp_type,
  299
+                                                             'Fingerprint': to_hexstring(rdata.fingerprint)})
  300
+                    elif rdata.rdtype == dns.rdatatype.NAPTR:
  301
+                        self.object['AnswerSection'].append({'Type': 'NAPTR',
  302
+                                                             'Flags': rdata.flags,
  303
+                                                             'Services': rdata.service,
  304
+                                                             'Order': rdata.order,
  305
+                                                             'Preference': rdata.preference,
  306
+                                                             'Regexp': rdata.regexp,
  307
+                                                             'Replacement': str(rdata.replacement)})
  308
+                    elif rdata.rdtype == dns.rdatatype.SRV:
  309
+                        self.object['AnswerSection'].append({'Type': 'SRV', 'Server': str(rdata.target),
  310
+                                                             'Port': rdata.port,
  311
+                                                             'Priority': rdata.priority,
  312
+                                                             'Weight': rdata.weight})
  313
+                    else:
  314
+                        self.object['AnswerSection'].append({'Type': "unknown"}) # TODO: the type number
  315
+                    self.object['AnswerSection'][-1]['TTL'] = rrset.ttl
  316
+                    self.object['AnswerSection'][-1]['Name'] = str(answer.owner_name)
  317
+        try:
  318
+            duration = querier.delay.total_seconds()
  319
+        except AttributeError: # total_seconds appeared only with Python 2.7
  320
+            delay = querier.delay
  321
+            duration = (delay.days*86400) + delay.seconds + \
  322
+                       (float(delay.microseconds)/1000000.0)
  323
+        self.object['Query'] = {'Server': querier.resolver.nameservers[0],
  324
+                                'Duration': duration}
  325
+        if querier.description:
  326
+            self.object['Query']['Description'] = querier.description
  327
+        self.object['Query']['Versions'] = "DNS Looking Glass %s, DNSpython version %s, Python version %s %s on %s\n" % \
  328
+                       (self.myversion, dnspythonversion.version,
  329
+                        platform.python_implementation(),
  330
+                        platform.python_version(), platform.system())
  331
+
  332
+            
  333
+    def result(self, querier):
  334
+        return json.dumps(self.object) + "\n"
  335
+
  336
+
  337
+# XML
  338
+# http://www.owlfish.com/software/simpleTAL/
  339
+from simpletal import simpleTAL, simpleTALES, simpleTALUtils
  340
+xml_template = """
  341
+<result>
  342
+ <query>
  343
+    <question><qname tal:content="qname"/><qtype tal:content="qtype"/></question>
  344
+    <server><resolver tal:content="resolver"/><duration tal:content="duration"/><description tal:condition="description" tal:content="description"/><versions tal:condition="version" tal:content="version"/></server>
  345
+ </query>
  346
+ <response>
  347
+    <!-- TODO: query ID -->
  348
+    <ad tal:condition="ad" tal:content="ad"/><tc tal:condition="tc" tal:content="tc"/><aa tal:condition="aa" tal:content="aa"/>
  349
+    <!-- No <anscount>, it is useless in XML. -->
  350
+    <answers tal:condition="rrsets">
  351
+      <rrset tal:replace="structure rrset" tal:repeat="rrset rrsets"/>
  352
+    </answers>
  353
+ </response>
  354
+</result>
  355
+"""
  356
+set_xml_template = """
  357
+<RRSet tal:condition="records" class="IN" tal:attributes="owner ownername; type type; ttl ttl"><record tal:repeat="record records" tal:replace="structure record"/></RRSet>
  358
+"""
  359
+a_xml_template = """
  360
+<A tal:attributes="address address"/>
  361
+"""
  362
+aaaa_xml_template = """
  363
+<AAAA tal:attributes="ip6address address"/>
  364
+"""
  365
+mx_xml_template = """
  366
+<MX tal:attributes="preference preference; exchange exchange"/>
  367
+"""
  368
+ns_xml_template = """
  369
+<NS tal:attributes="nsdname name"/>
  370
+"""
  371
+srv_xml_template = """
  372
+<SRV tal:attributes="priority priority; weight weight; port port; target name"/>
  373
+"""
  374
+txt_xml_template = """
  375
+<TXT tal:attributes="rdata text"/>
  376
+"""
  377
+spf_xml_template = """
  378
+<SPF tal:attributes="rdata text"/>
  379
+"""
  380
+loc_xml_template = """
  381
+<LOC tal:attributes="longitude longitude; latitude latitude; altitude altitude"/>
  382
+"""
  383
+ptr_xml_template = """
  384
+<PTR tal:attributes="ptrdname name"/>
  385
+"""
  386
+ds_xml_template = """
  387
+<DS tal:attributes="keytag keytag; algorithm algorithm; digesttype digesttype; digest digest"/>
  388
+"""
  389
+dlv_xml_template = """
  390
+<DLV tal:attributes="keytag keytag; algorithm algorithm; digesttype digesttype; digest digest"/>
  391
+"""
  392
+# TODO: keytag is an extension to the Internet-Draft
  393
+dnskey_xml_template = """
  394
+<DNSKEY tal:attributes="flags flags; protocol protocol; algorithm algorithm; publickey key; keytag keytag"/>
  395
+"""
  396
+sshfp_xml_template = """
  397
+<SSHFP tal:attributes="algorithm algorithm; fptype fptype; fingerprint fingerprint"/>
  398
+"""
  399
+naptr_xml_template = """
  400
+<NAPTR tal:attributes="flags flags; order order; preference preference; services services; regexp regexp; replacement replacement"/>
  401
+"""
  402
+soa_xml_template = """
  403
+<SOA tal:attributes="mname mname; rname rname; serial serial; refresh refresh; retry retry; expire expire; minimum minimum"/>
  404
+"""
  405
+# TODO: how to keep the comments of a template in TAL's output?
  406
+unknown_xml_template = """
  407
+<binaryRR tal:attributes="rtype rtype; rdlength rdlength; rdata rdata"/> <!-- Unknown type -->
  408
+"""
  409
+# TODO: Why is there a rdlength when you can deduce it from the rdata?
  410
+# That's strange in a non-binary format like XML.
  411
+class XmlFormatter(Formatter):
  412
+
  413
+    def format(self, answer, qtype, flags, querier):
  414
+        self.xml_template = simpleTAL.compileXMLTemplate (xml_template)
  415
+        self.set_template = simpleTAL.compileXMLTemplate (set_xml_template)
  416
+        self.a_template = simpleTAL.compileXMLTemplate (a_xml_template)
  417
+        self.aaaa_template = simpleTAL.compileXMLTemplate (aaaa_xml_template)
  418
+        self.mx_template = simpleTAL.compileXMLTemplate (mx_xml_template)
  419
+        self.srv_template = simpleTAL.compileXMLTemplate (srv_xml_template)
  420
+        self.txt_template = simpleTAL.compileXMLTemplate (txt_xml_template)
  421
+        self.spf_template = simpleTAL.compileXMLTemplate (spf_xml_template)
  422
+        self.loc_template = simpleTAL.compileXMLTemplate (loc_xml_template)
  423
+        self.ns_template = simpleTAL.compileXMLTemplate (ns_xml_template)
  424
+        self.ptr_template = simpleTAL.compileXMLTemplate (ptr_xml_template)
  425
+        self.soa_template = simpleTAL.compileXMLTemplate (soa_xml_template)
  426
+        self.ds_template = simpleTAL.compileXMLTemplate (ds_xml_template)
  427
+        self.dlv_template = simpleTAL.compileXMLTemplate (dlv_xml_template)
  428
+        self.dnskey_template = simpleTAL.compileXMLTemplate (dnskey_xml_template)
  429
+        self.sshfp_template = simpleTAL.compileXMLTemplate (sshfp_xml_template)
  430
+        self.naptr_template = simpleTAL.compileXMLTemplate (naptr_xml_template)
  431
+        self.unknown_template = simpleTAL.compileXMLTemplate (unknown_xml_template)
  432
+        self.context = simpleTALES.Context(allowPythonPath=False)
  433
+        self.acontext = simpleTALES.Context(allowPythonPath=False)
  434
+        self.rcontext = simpleTALES.Context(allowPythonPath=False)
  435
+        self.context.addGlobal ("qname", self.domain)
  436
+        self.context.addGlobal ("qtype", qtype)
  437
+        self.context.addGlobal ("resolver", querier.resolver.nameservers[0])
  438
+        try:
  439
+            duration = querier.delay.total_seconds()
  440
+        except AttributeError: # total_seconds appeared only with Python 2.7
  441
+            delay = querier.delay
  442
+            duration = (delay.days*86400) + delay.seconds + \
  443
+                       (float(delay.microseconds)/1000000.0)
  444
+        self.context.addGlobal ("duration", duration)
  445
+        self.context.addGlobal ("description", querier.description)
  446
+        self.context.addGlobal ("version",
  447
+                                "DNS Looking Glass %s, DNSpython version %s, Python version %s %s on %s\n" % \
  448
+                                (self.myversion, dnspythonversion.version,
  449
+                                 platform.python_implementation(),
  450
+                                 platform.python_version(), platform.system()))
  451
+        addresses = []
  452
+        if answer is not None:
  453
+            self.rrsets = []
  454
+            self.acontext.addGlobal ("ownername", answer.owner_name)
  455
+            if flags & dns.flags.AD:
  456
+                ad = 1
  457
+            else:
  458
+                ad = 0
  459
+            self.context.addGlobal ("ad", ad)
  460
+            if flags & dns.flags.TC:
  461
+                tc = 1
  462
+            else:
  463
+                tc = 0
  464
+            self.context.addGlobal ("tc", tc)
  465
+            if flags & dns.flags.AA:
  466
+                aa = 1
  467
+            else:
  468
+                aa = 0
  469
+            self.context.addGlobal ("aa", aa)
  470
+            # TODO: class
  471
+            for rrset in answer.rrsets:
  472
+                records = []
  473
+                self.acontext.addGlobal ("ttl", rrset.ttl)
  474
+                self.acontext.addGlobal ("type", dns.rdatatype.to_text(rrset.rdtype))
  475
+                for rdata in rrset:
  476
+                    icontext = simpleTALES.Context(allowPythonPath=False)
  477
+                    iresult = simpleTALUtils.FastStringOutput()
  478
+                    if rdata.rdtype == dns.rdatatype.A or rdata.rdtype == dns.rdatatype.AAAA:
  479
+                        icontext.addGlobal ("address", rdata.address)
  480
+                        if rdata.rdtype == dns.rdatatype.A:
  481
+                            self.a_template.expand (icontext, iresult,
  482
+                                                           suppressXMLDeclaration=True, 
  483
+                                                      outputEncoding=querier.encoding)
  484
+                        else:
  485
+                            self.aaaa_template.expand (icontext, iresult,
  486
+                                                           suppressXMLDeclaration=True, 
  487
+                                                      outputEncoding=querier.encoding)
  488
+                    elif rdata.rdtype == dns.rdatatype.SRV:
  489
+                        icontext.addGlobal ("priority", rdata.priority)
  490
+                        icontext.addGlobal ("weight", rdata.weight)
  491
+                        icontext.addGlobal ("port", rdata.port)
  492
+                        icontext.addGlobal ("name", rdata.target)
  493
+                        self.srv_template.expand (icontext, iresult,
  494
+                                                           suppressXMLDeclaration=True, 
  495
+                                                      outputEncoding=querier.encoding)
  496
+                    elif rdata.rdtype == dns.rdatatype.MX:
  497
+                        icontext.addGlobal ("preference", rdata.preference)
  498
+                        icontext.addGlobal ("exchange", rdata.exchange)
  499
+                        self.mx_template.expand (icontext, iresult,
  500
+                                                           suppressXMLDeclaration=True, 
  501
+                                                      outputEncoding=querier.encoding)
  502
+                    elif rdata.rdtype == dns.rdatatype.DS:
  503
+                        icontext.addGlobal ("keytag", rdata.key_tag)
  504
+                        icontext.addGlobal ("digesttype", rdata.digest_type)
  505
+                        icontext.addGlobal ("algorithm", rdata.algorithm)
  506
+                        icontext.addGlobal ("digest", "TODO") # rdata.digest is binary, encode it first with to_hexstring()
  507
+                        self.ds_template.expand (icontext, iresult,
  508
+                                                           suppressXMLDeclaration=True, 
  509
+                                                      outputEncoding=querier.encoding)
  510
+                    elif rdata.rdtype == dns.rdatatype.DLV:
  511
+                        icontext.addGlobal ("keytag", rdata.key_tag)
  512
+                        icontext.addGlobal ("digesttype", rdata.digest_type)
  513
+                        icontext.addGlobal ("algorithm", rdata.algorithm)
  514
+                        icontext.addGlobal ("digest", "TODO") # rdata.digest is binary, encode it first with to_hexstring()
  515
+                        self.dlv_template.expand (icontext, iresult,
  516
+                                                           suppressXMLDeclaration=True, 
  517
+                                                      outputEncoding=querier.encoding)
  518
+                    elif rdata.rdtype == dns.rdatatype.DNSKEY:
  519
+                        try:
  520
+                            key_tag = dns.dnssec.key_id(rdata)
  521
+                            icontext.addGlobal ("keytag", key_tag)
  522
+                        except AttributeError:
  523
+                            # key_id appeared only in dnspython 1.9. Not
  524
+                            # always available on 2012-05-17
  525
+                            pass                  
  526
+                        icontext.addGlobal ("protocol", rdata.protocol)
  527
+                        icontext.addGlobal ("flags", rdata.flags)
  528
+                        icontext.addGlobal ("algorithm", rdata.algorithm)
  529
+                        icontext.addGlobal ("key", "TODO") # rdata.key is binary, encode it first with to_hexstring()
  530
+                        self.dnskey_template.expand (icontext, iresult,
  531
+                                                           suppressXMLDeclaration=True, 
  532
+                                                      outputEncoding=querier.encoding)
  533
+                    elif rdata.rdtype == dns.rdatatype.SSHFP:
  534
+                        icontext.addGlobal ("algorithm", rdata.algorithm)
  535
+                        icontext.addGlobal ("fptype", rdata.fp_type)
  536
+                        icontext.addGlobal ("fingerprint", to_hexstring(rdata.fingerprint))
  537
+                        self.sshfp_template.expand (icontext, iresult,
  538
+                                                           suppressXMLDeclaration=True, 
  539
+                                                      outputEncoding=querier.encoding)
  540
+                    elif rdata.rdtype == dns.rdatatype.NAPTR:
  541
+                        icontext.addGlobal ("flags", rdata.flags)
  542
+                        icontext.addGlobal ("services", rdata.service)
  543
+                        icontext.addGlobal ("order", rdata.order)
  544
+                        icontext.addGlobal ("preference", rdata.preference)
  545
+                        regexp = unicode(rdata.regexp, "UTF-8")
  546
+                        icontext.addGlobal ("regexp",
  547
+                                            regexp)
  548
+                        # Yes, there is Unicode in NAPTRs, see
  549
+                        # mailclub.tel for instance. We assume it will
  550
+                        # always be UTF-8
  551
+                        icontext.addGlobal ("replacement", rdata.replacement)
  552
+                        self.naptr_template.expand (icontext, iresult,
  553
+                                                    suppressXMLDeclaration=True, 
  554
+                                                    outputEncoding=querier.encoding)
  555
+                    elif rdata.rdtype == dns.rdatatype.TXT:
  556
+                        icontext.addGlobal ("text", " ".join(rdata.strings))
  557
+                        self.txt_template.expand (icontext, iresult,
  558
+                                                           suppressXMLDeclaration=True, 
  559
+                                                      outputEncoding=querier.encoding)
  560
+                    elif rdata.rdtype == dns.rdatatype.SPF:
  561
+                        icontext.addGlobal ("text", " ".join(rdata.strings))
  562
+                        self.spf_template.expand (icontext, iresult,
  563
+                                                           suppressXMLDeclaration=True, 
  564
+                                                      outputEncoding=querier.encoding)
  565
+                    elif rdata.rdtype == dns.rdatatype.PTR:
  566
+                        icontext.addGlobal ("name", rdata.target)
  567
+                        self.ptr_template.expand (icontext, iresult,
  568
+                                                           suppressXMLDeclaration=True, 
  569
+                                                      outputEncoding=querier.encoding)
  570
+                    elif rdata.rdtype == dns.rdatatype.LOC:
  571
+                        icontext.addGlobal ("longitude", rdata.float_longitude)
  572
+                        icontext.addGlobal ("latitude", rdata.float_latitude)
  573
+                        icontext.addGlobal ("altitude", rdata.altitude)
  574
+                        self.loc_template.expand (icontext, iresult,
  575
+                                                           suppressXMLDeclaration=True, 
  576
+                                                      outputEncoding=querier.encoding)
  577
+                    elif rdata.rdtype == dns.rdatatype.NS:
  578
+                        icontext.addGlobal ("name", rdata.target)
  579
+                        # TODO: translate Punycode domain names back to Unicode?
  580
+                        self.ns_template.expand (icontext, iresult,
  581
+                                                           suppressXMLDeclaration=True)
  582
+                    elif rdata.rdtype == dns.rdatatype.SOA:
  583
+                        icontext.addGlobal ("rname", rdata.rname)
  584
+                        icontext.addGlobal ("mname", rdata.mname)
  585
+                        icontext.addGlobal ("serial", rdata.serial)
  586
+                        icontext.addGlobal ("refresh", rdata.refresh)
  587
+                        icontext.addGlobal ("retry", rdata.retry)
  588
+                        icontext.addGlobal ("expire", rdata.expire)
  589
+                        icontext.addGlobal ("minimum", rdata.minimum)
  590
+                        self.soa_template.expand (icontext, iresult,
  591
+                                                           suppressXMLDeclaration=True, 
  592
+                                                      outputEncoding=querier.encoding)                    
  593
+                    else:
  594
+                        icontext.addGlobal ("rtype", rdata.rdtype)
  595
+                        icontext.addGlobal ("rdlength", 0)  # TODO: useless, anyway (and
  596
+                        # no easy way to compute it in dnspython)
  597
+                        # TODO: rdata
  598
+                        self.unknown_template.expand (icontext, iresult,
  599
+                                                           suppressXMLDeclaration=True, 
  600
+                                                      outputEncoding=querier.encoding)   
  601
+                    records.append(unicode(iresult.getvalue(), querier.encoding))
  602
+                else:
  603
+                    pass # TODO what to send back when no data for this QTYPE?
  604
+                if records:
  605
+                    self.acontext.addGlobal ("records", records)
  606
+                    self.acontext.addGlobal ("ttl", rrset.ttl)
  607
+                    iresult = simpleTALUtils.FastStringOutput()
  608
+                    self.set_template.expand (self.acontext, iresult,
  609
+                                              suppressXMLDeclaration=True, 
  610
+                                              outputEncoding=querier.encoding)
  611
+                    self.rrsets.append(unicode(iresult.getvalue(), querier.encoding))
  612
+        else:
  613
+            self.rrsets = None
  614
+
  615
+    def result(self, querier):
  616
+        result = simpleTALUtils.FastStringOutput()
  617
+        self.context.addGlobal("rrsets", self.rrsets)
  618
+        self.xml_template.expand (self.context, result, 
  619
+                                                      outputEncoding=querier.encoding)
  620
+        return result.getvalue()
  621
+
  622
+
  623
+# HTML
  624
+html_template = """<?xml version="1.0" encoding="utf-8"?>
  625
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
  626
+<html xmlns="http://www.w3.org/1999/xhtml">
  627
+ <head>
  628
+    <title tal:content="title"/>
  629
+    <link tal:condition="css" rel="stylesheet" type="text/css" tal:attributes="href css"/>
  630
+    <link rel="author" href="http://www.bortzmeyer.org/static/moi.html"/>
  631
+    <link tal:condition="opensearch" rel="search"
  632
+           type="application/opensearchdescription+xml" 
  633
+           tal:attributes="href opensearch"
  634
+           title="DNS Looking Glass" />
  635
+    <meta http-equiv="Content-Type" tal:attributes="content contenttype"/>
  636
+    <meta name="robots" content="noindex,nofollow"/>
  637
+ </head>
  638
+ <body>
  639
+    <h1 tal:content="title"/>
  640
+    <div class="body">
  641
+    <p tal:condition="distinctowner">Response is name <span class="hostname" tal:content="ownername"/>.</p><p tal:condition="flags">Response flags are: <span tal:replace="flags"/>.</p>
  642
+    <div class="rrsets" tal:repeat="rrset rrsets">
  643
+     <p><span tal:condition="rrset/ttl">Time-to-Live of this answer is <span tal:replace="rrset/ttl"/>.</span></p>
  644
+    <ul tal:condition="rrset/records">
  645
+      <li tal:repeat="record rrset/records" tal:content="structure record"/>
  646
+    </ul>
  647
+    </div>
  648
+    <p tal:condition="not: rrsets">No data was found.</p>
  649
+    <p>Result obtained from resolver(s) <span class="hostname" tal:content="resolver"/> at <span tal:replace="datetime"/>. Query took <span tal:replace="duration"/>.</p>
  650
+    </div>
  651
+    <hr class="endsep"/>
  652
+    <p><span tal:condition="email">Service managed by <span class="email" tal:content="email"/>. </span><span tal:condition="doc"> See <a tal:attributes="href doc">details and documentation</a>.</span><span tal:condition="description_html" tal:content="structure description_html"/><span tal:condition="description" tal:content="description"/> / <span tal:condition="versions" tal:content="structure versions"/></p>
  653
+ </body>
  654
+</html>
  655
+"""
  656
+version_html_template = """
  657
+<span>DNS Looking Glass "<span tal:replace="myversion"/>", <a href="http://www.dnspython.org/">DNSpython</a> version <span tal:replace="dnsversion"/>, <a href="http://www.python.org/">Python</a> version <span tal:replace="pyversion"/></span>
  658
+"""
  659
+address_html_template = """
  660
+<a class="address" tal:attributes="href path" tal:content="address"/>
  661
+"""
  662
+mx_html_template = """
  663
+<span><a class="hostname" tal:attributes="href path" tal:content="hostname"/> (preference <span tal:replace="pref"/>)</span>
  664
+"""
  665
+# TODO: better presentation of "admin" (replacement of . by @ and mailto: URL)
  666
+# TODO: better presentation of intervals? (Weeks, days, etc)
  667
+# TODO: indicate the type of the record before the answer? Not obvious.
  668
+soa_html_template = """
  669
+<span>Zone administrator <span tal:replace="admin"/>, master server <a class="hostname" tal:attributes="href path" tal:content="master"/>, serial number <span tal:replace="serial"/>, refresh interval <span tal:replace="refresh"/> s, retry interval <span tal:replace="retry"/> s, expiration delay <span tal:replace="expire"/> s, negative reply TTL <span tal:replace="minimum"/> s</span>
  670
+"""
  671
+ns_html_template = """
  672
+<span><a class="hostname" tal:attributes="href path" tal:content="hostname"/></span>
  673
+"""
  674
+ptr_html_template = """
  675
+<span><a class="hostname" tal:attributes="href path" tal:content="hostname"/></span>
  676
+"""
  677
+srv_html_template = """
  678
+<span>Priority <span tal:content="priority"/>, weight <span tal:content="weight"/>, host <a class="hostname" tal:attributes="href path" tal:content="hostname"/>, port <span tal:content="port"/>,</span>
  679
+"""
  680
+txt_html_template = """
  681
+<span tal:content="text"/>
  682
+"""
  683
+spf_html_template = """
  684
+<span tal:content="text"/>
  685
+"""
  686
+ds_html_template = """
  687
+<span>Key <span tal:replace="keytag"/> (hash type <span tal:replace="digesttype"/>)</span>
  688
+"""
  689
+dlv_html_template = """
  690
+<span>Key <span tal:replace="keytag"/> (hash type <span tal:replace="digesttype"/>)</span>
  691
+"""
  692
+dnskey_html_template = """
  693
+<span><span tal:condition="keytag">Key <span tal:replace="keytag"/>, </span>algorithm <span tal:replace="algorithm"/>, flags <span tal:replace="flags"/></span>
  694
+"""
  695
+sshfp_html_template = """
  696
+<span>Algorithm <span tal:replace="algorithm"/>, Fingerprint type <span tal:replace="fptype"/>, fingerprint <span tal:replace="fingerprint"/></span>
  697
+"""
  698
+naptr_html_template = """
  699
+<span>Flags "<span tal:replace="flags"/>", Service(s) "<span tal:replace="services"/>", order <span tal:replace="order"/> and preference <span tal:replace="preference"/>, regular expression <span class="naptr_regexp" tal:content="regexp"/>, replacement <span class="domainname" tal:content="replacement"/></span>
  700
+"""
  701
+# TODO: link to Open Street Map
  702
+loc_html_template = """
  703
+<span><span tal:replace="longitude"/> / <span tal:replace="latitude"/> (altitude <span tal:replace="altitude"/>)</span>
  704
+"""
  705
+unknown_html_template = """
  706
+<span>Unknown record type (<span tal:replace="rrtype"/>)</span>
  707
+"""
  708
+class HtmlFormatter(Formatter):
  709
+
  710
+    def link_of(self, host, querier, reverse=False):
  711
+        if querier.base_url == "":
  712
+            url = '/'
  713
+        else:
  714
+            url = querier.base_url
  715
+        base = url + str(host)
  716
+        if not reverse:
  717
+            base += '/ADDR'
  718
+        base += '?format=HTML'
  719
+        if reverse:
  720
+            base += '&reverse=1'
  721
+        return base
  722
+
  723
+    def pretty_duration(self, duration):
  724
+        """ duration is in seconds """
  725
+        weeks = duration/(86400*7)
  726
+        days = (duration-(86400*7*weeks))/86400
  727
+        hours = (duration-(86400*7*weeks)-(86400*days))/3600
  728
+        minutes = (duration-(86400*7*weeks)-(86400*days)-(3600*hours))/60
  729
+        seconds = duration-(86400*7*weeks)-(86400*days)-(3600*hours)-(60*minutes)
  730
+        result = ""
  731
+        empty_result = True
  732
+        if weeks != 0:
  733
+            if weeks > 1:
  734
+                plural = "s"
  735
+            else:
  736
+                plural = ""
  737
+            result += "%i week%s" % (weeks, plural)
  738
+            empty_result = False
  739
+        if days != 0:
  740
+            if not empty_result:
  741
+                result += ", "
  742
+            if days > 1:
  743
+                plural = "s"
  744
+            else:
  745
+                plural = ""
  746
+            result += "%i day%s" % (days, plural)
  747
+            empty_result = False
  748
+        if hours != 0:
  749
+            if not empty_result:
  750
+                result += ", "
  751
+            if hours > 1:
  752
+                plural = "s"
  753
+            else:
  754
+                plural = ""
  755
+            result += "%i hour%s" % (hours, plural)
  756
+            empty_result = False
  757
+        if minutes != 0:
  758
+            if not empty_result:
  759
+                result += ", "
  760
+            if minutes > 1:
  761
+                plural = "s"
  762
+            else:
  763
+                plural = ""
  764
+            result += "%i minute%s" % (minutes, plural)
  765
+            empty_result = False
  766
+        if not empty_result:
  767
+            result += ", "
  768
+        if seconds > 1:
  769
+            plural = "s"
  770
+        else:
  771
+            plural = ""
  772
+        result += "%i second%s" % (seconds, plural)
  773
+        return result
  774
+    
  775
+    def format(self, answer, qtype, flags, querier):
  776
+        self.template = simpleTAL.compileXMLTemplate (html_template)
  777
+        self.address_template = simpleTAL.compileXMLTemplate (address_html_template)
  778
+        self.version_template = simpleTAL.compileXMLTemplate (version_html_template)
  779
+        self.mx_template = simpleTAL.compileXMLTemplate (mx_html_template)
  780
+        self.soa_template = simpleTAL.compileXMLTemplate (soa_html_template)
  781
+        self.ns_template = simpleTAL.compileXMLTemplate (ns_html_template)
  782
+        self.ptr_template = simpleTAL.compileXMLTemplate (ptr_html_template)
  783
+        self.srv_template = simpleTAL.compileXMLTemplate (srv_html_template)
  784
+        self.txt_template = simpleTAL.compileXMLTemplate (txt_html_template)
  785
+        self.spf_template = simpleTAL.compileXMLTemplate (spf_html_template)
  786
+        self.loc_template = simpleTAL.compileXMLTemplate (loc_html_template)
  787
+        self.ds_template = simpleTAL.compileXMLTemplate (ds_html_template)
  788
+        self.dlv_template = simpleTAL.compileXMLTemplate (dlv_html_template)
  789
+        self.dnskey_template = simpleTAL.compileXMLTemplate (dnskey_html_template)
  790
+        self.sshfp_template = simpleTAL.compileXMLTemplate (sshfp_html_template)
  791
+        self.naptr_template = simpleTAL.compileXMLTemplate (naptr_html_template)
  792
+        self.unknown_template = simpleTAL.compileXMLTemplate (unknown_html_template)
  793
+        self.context = simpleTALES.Context(allowPythonPath=False)
  794
+        self.context.addGlobal ("title", "Query for domain %s, type %s" % \
  795
+                                    (self.domain, qtype))
  796
+        self.context.addGlobal ("resolver", querier.resolver.nameservers[0])
  797
+        self.context.addGlobal ("email", querier.email_admin)
  798
+        self.context.addGlobal ("doc", querier.url_doc)
  799
+        self.context.addGlobal("contenttype", 
  800
+                               "text/html; charset=%s" % querier.encoding)
  801
+        self.context.addGlobal ("css", querier.url_css)
  802
+        self.context.addGlobal ("opensearch", querier.url_opensearch)
  803
+        self.context.addGlobal ("datetime", time.strftime("%Y-%m-%d %H:%M:%SZ",
  804
+                                                          time.gmtime(time.time())))
  805
+        self.context.addGlobal("duration", str(querier.delay))
  806
+        if querier.description_html:
  807
+            self.context.addGlobal("description_html", querier.description_html)
  808
+        elif querier.description:
  809
+            self.context.addGlobal("description", querier.description)
  810
+        iresult = simpleTALUtils.FastStringOutput()
  811
+        icontext = simpleTALES.Context(allowPythonPath=False)
  812
+        icontext.addGlobal("pyversion", platform.python_implementation() + " " + 
  813
+                           platform.python_version() + " on " + platform.system())
  814
+        icontext.addGlobal("dnsversion", dnspythonversion.version)
  815
+        icontext.addGlobal("myversion", self.myversion)
  816
+        self.version_template.expand (icontext, iresult,
  817
+                                      suppressXMLDeclaration=True, 
  818
+                                      outputEncoding=querier.encoding)
  819
+        self.context.addGlobal("versions", unicode(iresult.getvalue(), querier.encoding))
  820
+        str_flags = ""
  821
+        if flags & dns.flags.AD:
  822
+            str_flags += "/ Authentic Data "
  823
+        if flags & dns.flags.AA:
  824
+            str_flags += "/ Authoritative Answer "
  825
+        if flags & dns.flags.TC:
  826
+            str_flags += "/ Truncated Answer "
  827
+        if str_flags != "":
  828
+            self.context.addGlobal ("flags", str_flags)
  829
+        if answer is not None:
  830
+            self.rrsets = []
  831
+            if str(answer.owner_name).lower() != self.domain.lower():
  832
+                self.context.addGlobal ("distinctowner", True)
  833
+            self.context.addGlobal ("ownername", answer.owner_name)
  834
+            icontext = simpleTALES.Context(allowPythonPath=False)
  835
+            for rrset in answer.rrsets:
  836
+                records = []
  837
+                for rdata in rrset:
  838
+                    iresult = simpleTALUtils.FastStringOutput()
  839
+                    if rdata.rdtype == dns.rdatatype.A or rdata.rdtype == dns.rdatatype.AAAA:
  840
+                        icontext.addGlobal ("address", rdata.address)
  841
+                        icontext.addGlobal ("path", self.link_of(rdata.address,
  842
+                                                                 querier,
  843
+                                                                 reverse=True))
  844
+                        self.address_template.expand (icontext, iresult,
  845
+                                                       suppressXMLDeclaration=True, 
  846
+                                                      outputEncoding=querier.encoding)
  847
+                    elif rdata.rdtype == dns.rdatatype.SOA:
  848
+                        icontext.addGlobal ("master", rdata.mname)
  849
+                        icontext.addGlobal ("path", self.link_of(rdata.mname, querier))
  850
+                        icontext.addGlobal("admin", rdata.rname) # TODO: replace first point by @
  851
+                        icontext.addGlobal("serial", rdata.serial)
  852
+                        icontext.addGlobal("refresh", rdata.refresh)
  853
+                        icontext.addGlobal("retry", rdata.retry)
  854
+                        icontext.addGlobal("expire", rdata.expire)
  855
+                        icontext.addGlobal("minimum", rdata.minimum)
  856
+                        self.soa_template.expand (icontext, iresult,
  857
+                                                       suppressXMLDeclaration=True,
  858
+                                                      outputEncoding=querier.encoding)
  859
+                    elif rdata.rdtype == dns.rdatatype.MX:
  860
+                        icontext.addGlobal ("hostname", rdata.exchange)
  861
+                        icontext.addGlobal ("path", self.link_of(rdata.exchange, querier))
  862
+                        icontext.addGlobal("pref", rdata.preference)
  863
+                        self.mx_template.expand (icontext, iresult,
  864
+                                                       suppressXMLDeclaration=True,
  865
+                                                      outputEncoding=querier.encoding)
  866
+                    elif rdata.rdtype == dns.rdatatype.NS:
  867
+                        icontext.addGlobal ("hostname", rdata.target)
  868
+                        # TODO: translate back the Punycode name
  869
+                        # servers to Unicode with
  870
+                        # encodings.idna.ToUnicode?
  871
+                        icontext.addGlobal ("path", self.link_of(rdata.target, querier))
  872
+                        self.ns_template.expand (icontext, iresult,
  873
+                                                       suppressXMLDeclaration=True,
  874
+                                                      outputEncoding=querier.encoding)
  875
+                    elif rdata.rdtype == dns.rdatatype.PTR:
  876
+                        icontext.addGlobal ("hostname", rdata.target)
  877
+                        icontext.addGlobal ("path", self.link_of(rdata.target, querier))
  878
+                        self.ptr_template.expand (icontext, iresult,
  879
+                                                       suppressXMLDeclaration=True,
  880
+                                                      outputEncoding=querier.encoding)
  881
+                    elif rdata.rdtype == dns.rdatatype.SRV:
  882
+                        icontext.addGlobal ("hostname", rdata.target)
  883
+                        icontext.addGlobal ("path", self.link_of(rdata.target, querier))
  884
+                        icontext.addGlobal ("priority", rdata.priority)
  885
+                        icontext.addGlobal ("weight", rdata.weight)
  886
+                        icontext.addGlobal ("port", rdata.port)
  887
+                        self.srv_template.expand (icontext, iresult,
  888
+                                                       suppressXMLDeclaration=True,
  889
+                                                      outputEncoding=querier.encoding)
  890
+                    elif rdata.rdtype == dns.rdatatype.TXT:
  891
+                        icontext.addGlobal ("text", "\n".join(rdata.strings))
  892
+                        self.txt_template.expand (icontext, iresult,
  893
+                                                       suppressXMLDeclaration=True,
  894
+                                                      outputEncoding=querier.encoding)
  895
+                    elif rdata.rdtype == dns.rdatatype.SPF:
  896
+                        icontext.addGlobal ("text", "\n".join(rdata.strings))
  897
+                        self.spf_template.expand (icontext, iresult,
  898
+                                                       suppressXMLDeclaration=True,
  899
+                                                      outputEncoding=querier.encoding)
  900
+                    elif rdata.rdtype == dns.rdatatype.LOC:
  901
+                        # TODO: expanded longitude and latitude instead of floats?
  902
+                        icontext.addGlobal ("longitude", rdata.float_longitude)
  903
+                        icontext.addGlobal ("latitude", rdata.float_latitude)
  904
+                        icontext.addGlobal ("altitude", rdata.altitude)
  905
+                        self.loc_template.expand (icontext, iresult,
  906
+                                                       suppressXMLDeclaration=True,
  907
+                                                      outputEncoding=querier.encoding)
  908
+                    elif rdata.rdtype == dns.rdatatype.DS:
  909
+                        icontext.addGlobal ("algorithm", rdata.algorithm)
  910
+                        icontext.addGlobal ("digesttype", rdata.digest_type)
  911
+                        icontext.addGlobal ("digest", rdata.digest)
  912
+                        icontext.addGlobal ("keytag", rdata.key_tag)
  913
+                        self.ds_template.expand (icontext, iresult,
  914
+                                                       suppressXMLDeclaration=True,
  915
+                                                      outputEncoding=querier.encoding)
  916
+                    elif rdata.rdtype == dns.rdatatype.DLV:
  917
+                        icontext.addGlobal ("algorithm", rdata.algorithm)
  918
+                        icontext.addGlobal ("digesttype", rdata.digest_type)
  919
+                        icontext.addGlobal ("digest", rdata.digest)
  920
+                        icontext.addGlobal ("keytag", rdata.key_tag)
  921
+                        self.dlv_template.expand (icontext, iresult,
  922
+                                                       suppressXMLDeclaration=True,
  923
+                                                      outputEncoding=querier.encoding)
  924
+                    elif rdata.rdtype == dns.rdatatype.DNSKEY:
  925
+                        icontext.addGlobal ("algorithm", rdata.algorithm)
  926
+                        icontext.addGlobal ("protocol", rdata.protocol)
  927
+                        icontext.addGlobal ("flags", rdata.flags)
  928
+                        try:
  929
+                            key_tag = dns.dnssec.key_id(rdata)
  930
+                            icontext.addGlobal ("keytag", key_tag)
  931
+                        except AttributeError:
  932
+                            # key_id appeared only in dnspython 1.9. Not
  933
+                            # always available on 2012-05-17
  934
+                            pass
  935
+                        self.dnskey_template.expand (icontext, iresult,
  936
+                                                       suppressXMLDeclaration=True,
  937
+                                                      outputEncoding=querier.encoding)
  938
+                    elif rdata.rdtype == dns.rdatatype.SSHFP:
  939
+                        icontext.addGlobal ("algorithm", rdata.algorithm)
  940
+                        icontext.addGlobal ("fptype", rdata.fp_type)
  941
+                        icontext.addGlobal ("fingerprint", to_hexstring(rdata.fingerprint))
  942
+                        self.sshfp_template.expand (icontext, iresult,
  943
+                                                       suppressXMLDeclaration=True,
  944
+                                                      outputEncoding=querier.encoding)
  945
+                    elif rdata.rdtype == dns.rdatatype.NAPTR:
  946
+                        icontext.addGlobal ("flags", rdata.flags)
  947
+                        icontext.addGlobal ("order", rdata.order)
  948
+                        icontext.addGlobal ("preference", rdata.preference)
  949
+                        icontext.addGlobal ("services", rdata.service)
  950
+                        icontext.addGlobal ("regexp", unicode(rdata.regexp,
  951
+                                                              "UTF-8")) # UTF-8 rdata is found in the wild
  952
+                        icontext.addGlobal ("replacement", rdata.replacement)
  953
+                        self.naptr_template.expand (icontext, iresult,
  954
+                                                       suppressXMLDeclaration=True,
  955
+                                                      outputEncoding=querier.encoding)
  956
+                    else:
  957
+                        icontext.addGlobal ("rrtype", rdata.rdtype)
  958
+                        self.unknown_template.expand (icontext, iresult,
  959
+                                                       suppressXMLDeclaration=True,
  960
+                                                      outputEncoding=querier.encoding)
  961
+                    records.append(unicode(iresult.getvalue(), querier.encoding))
  962
+                self.rrsets.append({'ttl': self.pretty_duration(rrset.ttl),
  963
+                                    'records': records})
  964
+        else:
  965
+            self.rrsets = None
  966
+
  967
+    def result(self, querier):
  968
+        result = simpleTALUtils.FastStringOutput()        
  969
+        self.context.addGlobal("rrsets", self.rrsets)
  970
+        self.template.expand (self.context, result, 
  971
+                              outputEncoding=querier.encoding,
  972
+                              docType='<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">')
  973
+        return (result.getvalue() + "\n")
29  DNSLG/LeakyBucket.py
... ...
@@ -0,0 +1,29 @@
  1
+default_bucket_size = 20
  2
+
  3
+import time
  4
+
  5
+class LeakyBucket():
  6
+
  7
+    def __init__(self, size=default_bucket_size):
  8
+        self.size = size
  9
+        self.content = 0
  10
+        self.last_check = time.time()
  11
+        
  12
+    def update(self):
  13
+        duration = time.time() - self.last_check
  14
+        offset = duration 
  15
+        if self.content > offset:
  16
+            self.content -= offset
  17
+        elif self.content == 0:
  18
+            pass
  19
+        else:
  20
+            self.content = 0
  21
+        self.last_check = time.time()
  22
+
  23
+    def add(self, amount=1):
  24
+        if not self.full():
  25
+            self.content += amount
  26
+
  27
+    def full(self):
  28
+        self.update()
  29
+        return self.content >= self.size
362  DNSLG/__init__.py
... ...
@@ -0,0 +1,362 @@
  1
+#!/usr/bin/env python
  2
+
  3
+# Standard library
  4
+from cgi import escape
  5
+from urlparse import parse_qs
  6
+import encodings.idna
  7
+import os
  8
+from datetime import datetime
  9
+
  10
+# http://www.dnspython.org/
  11
+import dns.resolver
  12
+import dns.reversename
  13
+
  14
+# http://code.google.com/p/netaddr/ https://github.com/drkjam/netaddr
  15
+import netaddr
  16
+
  17
+# Internal modules
  18
+import Formatter
  19
+from LeakyBucket import LeakyBucket
  20
+import Answer
  21
+
  22
+# If you need to change thse values, it is better to do it when
  23
+# calling the Querier() constructor.
  24
+default_base_url = "" 
  25
+default_edns_size = 4096
  26
+# TODO: allow to use prefixes in the whitelist
  27
+default_whitelist=[netaddr.IPAddress("127.0.0.1"), netaddr.IPAddress("::1")]
  28
+default_encoding = "UTF-8"
  29
+default_handle_wk_files = True
  30
+default_bucket_size = 5
  31
+
  32
+# Misc. util. routines
  33
+def send_response(start_response, status, output, type):
  34
+    response_headers = [('Content-type', type),
  35
+                        ('Content-Length', str(len(output))),
  36
+                        ('Allow', 'GET')]
  37
+    start_response(status, response_headers)
  38
+
  39
+def punycode_of(domain):
  40
+    labels = domain.split(".")
  41
+    result = u""
  42
+    for label in labels:
  43
+        if label:
  44
+            result += (encodings.idna.ToASCII(label) + ".")
  45
+    return (result)
  46
+
  47
+class Querier:
  48
+
  49
+    def __init__(self, email_admin=None, url_doc=None, url_css=None, url_opensearch=None,
  50
+                 file_favicon=None,
  51
+                 encoding=default_encoding, base_url=default_base_url,
  52
+                 bucket_size=default_bucket_size,
  53
+                 whitelist=default_whitelist, edns_size=default_edns_size,
  54
+                 handle_wk_files=default_handle_wk_files,
  55
+                 google_code=None, description=None, description_html=None):
  56
+        self.resolver = dns.resolver.Resolver()
  57
+        self.default_nameservers = self.resolver.nameservers
  58
+        self.buckets = {}
  59
+        self.base_url = base_url
  60
+        self.whitelist = whitelist
  61
+        self.handle_wk_files = handle_wk_files
  62
+        self.email_admin = email_admin
  63
+        self.url_doc = url_doc
  64
+        self.url_css = url_css
  65
+        self.url_opensearch = url_opensearch
  66
+        if file_favicon:
  67
+            self.favicon = open(file_favicon).read()
  68
+        else:
  69
+            self.favicon = None
  70
+        self.encoding = encoding
  71
+        self.edns_size = edns_size
  72
+        self.bucket_size = default_bucket_size
  73
+        self.google_code = google_code
  74
+        self.description = description
  75
+        self.description_html = description_html
  76
+        self.reset_resolver()
  77
+        
  78
+    def reset_resolver(self):
  79
+        self.resolver.nameservers = self.default_nameservers[0:1] # Yes, it
  80
+        # decreases resilience but it seems it is the only way to be
  81
+        # sure of *which* name server actually replied (TODO: question
  82
+        # sent on the dnspython mailing lst on 2012-05-20). POssible
  83
+        # improvment: use the low-level interface of DNS Python and
  84
+        # handles this ourselves.
  85
+        # Default is to use EDNS without the DO bit
  86
+        if self.edns_size is not None:
  87
+            self.resolver.use_edns(0, 0, self.edns_size)
  88
+        else:
  89
+            self.resolver.use_edns(-1, 0, 0)
  90
+        self.resolver.search = []
  91
+            
  92
+    def default(self, start_response, path):
  93
+        output = """
  94
+I'm the default handler, \"%s\" was called.
  95
+Are you sure of the URL?\n""" % path
  96
+        send_response(start_response, '404 No such resource' , output, 'text/plain; charset=%s' % self.encoding)
  97
+        return [output]
  98
+
  99
+    def emptyfile(self, start_response):
  100
+        output = ""        
  101
+        send_response(start_response, '200 OK' , output, 'text/plain')
  102
+        return [output]
  103
+
  104
+    def robotstxt(self, start_response):
  105
+        # http://www.robotstxt.org/
  106
+        # TODO: allow to read it in the configuration file
  107
+        output = """
  108
+User-agent: *
  109
+Disallow: /
  110
+"""        
  111
+        send_response(start_response, '200 OK' , output, 'text/plain')
  112
+        return [output]
  113
+
  114
+    def notfound(self, start_response):
  115
+        output = "Not found\r\n"        
  116
+        send_response(start_response, '404 Not Found' , output, 'text/plain')
  117
+        return [output]
  118
+
  119
+    def query(self, start_response, path, client, format="HTML", alt_resolver=None,
  120
+              do_dnssec=False, tcp=False, edns_size=default_edns_size,
  121
+              reverse=False):
  122
+        """ path must starts with a /, then the domain name then an
  123
+        (optional) / followed by the QTYPE """
  124
+        # TODO: document and implement the query class
  125
+        if not path.startswith('/'):
  126
+            raise Exception("Internal error: no / at the beginning of %s" % path)
  127
+        plaintype = 'text/plain; charset=%s' % self.encoding
  128
+        if format == "TEXT":
  129
+            mtype = 'text/plain; charset=%s' % self.encoding
  130
+        elif format == "HTML":
  131
+            mtype = 'text/html; charset=%s' % self.encoding
  132
+        elif format == "JSON":
  133
+            mtype = 'application/json'
  134
+        elif format == "ZONE":
  135
+            mtype = 'text/dns' # RFC 4027
  136
+            # TODO: application/dns, "detached" DNS (binary) as in RFC 2540?
  137
+        elif format == "XML":
  138
+            mtype = 'application/xml'
  139
+        else:
  140
+            output = "Unsupported format \"%s\"\n" % format
  141
+            send_response(start_response, '400 Bad request', output, plaintype)
  142
+            return [output]
  143
+        ip_client = netaddr.IPAddress(client)
  144
+        if ip_client.version == 4: