commit
e4fad70827
@ -0,0 +1,8 @@
|
||||
import dns.resolver
|
||||
|
||||
class ExtendedAnswer(dns.resolver.Answer):
|
||||
def __init__(self, initial_answer):
|
||||
self.qname = initial_answer.qname
|
||||
self.rrsets = [initial_answer.rrset,]
|
||||
self.owner_name = initial_answer.rrset.name
|
||||
|
@ -0,0 +1,973 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import dns
|
||||
import dns.version as dnspythonversion
|
||||
import base64
|
||||
import platform
|
||||
import pkg_resources
|
||||
import time
|
||||
|
||||
# TODO: Accept explicit requests for CNAME and DNAME?
|
||||
# TODO: DANE/TLSA record type. Not yet in DNS Python so not easy...
|
||||
|
||||
import Answer
|
||||
|
||||
def to_hexstring(str):
|
||||
result = ""
|
||||
for char in str:
|
||||
result += ("%x" % ord(char))
|
||||
return result.upper()
|
||||
|
||||
class Formatter():
|
||||
""" This ia the base class for the various Formatters. A formatter
|
||||
takes a "DNS answer" object and format it for a given output
|
||||
format (JSON, XML, etc). Implementing a new format means deriving
|
||||
this class and providing the required methods."""
|
||||
def __init__(self, domain):
|
||||
try:
|
||||
self.myversion = pkg_resources.require("DNS-LG")[0].version
|
||||
except pkg_resources.DistributionNotFound:
|
||||
self.myversion = "VERSION UNKNOWN"
|
||||
self.domain = domain
|
||||
|
||||
def format(self, answer, qtype, flags, querier):
|
||||
""" Parameter "answer" must be of type
|
||||
Answer.ExtendedAnswer. "qtype" is a string, flags an integer
|
||||
and querier a DNSLG.Querier. This method changes the internal
|
||||
state of the Formatter, it returns nothing."""
|
||||
pass
|
||||
|
||||
def result(self, querier):
|
||||
""" Returns the state of the Formatter, to be sent to the client."""
|
||||
return "NOT IMPLEMENTED IN THE BASE CLASS"
|
||||
|
||||
# TEXT
|
||||
class TextFormatter(Formatter):
|
||||
|
||||
def format(self, answer, qtype, flags, querier):
|
||||
# TODO: it would be nice one day to have a short format to
|
||||
# have only data, not headers. Or may be several short
|
||||
# formats, suitable for typical Unix text parsing tools. In
|
||||
# the mean time, use "zone" for that.
|
||||
self.output = ""
|
||||
self.output += "Query for: %s, type %s\n" % (self.domain.encode(querier.encoding),
|
||||
qtype)
|
||||
if answer is not None and (str(answer.owner_name) != self.domain):
|
||||
self.output += "Result name: %s\n" % \
|
||||
str(answer.owner_name).encode(querier.encoding)
|
||||
str_flags = ""
|
||||
if flags & dns.flags.AD:
|
||||
str_flags += "/ Authentic Data "
|
||||
if flags & dns.flags.AA:
|
||||
str_flags += "/ Authoritative Answer "
|
||||
if flags & dns.flags.TC:
|
||||
str_flags += "/ Truncated Answer "
|
||||
self.output += "Flags: %s\n" % str_flags
|
||||
if answer is None:
|
||||
self.output += "[No data for this query type]\n"
|
||||
else:
|
||||
for rrset in answer.rrsets:
|
||||
for rdata in rrset:
|
||||
if rdata.rdtype == dns.rdatatype.A or rdata.rdtype == dns.rdatatype.AAAA:
|
||||
self.output += "IP address: %s\n" % rdata.address
|
||||
elif rdata.rdtype == dns.rdatatype.MX:
|
||||
self.output += "Mail exchanger: %s (preference %i)\n" % \
|
||||
(rdata.exchange, rdata.preference)
|
||||
elif rdata.rdtype == dns.rdatatype.TXT:
|
||||
self.output += "Text: %s\n" % " ".join(rdata.strings)
|
||||
elif rdata.rdtype == dns.rdatatype.SPF:
|
||||
self.output += "SPF policy: %s\n" % " ".join(rdata.strings)
|
||||
elif rdata.rdtype == dns.rdatatype.SOA:
|
||||
self.output += "Start of zone authority: serial number %i, zone administrator %s, master nameserver %s\n" % \
|
||||
(rdata.serial, rdata.rname, rdata.mname)
|
||||
elif rdata.rdtype == dns.rdatatype.NS:
|
||||
self.output += "Name server: %s\n" % rdata.target
|
||||
elif rdata.rdtype == dns.rdatatype.DS:
|
||||
self.output += "Delegation of signature: key %i, hash type %i\n" % \
|
||||
(rdata.key_tag, rdata.digest_type)
|
||||
# TODO: display the digest with to_hexstring
|
||||
elif rdata.rdtype == dns.rdatatype.DLV:
|
||||
self.output += "Delegation of signature: key %i, hash type %i\n" % \
|
||||
(rdata.key_tag, rdata.digest_type)
|
||||
elif rdata.rdtype == dns.rdatatype.RRSIG:
|
||||
pass # Should we show signatures?
|
||||
elif rdata.rdtype == dns.rdatatype.LOC:
|
||||
self.output += "Location: longitude %i degrees %i' %i\" latitude %i degrees %i' %i\" altitude %f\n" % \
|
||||
(rdata.longitude[0], rdata.longitude[1], rdata.longitude[2],
|
||||
rdata.latitude[0], rdata.latitude[1], rdata.latitude[2],
|
||||
rdata.altitude)
|
||||
elif rdata.rdtype == dns.rdatatype.SRV:
|
||||
self.output += "Service location: server %s, port %i, priority %i, weight %i\n" % \
|
||||
(rdata.target, rdata.port, rdata.priority, rdata.weight)
|
||||
elif rdata.rdtype == dns.rdatatype.PTR:
|
||||
self.output += "Target: %s\n" % rdata.target
|
||||
elif rdata.rdtype == dns.rdatatype.DNSKEY:
|
||||
self.output += "DNSSEC key: "
|
||||
try:
|
||||
key_tag = dns.dnssec.key_id(rdata)
|
||||
self.output += "tag %i " % key_tag
|
||||
except AttributeError:
|
||||
# key_id appeared only in dnspython 1.9. Not
|
||||
# always available on 2012-05-17
|
||||
pass
|
||||
self.output += "algorithm %i, flags %i\n" % (rdata.algorithm, rdata.flags)
|
||||
elif rdata.rdtype == dns.rdatatype.SSHFP:
|
||||
self.output += "SSH fingerprint: algorithm %i, digest type %i, fingerprint %s\n" % \
|
||||
(rdata.algorithm, rdata.fp_type, to_hexstring(rdata.fingerprint))
|
||||
elif rdata.rdtype == dns.rdatatype.NAPTR:
|
||||
self.output += ("Naming Authority Pointer: flags \"%s\", order %i, " + \
|
||||
"preference %i, rexegp \"%s\" -> replacement \"%s\", " + \
|
||||
"services \"%s\"\n") % \
|
||||
(rdata.flags, rdata.order, rdata.preference,
|
||||
rdata.regexp, str(rdata.replacement), rdata.service)
|
||||
else:
|
||||
self.output += "Unknown record type %i: (DATA)\n" % rdata.rdtype
|
||||
self.output += "TTL: %i\n" % rrset.ttl
|
||||
self.output += "Resolver queried: %s\n" % querier.resolver.nameservers[0]
|
||||
self.output += "Query done at: %s\n" % time.strftime("%Y-%m-%d %H:%M:%SZ",
|
||||
time.gmtime(time.time()))
|
||||
self.output += "Query duration: %s\n" % querier.delay
|
||||
if querier.description:
|
||||
self.output += "Service description: %s\n" % querier.description
|
||||
self.output += "DNS Looking Glass %s, DNSpython version %s, Python version %s %s on %s\n" % \
|
||||
(self.myversion,
|
||||
dnspythonversion.version, platform.python_implementation(),
|
||||
platform.python_version(), platform.system())
|
||||
|
||||
def result(self, querier):
|
||||
return self.output
|
||||
|
||||
|
||||
# ZONE FILE
|
||||
class ZoneFormatter(Formatter):
|
||||
|
||||
def format(self, answer, qtype, flags, querier):
|
||||
self.output = ""
|
||||
self.output += "; Question: %s, type %s\n" % (self.domain.encode(querier.encoding),
|
||||
qtype)
|
||||
str_flags = ""
|
||||
if flags & dns.flags.AD:
|
||||
str_flags += " ad "
|
||||
if flags & dns.flags.AA:
|
||||
str_flags += " aa "
|
||||
if flags & dns.flags.TC:
|
||||
str_flags += " tc "
|
||||
if str_flags != "":
|
||||
self.output += "; Flags:" + str_flags + "\n"
|
||||
self.output += "\n"
|
||||
if answer is None:
|
||||
self.output += "; No data for this type\n"
|
||||
else:
|
||||
for rrset in answer.rrsets:
|
||||
for rdata in rrset:
|
||||
# TODO: do not hardwire the class
|
||||
self.output += "%s\tIN\t" % answer.owner_name # TODO: do not repeat the name if there is a RRset
|
||||
# TODO: it could use some refactoring: most (but _not all_) of types
|
||||
# use the same code.
|
||||
if rdata.rdtype == dns.rdatatype.A:
|
||||
self.output += "A\t%s\n" % rdata.to_text()
|
||||
elif rdata.rdtype == dns.rdatatype.AAAA:
|
||||
self.output += "AAAA\t%s\n" % rdata.to_text()
|
||||
elif rdata.rdtype == dns.rdatatype.MX:
|
||||
self.output += "MX\t%s\n" % rdata.to_text()
|
||||
elif rdata.rdtype == dns.rdatatype.SPF:
|
||||
self.output += "SPF\t%s\n" % rdata.to_text()
|
||||
elif rdata.rdtype == dns.rdatatype.TXT:
|
||||
self.output += "TXT\t%s\n" % rdata.to_text()
|
||||
elif rdata.rdtype == dns.rdatatype.SOA:
|
||||
self.output += "SOA\t%s\n" % rdata.to_text()
|
||||
elif rdata.rdtype == dns.rdatatype.NS:
|
||||
self.output += "NS\t%s\n" % rdata.to_text()
|
||||
elif rdata.rdtype == dns.rdatatype.PTR:
|
||||
self.output += "PTR\t%s\n" % rdata.to_text()
|
||||
elif rdata.rdtype == dns.rdatatype.LOC:
|
||||
self.output += "LOC\t%s\n" % rdata.to_text()
|
||||
elif rdata.rdtype == dns.rdatatype.DNSKEY:
|
||||
self.output += "DNSKEY\t%s" % rdata.to_text()
|
||||
try:
|
||||
key_tag = dns.dnssec.key_id(rdata)
|
||||
self.output += "; key ID = %i\n" % key_tag
|
||||
except AttributeError:
|
||||
# key_id appeared only in dnspython 1.9. Not
|
||||
# always available on 2012-05-17
|
||||
self.output += "\n"
|
||||
elif rdata.rdtype == dns.rdatatype.DS:
|
||||
self.output += "DS\t%s\n" % rdata.to_text()
|
||||
elif rdata.rdtype == dns.rdatatype.DLV:
|
||||
self.output += "DLV\t%s\n" % rdata.to_text()
|
||||
elif rdata.rdtype == dns.rdatatype.SSHFP:
|
||||
self.output += "SSHFP\t%s\n" % rdata.to_text()
|
||||
elif rdata.rdtype == dns.rdatatype.NAPTR:
|
||||
self.output += "NAPTR\t%s\n" % rdata.to_text()
|
||||
elif rdata.rdtype == dns.rdatatype.RRSIG:
|
||||
pass # Should we show signatures?
|
||||
elif rdata.rdtype == dns.rdatatype.SRV:
|
||||
self.output += "SRV\t%s\n" % rdata.to_text()
|
||||
else:
|
||||
# dnspython dumps the types it knows. TODO: uses that?
|
||||
self.output += "TYPE%i ; DATA %s\n" % (rdata.rdtype, rdata.to_text())
|
||||
self.output += "; TTL: %i\n" % rrset.ttl
|
||||
self.output += "\n; Server: %s\n" % querier.resolver.nameservers[0]
|
||||
self.output += "; When: %s\n" % time.strftime("%Y-%m-%d %H:%M:%SZ",
|
||||
time.gmtime(time.time()))
|
||||
self.output += "; Query duration: %s\n" % querier.delay
|
||||
if querier.description:
|
||||
self.output += "; Service description: %s\n" % querier.description
|
||||
self.output += "; DNS Looking Glass %s, DNSpython version %s, Python version %s %s on %s\n" % \
|
||||
(self.myversion, dnspythonversion.version,
|
||||
platform.python_implementation(),
|
||||
platform.python_version(), platform.system())
|
||||
|
||||
def result(self, querier):
|
||||
return self.output
|
||||
|
||||
|
||||
# JSON
|
||||
import json
|
||||
# http://docs.python.org/library/json.html
|
||||
class JsonFormatter(Formatter):
|
||||
|
||||
def format(self, answer, qtype, flags, querier):
|
||||
self.object = {}
|
||||
self.object['ReturnCode'] = "NOERROR"
|
||||
self.object['QuestionSection'] = {'Qname': self.domain, 'Qtype': qtype}
|
||||
if flags & dns.flags.AD:
|
||||
self.object['AD'] = True
|
||||
if flags & dns.flags.AA:
|
||||
self.object['AA'] = True
|
||||
if flags & dns.flags.TC:
|
||||
self.object['TC'] = True
|
||||
self.object['AnswerSection'] = []
|
||||
if answer is not None:
|
||||
for rrset in answer.rrsets:
|
||||
for rdata in rrset: # TODO: sort them? For instance by preference for MX?
|
||||
if rdata.rdtype == dns.rdatatype.A:
|
||||
self.object['AnswerSection'].append({'Type': 'A', 'Address': rdata.address})
|
||||
elif rdata.rdtype == dns.rdatatype.AAAA:
|
||||
self.object['AnswerSection'].append({'Type': 'AAAA', 'Address': rdata.address})
|
||||
elif rdata.rdtype == dns.rdatatype.LOC:
|
||||
self.object['AnswerSection'].append({'Type': 'LOC',
|
||||
'Longitude': '%f' % rdata.float_longitude,
|
||||
'Latitude': '%f' % rdata.float_latitude,
|
||||
'Altitude': '%f' % rdata.altitude})
|
||||
elif rdata.rdtype == dns.rdatatype.PTR:
|
||||
self.object['AnswerSection'].append({'Type': 'PTR',
|
||||
'Target': str(rdata.target)})
|
||||
elif rdata.rdtype == dns.rdatatype.MX:
|
||||
self.object['AnswerSection'].append({'Type': 'MX',
|
||||
'MailExchanger': str(rdata.exchange),
|
||||
'Preference': rdata.preference})
|
||||
elif rdata.rdtype == dns.rdatatype.TXT:
|
||||
self.object['AnswerSection'].append({'Type': 'TXT', 'Text': " ".join(rdata.strings)})
|
||||
elif rdata.rdtype == dns.rdatatype.SPF:
|
||||
self.object['AnswerSection'].append({'Type': 'SPF', 'Text': " ".join(rdata.strings)})
|
||||
elif rdata.rdtype == dns.rdatatype.SOA:
|
||||
self.object['AnswerSection'].append({'Type': 'SOA', 'Serial': rdata.serial,
|
||||
'MasterServerName': str(rdata.mname),
|
||||
'MaintainerName': str(rdata.rname),
|
||||
'Refresh': rdata.refresh,
|
||||
'Retry': rdata.retry,
|
||||
'Expire': rdata.expire,
|
||||
'Minimum': rdata.minimum,
|
||||
})
|
||||
elif rdata.rdtype == dns.rdatatype.NS:
|
||||
self.object['AnswerSection'].append({'Type': 'NS', 'Name': str(rdata.target)})
|
||||
elif rdata.rdtype == dns.rdatatype.DNSKEY:
|
||||
returned_object = {'Type': 'DNSKEY',
|
||||
'Algorithm': rdata.algorithm,
|
||||
'Flags': rdata.flags}
|
||||
try:
|
||||
key_tag = dns.dnssec.key_id(rdata)
|
||||
returned_object['Tag'] = key_tag
|
||||
except AttributeError:
|
||||
# key_id appeared only in dnspython 1.9. Not
|
||||
# always available on 2012-05-17
|
||||
pass
|
||||
self.object['AnswerSection'].append(returned_object)
|
||||
elif rdata.rdtype == dns.rdatatype.DS:
|
||||
self.object['AnswerSection'].append({'Type': 'DS', 'DelegationKey': rdata.key_tag,
|
||||
'DigestType': rdata.digest_type})
|
||||
elif rdata.rdtype == dns.rdatatype.DLV:
|
||||
self.object['AnswerSection'].append({'Type': 'DLV', 'DelegationKey': rdata.key_tag,
|
||||
'DigestType': rdata.digest_type})
|
||||
elif rdata.rdtype == dns.rdatatype.RRSIG:
|
||||
pass # Should we show signatures?
|
||||
elif rdata.rdtype == dns.rdatatype.SSHFP:
|
||||
self.object['AnswerSection'].append({'Type': 'SSHFP',
|
||||
'Algorithm': rdata.algorithm,
|
||||
'DigestType': rdata.fp_type,
|
||||
'Fingerprint': to_hexstring(rdata.fingerprint)})
|
||||
elif rdata.rdtype == dns.rdatatype.NAPTR:
|
||||
self.object['AnswerSection'].append({'Type': 'NAPTR',
|
||||
'Flags': rdata.flags,
|
||||
'Services': rdata.service,
|
||||
'Order': rdata.order,
|
||||
'Preference': rdata.preference,
|
||||
'Regexp': rdata.regexp,
|
||||
'Replacement': str(rdata.replacement)})
|
||||
elif rdata.rdtype == dns.rdatatype.SRV:
|
||||
self.object['AnswerSection'].append({'Type': 'SRV', 'Server': str(rdata.target),
|
||||
'Port': rdata.port,
|
||||
'Priority': rdata.priority,
|
||||
'Weight': rdata.weight})
|
||||
else:
|
||||
self.object['AnswerSection'].append({'Type': "unknown"}) # TODO: the type number
|
||||
self.object['AnswerSection'][-1]['TTL'] = rrset.ttl
|
||||
self.object['AnswerSection'][-1]['Name'] = str(answer.owner_name)
|
||||
try:
|
||||
duration = querier.delay.total_seconds()
|
||||
except AttributeError: # total_seconds appeared only with Python 2.7
|
||||
delay = querier.delay
|
||||
duration = (delay.days*86400) + delay.seconds + \
|
||||
(float(delay.microseconds)/1000000.0)
|
||||
self.object['Query'] = {'Server': querier.resolver.nameservers[0],
|
||||
'Duration': duration}
|
||||
if querier.description:
|
||||
self.object['Query']['Description'] = querier.description
|
||||
self.object['Query']['Versions'] = "DNS Looking Glass %s, DNSpython version %s, Python version %s %s on %s\n" % \
|
||||
(self.myversion, dnspythonversion.version,
|
||||
platform.python_implementation(),
|
||||
platform.python_version(), platform.system())
|
||||
|
||||
|
||||
def result(self, querier):
|
||||
return json.dumps(self.object) + "\n"
|
||||
|
||||
|
||||
# XML
|
||||
# http://www.owlfish.com/software/simpleTAL/
|
||||
from simpletal import simpleTAL, simpleTALES, simpleTALUtils
|
||||
xml_template = """
|
||||
<result>
|
||||
<query>
|
||||
<question><qname tal:content="qname"/><qtype tal:content="qtype"/></question>
|
||||
<server><resolver tal:content="resolver"/><duration tal:content="duration"/><description tal:condition="description" tal:content="description"/><versions tal:condition="version" tal:content="version"/></server>
|
||||
</query>
|
||||
<response>
|
||||
<!-- TODO: query ID -->
|
||||
<ad tal:condition="ad" tal:content="ad"/><tc tal:condition="tc" tal:content="tc"/><aa tal:condition="aa" tal:content="aa"/>
|
||||
<!-- No <anscount>, it is useless in XML. -->
|
||||
<answers tal:condition="rrsets">
|
||||
<rrset tal:replace="structure rrset" tal:repeat="rrset rrsets"/>
|
||||
</answers>
|
||||
</response>
|
||||
</result>
|
||||
"""
|
||||
set_xml_template = """
|
||||
<RRSet tal:condition="records" class="IN" tal:attributes="owner ownername; type type; ttl ttl"><record tal:repeat="record records" tal:replace="structure record"/></RRSet>
|
||||
"""
|
||||
a_xml_template = """
|
||||
<A tal:attributes="address address"/>
|
||||
"""
|
||||
aaaa_xml_template = """
|
||||
<AAAA tal:attributes="ip6address address"/>
|
||||
"""
|
||||
mx_xml_template = """
|
||||
<MX tal:attributes="preference preference; exchange exchange"/>
|
||||
"""
|
||||
ns_xml_template = """
|
||||
<NS tal:attributes="nsdname name"/>
|
||||
"""
|
||||
srv_xml_template = """
|
||||
<SRV tal:attributes="priority priority; weight weight; port port; target name"/>
|
||||
"""
|
||||
txt_xml_template = """
|
||||
<TXT tal:attributes="rdata text"/>
|
||||
"""
|
||||
spf_xml_template = """
|
||||
<SPF tal:attributes="rdata text"/>
|
||||
"""
|
||||
loc_xml_template = """
|
||||
<LOC tal:attributes="longitude longitude; latitude latitude; altitude altitude"/>
|
||||
"""
|
||||
ptr_xml_template = """
|
||||
<PTR tal:attributes="ptrdname name"/>
|
||||
"""
|
||||
ds_xml_template = """
|
||||
<DS tal:attributes="keytag keytag; algorithm algorithm; digesttype digesttype; digest digest"/>
|
||||
"""
|
||||
dlv_xml_template = """
|
||||
<DLV tal:attributes="keytag keytag; algorithm algorithm; digesttype digesttype; digest digest"/>
|
||||
"""
|
||||
# TODO: keytag is an extension to the Internet-Draft
|
||||
dnskey_xml_template = """
|
||||
<DNSKEY tal:attributes="flags flags; protocol protocol; algorithm algorithm; publickey key; keytag keytag"/>
|
||||
"""
|
||||
sshfp_xml_template = """
|
||||
<SSHFP tal:attributes="algorithm algorithm; fptype fptype; fingerprint fingerprint"/>
|
||||
"""
|
||||
naptr_xml_template = """
|
||||
<NAPTR tal:attributes="flags flags; order order; preference preference; services services; regexp regexp; replacement replacement"/>
|
||||
"""
|
||||
soa_xml_template = """
|
||||
<SOA tal:attributes="mname mname; rname rname; serial serial; refresh refresh; retry retry; expire expire; minimum minimum"/>
|
||||
"""
|
||||
# TODO: how to keep the comments of a template in TAL's output?
|
||||
unknown_xml_template = """
|
||||
<binaryRR tal:attributes="rtype rtype; rdlength rdlength; rdata rdata"/> <!-- Unknown type -->
|
||||
"""
|
||||
# TODO: Why is there a rdlength when you can deduce it from the rdata?
|
||||
# That's strange in a non-binary format like XML.
|
||||
class XmlFormatter(Formatter):
|
||||
|
||||
def format(self, answer, qtype, flags, querier):
|
||||
self.xml_template = simpleTAL.compileXMLTemplate (xml_template)
|
||||
self.set_template = simpleTAL.compileXMLTemplate (set_xml_template)
|
||||
self.a_template = simpleTAL.compileXMLTemplate (a_xml_template)
|
||||
self.aaaa_template = simpleTAL.compileXMLTemplate (aaaa_xml_template)
|
||||
self.mx_template = simpleTAL.compileXMLTemplate (mx_xml_template)
|
||||
self.srv_template = simpleTAL.compileXMLTemplate (srv_xml_template)
|
||||
self.txt_template = simpleTAL.compileXMLTemplate (txt_xml_template)
|
||||
self.spf_template = simpleTAL.compileXMLTemplate (spf_xml_template)
|
||||
self.loc_template = simpleTAL.compileXMLTemplate (loc_xml_template)
|
||||
self.ns_template = simpleTAL.compileXMLTemplate (ns_xml_template)
|
||||
self.ptr_template = simpleTAL.compileXMLTemplate (ptr_xml_template)
|
||||
self.soa_template = simpleTAL.compileXMLTemplate (soa_xml_template)
|
||||
self.ds_template = simpleTAL.compileXMLTemplate (ds_xml_template)
|
||||
self.dlv_template = simpleTAL.compileXMLTemplate (dlv_xml_template)
|
||||
self.dnskey_template = simpleTAL.compileXMLTemplate (dnskey_xml_template)
|
||||
self.sshfp_template = simpleTAL.compileXMLTemplate (sshfp_xml_template)
|
||||
self.naptr_template = simpleTAL.compileXMLTemplate (naptr_xml_template)
|
||||
self.unknown_template = simpleTAL.compileXMLTemplate (unknown_xml_template)
|
||||
self.context = simpleTALES.Context(allowPythonPath=False)
|
||||
self.acontext = simpleTALES.Context(allowPythonPath=False)
|
||||
self.rcontext = simpleTALES.Context(allowPythonPath=False)
|
||||
self.context.addGlobal ("qname", self.domain)
|
||||
self.context.addGlobal ("qtype", qtype)
|
||||
self.context.addGlobal ("resolver", querier.resolver.nameservers[0])
|
||||
try:
|
||||
duration = querier.delay.total_seconds()
|
||||
except AttributeError: # total_seconds appeared only with Python 2.7
|
||||
delay = querier.delay
|
||||
duration = (delay.days*86400) + delay.seconds + \
|
||||
(float(delay.microseconds)/1000000.0)
|
||||
self.context.addGlobal ("duration", duration)
|
||||
self.context.addGlobal ("description", querier.description)
|
||||
self.context.addGlobal ("version",
|
||||
"DNS Looking Glass %s, DNSpython version %s, Python version %s %s on %s\n" % \
|
||||
(self.myversion, dnspythonversion.version,
|
||||
platform.python_implementation(),
|
||||
platform.python_version(), platform.system()))
|
||||
addresses = []
|
||||
if answer is not None:
|
||||
self.rrsets = []
|
||||
self.acontext.addGlobal ("ownername", answer.owner_name)
|
||||
if flags & dns.flags.AD:
|
||||
ad = 1
|
||||
else:
|
||||
ad = 0
|
||||
self.context.addGlobal ("ad", ad)
|
||||
if flags & dns.flags.TC:
|
||||
tc = 1
|
||||
else:
|
||||
tc = 0
|
||||
self.context.addGlobal ("tc", tc)
|
||||
if flags & dns.flags.AA:
|
||||
aa = 1
|
||||
else:
|
||||
aa = 0
|
||||
self.context.addGlobal ("aa", aa)
|
||||
# TODO: class
|
||||
for rrset in answer.rrsets:
|
||||
records = []
|
||||
self.acontext.addGlobal ("ttl", rrset.ttl)
|
||||
self.acontext.addGlobal ("type", dns.rdatatype.to_text(rrset.rdtype))
|
||||
for rdata in rrset:
|
||||
icontext = simpleTALES.Context(allowPythonPath=False)
|
||||
iresult = simpleTALUtils.FastStringOutput()
|
||||
if rdata.rdtype == dns.rdatatype.A or rdata.rdtype == dns.rdatatype.AAAA:
|
||||
icontext.addGlobal ("address", rdata.address)
|
||||
if rdata.rdtype == dns.rdatatype.A:
|
||||
self.a_template.expand (icontext, iresult,
|
||||
suppressXMLDeclaration=True,
|
||||
outputEncoding=querier.encoding)
|
||||
else:
|
||||
self.aaaa_template.expand (icontext, iresult,
|
||||
suppressXMLDeclaration=True,
|
||||
outputEncoding=querier.encoding)
|
||||
elif rdata.rdtype == dns.rdatatype.SRV:
|
||||
icontext.addGlobal ("priority", rdata.priority)
|
||||
icontext.addGlobal ("weight", rdata.weight)
|
||||
icontext.addGlobal ("port", rdata.port)
|
||||
icontext.addGlobal ("name", rdata.target)
|
||||
self.srv_template.expand (icontext, iresult,
|
||||
suppressXMLDeclaration=True,
|
||||
outputEncoding=querier.encoding)
|
||||
elif rdata.rdtype == dns.rdatatype.MX:
|
||||
icontext.addGlobal ("preference", rdata.preference)
|
||||
icontext.addGlobal ("exchange", rdata.exchange)
|
||||
self.mx_template.expand (icontext, iresult,
|
||||
suppressXMLDeclaration=True,
|
||||
outputEncoding=querier.encoding)
|
||||
elif rdata.rdtype == dns.rdatatype.DS:
|
||||
icontext.addGlobal ("keytag", rdata.key_tag)
|
||||
icontext.addGlobal ("digesttype", rdata.digest_type)
|
||||
icontext.addGlobal ("algorithm", rdata.algorithm)
|
||||
icontext.addGlobal ("digest", "TODO") # rdata.digest is binary, encode it first with to_hexstring()
|
||||
self.ds_template.expand (icontext, iresult,
|
||||
suppressXMLDeclaration=True,
|
||||
outputEncoding=querier.encoding)
|
||||
elif rdata.rdtype == dns.rdatatype.DLV:
|
||||
icontext.addGlobal ("keytag", rdata.key_tag)
|
||||
icontext.addGlobal ("digesttype", rdata.digest_type)
|
||||
icontext.addGlobal ("algorithm", rdata.algorithm)
|
||||
icontext.addGlobal ("digest", "TODO") # rdata.digest is binary, encode it first with to_hexstring()
|
||||
self.dlv_template.expand (icontext, iresult,
|
||||
suppressXMLDeclaration=True,
|
||||
outputEncoding=querier.encoding)
|
||||
elif rdata.rdtype == dns.rdatatype.DNSKEY:
|
||||
try:
|
||||
key_tag = dns.dnssec.key_id(rdata)
|
||||
icontext.addGlobal ("keytag", key_tag)
|
||||
except AttributeError:
|
||||
# key_id appeared only in dnspython 1.9. Not
|
||||
# always available on 2012-05-17
|
||||
pass
|
||||
icontext.addGlobal ("protocol", rdata.protocol)
|
||||
icontext.addGlobal ("flags", rdata.flags)
|
||||
icontext.addGlobal ("algorithm", rdata.algorithm)
|
||||
icontext.addGlobal ("key", "TODO") # rdata.key is binary, encode it first with to_hexstring()
|
||||
self.dnskey_template.expand (icontext, iresult,
|
||||
suppressXMLDeclaration=True,
|
||||
outputEncoding=querier.encoding)
|
||||
elif rdata.rdtype == dns.rdatatype.SSHFP:
|
||||
icontext.addGlobal ("algorithm", rdata.algorithm)
|
||||
icontext.addGlobal ("fptype", rdata.fp_type)
|
||||
icontext.addGlobal ("fingerprint", to_hexstring(rdata.fingerprint))
|
||||
self.sshfp_template.expand (icontext, iresult,
|
||||
suppressXMLDeclaration=True,
|
||||
outputEncoding=querier.encoding)
|
||||
elif rdata.rdtype == dns.rdatatype.NAPTR:
|
||||
icontext.addGlobal ("flags", rdata.flags)
|
||||
icontext.addGlobal ("services", rdata.service)
|
||||
icontext.addGlobal ("order", rdata.order)
|
||||
icontext.addGlobal ("preference", rdata.preference)
|
||||
regexp = unicode(rdata.regexp, "UTF-8")
|
||||
icontext.addGlobal ("regexp",
|
||||
regexp)
|
||||
# Yes, there is Unicode in NAPTRs, see
|
||||
# mailclub.tel for instance. We assume it will
|
||||
# always be UTF-8
|
||||
icontext.addGlobal ("replacement", rdata.replacement)
|
||||
self.naptr_template.expand (icontext, iresult,
|
||||
suppressXMLDeclaration=True,
|
||||
outputEncoding=querier.encoding)
|
||||
elif rdata.rdtype == dns.rdatatype.TXT:
|
||||
icontext.addGlobal ("text", " ".join(rdata.strings))
|
||||
self.txt_template.expand (icontext, iresult,
|
||||
suppressXMLDeclaration=True,
|
||||
outputEncoding=querier.encoding)
|
||||
elif rdata.rdtype == dns.rdatatype.SPF:
|
||||
icontext.addGlobal ("text", " ".join(rdata.strings))
|
||||
self.spf_template.expand (icontext, iresult,
|
||||
suppressXMLDeclaration=True,
|
||||
outputEncoding=querier.encoding)
|
||||
elif rdata.rdtype == dns.rdatatype.PTR:
|
||||
icontext.addGlobal ("name", rdata.target)
|
||||
self.ptr_template.expand (icontext, iresult,
|
||||
suppressXMLDeclaration=True,
|
||||
outputEncoding=querier.encoding)
|
||||
elif rdata.rdtype == dns.rdatatype.LOC:
|
||||
icontext.addGlobal ("longitude", rdata.float_longitude)
|
||||
icontext.addGlobal ("latitude", rdata.float_latitude)
|
||||
icontext.addGlobal ("altitude", rdata.altitude)
|
||||
self.loc_template.expand (icontext, iresult,
|
||||
suppressXMLDeclaration=True,
|
||||
outputEncoding=querier.encoding)
|
||||
elif rdata.rdtype == dns.rdatatype.NS:
|
||||
icontext.addGlobal ("name", rdata.target)
|
||||
# TODO: translate Punycode domain names back to Unicode?
|
||||
self.ns_template.expand (icontext, iresult,
|
||||
suppressXMLDeclaration=True)
|
||||
elif rdata.rdtype == dns.rdatatype.SOA:
|
||||
icontext.addGlobal ("rname", rdata.rname)
|
||||
icontext.addGlobal ("mname", rdata.mname)
|
||||
icontext.addGlobal ("serial", rdata.serial)
|
||||
icontext.addGlobal ("refresh", rdata.refresh)
|
||||
icontext.addGlobal ("retry", rdata.retry)
|
||||
icontext.addGlobal ("expire", rdata.expire)
|
||||
icontext.addGlobal ("minimum", rdata.minimum)
|
||||
self.soa_template.expand (icontext, iresult,
|
||||
suppressXMLDeclaration=True,
|
||||
outputEncoding=querier.encoding)
|
||||
else:
|
||||
icontext.addGlobal ("rtype", rdata.rdtype)
|
||||
icontext.addGlobal ("rdlength", 0) # TODO: useless, anyway (and
|
||||
# no easy way to compute it in dnspython)
|
||||
# TODO: rdata
|
||||
self.unknown_template.expand (icontext, iresult,
|
||||
suppressXMLDeclaration=True,
|
||||
outputEncoding=querier.encoding)
|
||||
records.append(unicode(iresult.getvalue(), querier.encoding))
|
||||
else:
|
||||
pass # TODO what to send back when no data for this QTYPE?
|
||||
if records:
|
||||
self.acontext.addGlobal ("records", records)
|
||||
self.acontext.addGlobal ("ttl", rrset.ttl)
|
||||
iresult = simpleTALUtils.FastStringOutput()
|
||||
self.set_template.expand (self.acontext, iresult,
|
||||
suppressXMLDeclaration=True,
|
||||
outputEncoding=querier.encoding)
|
||||
self.rrsets.append(unicode(iresult.getvalue(), querier.encoding))
|
||||
else:
|
||||
self.rrsets = None
|
||||
|
||||
def result(self, querier):
|
||||
result = simpleTALUtils.FastStringOutput()
|
||||
self.context.addGlobal("rrsets", self.rrsets)
|
||||
self.xml_template.expand (self.context, result,
|
||||
outputEncoding=querier.encoding)
|
||||
return result.getvalue()
|
||||
|
||||
|
||||
# HTML
|
||||
html_template = """<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<title tal:content="title"/>
|
||||
<link tal:condition="css" rel="stylesheet" type="text/css" tal:attributes="href css"/>
|
||||
<link rel="author" href="http://www.bortzmeyer.org/static/moi.html"/>
|
||||
<link tal:condition="opensearch" rel="search"
|
||||
type="application/opensearchdescription+xml"
|
||||
tal:attributes="href opensearch"
|
||||
title="DNS Looking Glass" />
|
||||
<meta http-equiv="Content-Type" tal:attributes="content contenttype"/>
|
||||
<meta name="robots" content="noindex,nofollow"/>
|
||||
</head>
|
||||
<body>
|
||||
<h1 tal:content="title"/>
|
||||
<div class="body">
|
||||
<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>
|
||||
<div class="rrsets" tal:repeat="rrset rrsets">
|
||||
<p><span tal:condition="rrset/ttl">Time-to-Live of this answer is <span tal:replace="rrset/ttl"/>.</span></p>
|
||||
<ul tal:condition="rrset/records">
|
||||
<li tal:repeat="record rrset/records" tal:content="structure record"/>
|
||||
</ul>
|
||||
</div>
|
||||
<p tal:condition="not: rrsets">No data was found.</p>
|
||||
<p>Result obtained from resolver(s) <span class="hostname" tal:content="resolver"/> at <span tal:replace="datetime"/>. Query took <span tal:replace="duration"/>.</p>
|
||||
</div>
|
||||
<hr class="endsep"/>
|
||||
<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>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
version_html_template = """
|
||||
<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>
|
||||
"""
|
||||
address_html_template = """
|
||||
<a class="address" tal:attributes="href path" tal:content="address"/>
|
||||
"""
|
||||
mx_html_template = """
|
||||
<span><a class="hostname" tal:attributes="href path" tal:content="hostname"/> (preference <span tal:replace="pref"/>)</span>
|
||||
"""
|
||||
# TODO: better presentation of "admin" (replacement of . by @ and mailto: URL)
|
||||
# TODO: better presentation of intervals? (Weeks, days, etc)
|
||||
# TODO: indicate the type of the record before the answer? Not obvious.
|
||||
soa_html_template = """
|
||||
<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>
|
||||
"""
|
||||
ns_html_template = """
|
||||
<span><a class="hostname" tal:attributes="href path" tal:content="hostname"/></span>
|
||||
"""
|
||||
ptr_html_template = """
|
||||
<span><a class="hostname" tal:attributes="href path" tal:content="hostname"/></span>
|
||||
"""
|
||||
srv_html_template = """
|
||||
<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>
|
||||
"""
|
||||
txt_html_template = """
|
||||
<span tal:content="text"/>
|
||||
"""
|
||||
spf_html_template = """
|
||||
<span tal:content="text"/>
|
||||
"""
|
||||
ds_html_template = """
|
||||
<span>Key <span tal:replace="keytag"/> (hash type <span tal:replace="digesttype"/>)</span>
|
||||
"""
|
||||
dlv_html_template = """
|
||||
<span>Key <span tal:replace="keytag"/> (hash type <span tal:replace="digesttype"/>)</span>
|
||||
"""
|
||||
dnskey_html_template = """
|
||||
<span><span tal:condition="keytag">Key <span tal:replace="keytag"/>, </span>algorithm <span tal:replace="algorithm"/>, flags <span tal:replace="flags"/></span>
|
||||
"""
|
||||
sshfp_html_template = """
|
||||
<span>Algorithm <span tal:replace="algorithm"/>, Fingerprint type <span tal:replace="fptype"/>, fingerprint <span tal:replace="fingerprint"/></span>
|
||||
"""
|
||||
naptr_html_template = """
|
||||
<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>
|
||||
"""
|
||||
# TODO: link to Open Street Map
|
||||
loc_html_template = """
|
||||
<span><span tal:replace="longitude"/> / <span tal:replace="latitude"/> (altitude <span tal:replace="altitude"/>)</span>
|
||||
"""
|
||||
unknown_html_template = """
|
||||
<span>Unknown record type (<span tal:replace="rrtype"/>)</span>
|
||||
"""
|
||||
class HtmlFormatter(Formatter):
|
||||
|
||||
def link_of(self, host, querier, reverse=False):
|
||||
if querier.base_url == "":
|
||||
url = '/'
|
||||
else:
|
||||
url = querier.base_url
|
||||
base = url + str(host)
|
||||
if not reverse:
|
||||
base += '/ADDR'
|
||||
base += '?format=HTML'
|
||||
if reverse:
|
||||
base += '&reverse=1'
|
||||
return base
|
||||
|
||||
def pretty_duration(self, duration):
|
||||
""" duration is in seconds """
|
||||
weeks = duration/(86400*7)
|
||||
days = (duration-(86400*7*weeks))/86400
|
||||
hours = (duration-(86400*7*weeks)-(86400*days))/3600
|
||||
minutes = (duration-(86400*7*weeks)-(86400*days)-(3600*hours))/60
|
||||
seconds = duration-(86400*7*weeks)-(86400*days)-(3600*hours)-(60*minutes)
|
||||
result = ""
|
||||
empty_result = True
|
||||
if weeks != 0:
|
||||
if weeks > 1:
|
||||
plural = "s"
|
||||
else:
|
||||
plural = ""
|
||||
result += "%i week%s" % (weeks, plural)
|
||||
empty_result = False
|
||||
if days != 0:
|
||||
if not empty_result:
|
||||
result += ", "
|
||||
if days > 1:
|
||||
plural = "s"
|
||||
else:
|
||||
plural = ""
|
||||
result += "%i day%s" % (days, plural)
|
||||
empty_result = False
|
||||
if hours != 0:
|
||||
if not empty_result:
|
||||
result += ", "
|
||||
if hours > 1:
|
||||
plural = "s"
|
||||
else:
|
||||
plural = ""
|
||||
result += "%i hour%s" % (hours, plural)
|
||||
empty_result = False
|
||||
if minutes != 0:
|
||||
if not empty_result:
|
||||
result += ", "
|
||||
if minutes > 1:
|
||||
plural = "s"
|
||||
else:
|
||||
plural = ""
|
||||
result += "%i minute%s" % (minutes, plural)
|
||||
empty_result = False
|
||||
if not empty_result:
|
||||
result += ", "
|
||||
if seconds > 1:
|
||||
plural = "s"
|
||||
else:
|
||||
plural = ""
|
||||
result += "%i second%s" % (seconds, plural)
|
||||
return result
|
||||
|
||||
def format(self, answer, qtype, flags, querier):
|
||||
self.template = simpleTAL.compileXMLTemplate (html_template)
|
||||
self.address_template = simpleTAL.compileXMLTemplate (address_html_template)
|
||||
self.version_template = simpleTAL.compileXMLTemplate (version_html_template)
|
||||
self.mx_template = simpleTAL.compileXMLTemplate (mx_html_template)
|
||||
self.soa_template = simpleTAL.compileXMLTemplate (soa_html_template)
|
||||
self.ns_template = simpleTAL.compileXMLTemplate (ns_html_template)
|
||||
self.ptr_template = simpleTAL.compileXMLTemplate (ptr_html_template)
|
||||
self.srv_template = simpleTAL.compileXMLTemplate (srv_html_template)
|
||||
self.txt_template = simpleTAL.compileXMLTemplate (txt_html_template)
|
||||
self.spf_template = simpleTAL.compileXMLTemplate (spf_html_template)
|
||||
self.loc_template = simpleTAL.compileXMLTemplate (loc_html_template)
|
||||
self.ds_template = simpleTAL.compileXMLTemplate (ds_html_template)
|
||||
self.dlv_template = simpleTAL.compileXMLTemplate (dlv_html_template)
|
||||
self.dnskey_template = simpleTAL.compileXMLTemplate (dnskey_html_template)
|
||||
self.sshfp_template = simpleTAL.compileXMLTemplate (sshfp_html_template)
|
||||
self.naptr_template = simpleTAL.compileXMLTemplate (naptr_html_template)
|
||||
self.unknown_template = simpleTAL.compileXMLTemplate (unknown_html_template)
|
||||
self.context = simpleTALES.Context(allowPythonPath=False)
|
||||
self.context.addGlobal ("title", "Query for domain %s, type %s" % \
|
||||
(self.domain, qtype))
|
||||
self.context.addGlobal ("resolver", querier.resolver.nameservers[0])
|
||||
self.context.addGlobal ("email", querier.email_admin)
|
||||
self.context.addGlobal ("doc", querier.url_doc)
|
||||
self.context.addGlobal("contenttype",
|
||||
"text/html; charset=%s" % querier.encoding)
|
||||
self.context.addGlobal ("css", querier.url_css)
|
||||
self.context.addGlobal ("opensearch", querier.url_opensearch)
|
||||
self.context.addGlobal ("datetime", time.strftime("%Y-%m-%d %H:%M:%SZ",
|
||||
time.gmtime(time.time())))
|
||||
self.context.addGlobal("duration", str(querier.delay))
|
||||
if querier.description_html:
|
||||
self.context.addGlobal("description_html", querier.description_html)
|
||||
elif querier.description:
|
||||
self.context.addGlobal("description", querier.description)
|
||||
iresult = simpleTALUtils.FastStringOutput()
|
||||
icontext = simpleTALES.Context(allowPythonPath=False)
|
||||
icontext.addGlobal("pyversion", platform.python_implementation() + " " +
|
||||
platform.python_version() + " on " + platform.system())
|
||||
icontext.addGlobal("dnsversion", dnspythonversion.version)
|
||||
icontext.addGlobal("myversion", self.myversion)
|
||||
self.version_template.expand (icontext, iresult,
|
||||
suppressXMLDeclaration=True,
|
||||
outputEncoding=querier.encoding)
|
||||
self.context.addGlobal("versions", unicode(iresult.getvalue(), querier.encoding))
|
||||
str_flags = ""
|
||||
if flags & dns.flags.AD:
|
||||
str_flags += "/ Authentic Data "
|
||||
if flags & dns.flags.AA:
|
||||
str_flags += "/ Authoritative Answer "
|
||||
if flags & dns.flags.TC:
|
||||
str_flags += "/ Truncated Answer "
|
||||
if str_flags != "":
|
||||
self.context.addGlobal ("flags", str_flags)
|
||||
if answer is not None:
|
||||
self.rrsets = []
|
||||
if str(answer.owner_name).lower() != self.domain.lower():
|
||||
self.context.addGlobal ("distinctowner", True)
|
||||
self.context.addGlobal ("ownername", answer.owner_name)
|
||||
icontext = simpleTALES.Context(allowPythonPath=False)
|
||||
for rrset in answer.rrsets:
|
||||
records = []
|
||||
for rdata in rrset:
|
||||
iresult = simpleTALUtils.FastStringOutput()
|
||||
if rdata.rdtype == dns.rdatatype.A or rdata.rdtype == dns.rdatatype.AAAA:
|
||||
icontext.addGlobal ("address", rdata.address)
|
||||
icontext.addGlobal ("path", self.link_of(rdata.address,
|
||||
querier,
|
||||
reverse=True))
|
||||
self.address_template.expand (icontext, iresult,
|
||||
suppressXMLDeclaration=True,
|
||||
outputEncoding=querier.encoding)
|
||||
elif rdata.rdtype == dns.rdatatype.SOA:
|
||||
icontext.addGlobal ("master", rdata.mname)
|
||||
icontext.addGlobal ("path", self.link_of(rdata.mname, querier))
|
||||
icontext.addGlobal("admin", rdata.rname) # TODO: replace first point by @
|
||||
icontext.addGlobal("serial", rdata.serial)
|
||||
icontext.addGlobal("refresh", rdata.refresh)
|
||||
icontext.addGlobal("retry", rdata.retry)
|
||||
icontext.addGlobal("expire", rdata.expire)
|
||||
icontext.addGlobal("minimum", rdata.minimum)
|
||||
self.soa_template.expand (icontext, iresult,
|
||||
suppressXMLDeclaration=True,
|
||||
outputEncoding=querier.encoding)
|
||||
elif rdata.rdtype == dns.rdatatype.MX:
|
||||
icontext.addGlobal ("hostname", rdata.exchange)
|
||||
icontext.addGlobal ("path", self.link_of(rdata.exchange, querier))
|
||||
icontext.addGlobal("pref", rdata.preference)
|
||||
self.mx_template.expand (icontext, iresult,
|
||||
suppressXMLDeclaration=True,
|
||||
outputEncoding=querier.encoding)
|
||||
elif rdata.rdtype == dns.rdatatype.NS:
|
||||
icontext.addGlobal ("hostname", rdata.target)
|
||||
# TODO: translate back the Punycode name
|
||||
# servers to Unicode with
|
||||
# encodings.idna.ToUnicode?
|
||||
icontext.addGlobal ("path", self.link_of(rdata.target, querier))
|
||||
self.ns_template.expand (icontext, iresult,
|
||||
suppressXMLDeclaration=True,
|
||||
outputEncoding=querier.encoding)
|
||||
elif rdata.rdtype == dns.rdatatype.PTR:
|
||||
icontext.addGlobal ("hostname", rdata.target)
|
||||
icontext.addGlobal ("path", self.link_of(rdata.target, querier))
|
||||
self.ptr_template.expand (icontext, iresult,
|
||||
suppressXMLDeclaration=True,
|
||||
outputEncoding=querier.encoding)
|
||||
elif rdata.rdtype == dns.rdatatype.SRV:
|
||||
icontext.addGlobal ("hostname", rdata.target)
|
||||
icontext.addGlobal ("path", self.link_of(rdata.target, querier))
|
||||
icontext.addGlobal ("priority", rdata.priority)
|
||||
icontext.addGlobal ("weight", rdata.weight)
|
||||
icontext.addGlobal ("port", rdata.port)
|
||||
self.srv_template.expand (icontext, iresult,
|
||||
suppressXMLDeclaration=True,
|
||||
outputEncoding=querier.encoding)
|
||||
elif rdata.rdtype == dns.rdatatype.TXT:
|
||||
icontext.addGlobal ("text", "\n".join(rdata.strings))
|
||||
self.txt_template.expand (icontext, iresult,
|
||||
suppressXMLDeclaration=True,
|
||||
outputEncoding=querier.encoding)
|
||||
elif rdata.rdtype == dns.rdatatype.SPF:
|
||||
icontext.addGlobal ("text", "\n".join(rdata.strings))
|
||||
self.spf_template.expand (icontext, iresult,
|
||||
suppressXMLDeclaration=True,
|
||||
outputEncoding=querier.encoding)
|
||||
elif rdata.rdtype == dns.rdatatype.LOC:
|
||||
# TODO: expanded longitude and latitude instead of floats?
|
||||
icontext.addGlobal ("longitude", rdata.float_longitude)
|
||||
icontext.addGlobal ("latitude", rdata.float_latitude)
|
||||
icontext.addGlobal ("altitude", rdata.altitude)
|
||||
self.loc_template.expand (icontext, iresult,
|
||||
suppressXMLDeclaration=True,
|
||||
outputEncoding=querier.encoding)
|
||||
elif rdata.rdtype == dns.rdatatype.DS:
|
||||
icontext.addGlobal ("algorithm", rdata.algorithm)
|
||||
icontext.addGlobal ("digesttype", rdata.digest_type)
|
||||
icontext.addGlobal ("digest", rdata.digest)
|
||||
icontext.addGlobal ("keytag", rdata.key_tag)
|
||||
self.ds_template.expand (icontext, iresult,
|
||||
suppressXMLDeclaration=True,
|
||||
outputEncoding=querier.encoding)
|
||||
elif rdata.rdtype == dns.rdatatype.DLV:
|
||||
icontext.addGlobal ("algorithm", rdata.algorithm)
|
||||
icontext.addGlobal ("digesttype", rdata.digest_type)
|
||||
icontext.addGlobal ("digest", rdata.digest)
|
||||
icontext.addGlobal ("keytag", rdata.key_tag)
|
||||
self.dlv_template.expand (icontext, iresult,
|
||||
suppressXMLDeclaration=True,
|
||||
outputEncoding=querier.encoding)
|
||||
elif rdata.rdtype == dns.rdatatype.DNSKEY:
|
||||
icontext.addGlobal ("algorithm", rdata.algorithm)
|
||||
icontext.addGlobal ("protocol", rdata.protocol)
|
||||
icontext.addGlobal ("flags", rdata.flags)
|
||||
try:
|
||||
key_tag = dns.dnssec.key_id(rdata)
|
||||
icontext.addGlobal ("keytag", key_tag)
|
||||
except AttributeError:
|
||||
# key_id appeared only in dnspython 1.9. Not
|
||||
# always available on 2012-05-17
|
||||
pass
|
||||
self.dnskey_template.expand (icontext, iresult,
|
||||
suppressXMLDeclaration=True,
|
||||
outputEncoding=querier.encoding)
|
||||
elif rdata.rdtype == dns.rdatatype.SSHFP:
|
||||
icontext.addGlobal ("algorithm", rdata.algorithm)
|
||||
icontext.addGlobal ("fptype", rdata.fp_type)
|
||||
icontext.addGlobal ("fingerprint", to_hexstring(rdata.fingerprint))
|
||||
self.sshfp_template.expand (icontext, iresult,
|
||||
suppressXMLDeclaration=True,
|
||||
outputEncoding=querier.encoding)
|
||||
elif rdata.rdtype == dns.rdatatype.NAPTR:
|
||||
icontext.addGlobal ("flags", rdata.flags)
|
||||
icontext.addGlobal ("order", rdata.order)
|
||||
icontext.addGlobal ("preference", rdata.preference)
|
||||
icontext.addGlobal ("services", rdata.service)
|
||||
icontext.addGlobal ("regexp", unicode(rdata.regexp,
|
||||
"UTF-8")) # UTF-8 rdata is found in the wild
|
||||
icontext.addGlobal ("replacement", rdata.replacement)
|
||||
self.naptr_template.expand (icontext, iresult,
|
||||
suppressXMLDeclaration=True,
|
||||
outputEncoding=querier.encoding)
|
||||
else:
|
||||
icontext.addGlobal ("rrtype", rdata.rdtype)
|
||||
self.unknown_template.expand (icontext, iresult,
|
||||
suppressXMLDeclaration=True,
|
||||
outputEncoding=querier.encoding)
|
||||
records.append(unicode(iresult.getvalue(), querier.encoding))
|
||||
self.rrsets.append({'ttl': self.pretty_duration(rrset.ttl),
|
||||
'records': records})
|
||||
else:
|
||||
self.rrsets = None
|
||||
|
||||
def result(self, querier):
|
||||
result = simpleTALUtils.FastStringOutput()
|
||||
self.context.addGlobal("rrsets", self.rrsets)
|
||||
self.template.expand (self.context, result,
|
||||
outputEncoding=querier.encoding,
|
||||
docType='<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">')
|
||||
return (result.getvalue() + "\n")
|
@ -0,0 +1,29 @@
|
||||
default_bucket_size = 20
|
||||
|
||||
import time
|
||||
|
||||
class LeakyBucket():
|
||||
|
||||
def __init__(self, size=default_bucket_size):
|
||||
self.size = size
|
||||
self.content = 0
|
||||
self.last_check = time.time()
|
||||
|
||||
def update(self):
|
||||
duration = time.time() - self.last_check
|
||||
offset = duration
|
||||
if self.content > offset:
|
||||
self.content -= offset
|
||||
elif self.content == 0:
|
||||
pass
|
||||
else:
|
||||
self.content = 0
|
||||
self.last_check = time.time()
|
||||
|
||||
def add(self, amount=1):
|
||||
if not self.full():
|
||||
self.content += amount
|
||||
|
||||
def full(self):
|
||||
self.update()
|
||||
return self.content >= self.size
|
@ -0,0 +1,362 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Standard library
|
||||
from cgi import escape
|
||||
from urlparse import parse_qs
|
||||
import encodings.idna
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
# http://www.dnspython.org/
|
||||
import dns.resolver
|
||||
import dns.reversename
|
||||
|
||||
# http://code.google.com/p/netaddr/ https://github.com/drkjam/netaddr
|
||||
import netaddr
|
||||
|
||||
# Internal modules
|
||||
import Formatter
|
||||
from LeakyBucket import LeakyBucket
|
||||
import Answer
|
||||
|
||||
# If you need to change thse values, it is better to do it when
|
||||
# calling the Querier() constructor.
|
||||
default_base_url = ""
|
||||
default_edns_size = 4096
|
||||
# TODO: allow to use prefixes in the whitelist
|
||||
default_whitelist=[netaddr.IPAddress("127.0.0.1"), netaddr.IPAddress("::1")]
|
||||
default_encoding = "UTF-8"
|
||||
default_handle_wk_files = True
|
||||
default_bucket_size = 5
|
||||
|
||||
# Misc. util. routines
|
||||
def send_response(start_response, status, output, type):
|
||||
response_headers = [('Content-type', type),
|
||||
('Content-Length', str(len(output))),
|
||||
('Allow', 'GET')]
|
||||
start_response(status, response_headers)
|
||||
|
||||
def punycode_of(domain):
|
||||
labels = domain.split(".")
|
||||
result = u""
|
||||
for label in labels:
|
||||
if label:
|
||||
result += (encodings.idna.ToASCII(label) + ".")
|
||||
return (result)
|
||||
|
||||
class Querier:
|
||||
|
||||
def __init__(self, email_admin=None, url_doc=None, url_css=None, url_opensearch=None,
|
||||
file_favicon=None,
|
||||
encoding=default_encoding, base_url=default_base_url,
|
||||
bucket_size=default_bucket_size,
|
||||
whitelist=default_whitelist, edns_size=default_edns_size,
|
||||
handle_wk_files=default_handle_wk_files,
|
||||
google_code=None, description=None, description_html=None):
|
||||
self.resolver = dns.resolver.Resolver()
|
||||
self.default_nameservers = self.resolver.nameservers
|
||||
self.buckets = {}
|
||||
self.base_url = base_url
|
||||
self.whitelist = whitelist
|
||||
self.handle_wk_files = handle_wk_files
|
||||
self.email_admin = email_admin
|
||||
self.url_doc = url_doc
|
||||
self.url_css = url_css
|
||||
self.url_opensearch = url_opensearch
|
||||
if file_favicon:
|
||||
self.favicon = open(file_favicon).read()
|
||||
else:
|
||||
self.favicon = None
|
||||
self.encoding = encoding
|
||||
self.edns_size = edns_size
|
||||
self.bucket_size = default_bucket_size
|
||||
self.google_code = google_code
|
||||
self.description = description
|
||||
self.description_html = description_html
|
||||
self.reset_resolver()
|
||||
|
||||
def reset_resolver(self):
|
||||
self.resolver.nameservers = self.default_nameservers[0:1] # Yes, it
|
||||
# decreases resilience but it seems it is the only way to be
|
||||
# sure of *which* name server actually replied (TODO: question
|
||||
# sent on the dnspython mailing lst on 2012-05-20). POssible
|
||||
# improvment: use the low-level interface of DNS Python and
|
||||
# handles this ourselves.
|
||||
# Default is to use EDNS without the DO bit
|
||||
if self.edns_size is not None:
|
||||
self.resolver.use_edns(0, 0, self.edns_size)
|
||||
else:
|
||||
self.resolver.use_edns(-1, 0, 0)
|
||||
self.resolver.search = []
|
||||
|
||||
def default(self, start_response, path):
|
||||
output = """
|
||||
I'm the default handler, \"%s\" was called.
|
||||
Are you sure of the URL?\n""" % path
|
||||
send_response(start_response, '404 No such resource' , output, 'text/plain; charset=%s' % self.encoding)
|
||||
return [output]
|
||||
|
||||
def emptyfile(self, start_response):
|
||||
output = ""
|
||||
send_response(start_response, '200 OK' , output, 'text/plain')
|
||||
return [output]
|
||||
|
||||
def robotstxt(self, start_response):
|
||||
# http://www.robotstxt.org/
|
||||
# TODO: allow to read it in the configuration file
|
||||
output = """
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
"""
|
||||
send_response(start_response, '200 OK' , output, 'text/plain')
|
||||
return [output]
|
||||
|
||||
def notfound(self, start_response):
|
||||
output = "Not found\r\n"
|
||||
send_response(start_response, '404 Not Found' , output, 'text/plain')
|
||||
return [output]
|
||||
|
||||
def query(self, start_response, path, client, format="HTML", alt_resolver=None,
|
||||
do_dnssec=False, tcp=False, edns_size=default_edns_size,
|
||||
reverse=False):
|
||||
""" path must starts with a /, then the domain name then an
|
||||
(optional) / followed by the QTYPE """
|
||||
# TODO: document and implement the query class
|
||||
if not path.startswith('/'):
|
||||
raise Exception("Internal error: no / at the beginning of %s" % path)
|
||||
plaintype = 'text/plain; charset=%s' % self.encoding
|
||||
if format == "TEXT":
|
||||
mtype = 'text/plain; charset=%s' % self.encoding
|
||||
elif format == "HTML":
|
||||
mtype = 'text/html; charset=%s' % self.encoding
|
||||
elif format == "JSON":
|
||||
mtype = 'application/json'
|
||||
elif format == "ZONE":
|
||||
mtype = 'text/dns' # RFC 4027
|
||||
# TODO: application/dns, "detached" DNS (binary) as in RFC 2540?
|
||||
elif format == "XML":
|
||||
mtype = 'application/xml'
|
||||
else:
|
||||
output = "Unsupported format \"%s\"\n" % format
|
||||
send_response(start_response, '400 Bad request', output, plaintype)
|
||||
return [output]
|
||||
ip_client = netaddr.IPAddress(client)
|
||||
if ip_client.version == 4:
|
||||
ip_prefix = netaddr.IPNetwork(client + "/28")
|
||||
elif ip_client.version == 6:
|
||||
ip_prefix = netaddr.IPNetwork(client + "/64")
|
||||
else:
|
||||
output = "Unsupported address family \"%s\"\n" % ip_client.version
|
||||
send_response(start_response, '400 Unknown IP version', output, plaintype)
|
||||
return [output]
|
||||
if ip_client not in self.whitelist:
|
||||
if self.buckets.has_key(ip_prefix.cidr):
|
||||
if self.buckets[ip_prefix.cidr].full():
|
||||
status = '429 Too many requests'
|
||||
# 429 registered by RFC 6585 in april 2012
|
||||
# http://www.iana.org/assignments/http-status-codes
|
||||
# Already common
|
||||
# http://www.flickr.com/photos/girliemac/6509400997/in/set-72157628409467125
|
||||
output = "%s sent too many requests" % client # TODO: better message
|
||||
send_response(start_response, status, output, plaintype)
|
||||
return [output]
|
||||
else:
|
||||
self.buckets[ip_prefix.cidr].add(1)
|
||||
else:
|
||||
self.buckets[ip_prefix.cidr] = LeakyBucket(size=self.bucket_size)
|
||||
args = path[1:]
|
||||
slashpos = args.find('/')
|
||||
if slashpos == -1:
|
||||
if reverse:
|
||||
domain = str(dns.reversename.from_address(args))
|
||||
qtype = 'PTR'
|
||||
else:
|
||||
domain = args
|
||||
qtype = 'A'
|
||||
else:
|
||||
if reverse:
|
||||
domain = str(dns.reversename.from_address(args[:slashpos]))
|
||||
else:
|
||||
domain = args[:slashpos]
|
||||
requested_qtype = args[slashpos+1:].upper()
|
||||
# We do not test if the QTYPE exists. If it doesn't
|
||||
# dnspython will raise an exception. The formatter will
|
||||
# have to deal with the various records.
|
||||
if requested_qtype == "":
|
||||
if reverse:
|
||||
type = 'PTR'
|
||||
else:
|
||||
qtype = 'A'
|
||||
else:
|
||||
qtype = requested_qtype
|
||||
if reverse and qtype != 'PTR':
|
||||
output = "You cannot ask for a query type other than PTR with reverse queries\n"
|
||||
send_response(start_response, '400 Bad qtype with reverse',
|
||||
output, plaintype)
|
||||
return [output]
|
||||
# Pseudo-qtype ADDR is handled specially later
|
||||
# Alas, dnspython does not react properly to QTYPE=ANY :-(
|
||||
if qtype == "ANY":
|
||||
output = "dnspython does not support ANY queries, sorry\n"
|
||||
send_response(start_response, '400 Bad record type', output,
|
||||
plaintype)
|
||||
return [output]
|
||||
if not domain.endswith('.'):
|
||||
domain += '.'
|
||||
if domain == 'root.':
|
||||
domain = '.'
|
||||
domain = unicode(domain, self.encoding)
|
||||
punycode_domain = punycode_of(domain)
|
||||
if punycode_domain != domain:
|
||||
qdomain = punycode_domain.encode("US-ASCII")
|
||||
else:
|
||||
qdomain = domain.encode("US-ASCII")
|
||||
try:
|
||||
if format == "HTML":
|
||||
formatter = Formatter.HtmlFormatter(domain)
|
||||
elif format == "TEXT":
|
||||
formatter = Formatter.TextFormatter(domain)
|
||||
elif format == "JSON":
|
||||
formatter = Formatter.JsonFormatter(domain)
|
||||
elif format == "ZONE":
|
||||
formatter = Formatter.ZoneFormatter(domain)
|
||||
elif format == "XML":
|
||||
formatter = Formatter.XmlFormatter(domain)
|
||||
self.reset_resolver()
|
||||
if do_dnssec:
|
||||
self.resolver.use_edns(0, dns.flags.DO, edns_size)
|
||||
if alt_resolver:
|
||||
self.resolver.nameservers = [alt_resolver,]
|
||||
query_start = datetime.now()
|
||||
if qtype != "ADDR":
|
||||
answers = self.resolver.query(qdomain, qtype, tcp=tcp)
|
||||
answer = Answer.ExtendedAnswer(answers)
|
||||
else:
|
||||
try:
|
||||
answers = self.resolver.query(qdomain, "A", tcp=tcp)
|
||||
answer = Answer.ExtendedAnswer(answers)
|
||||
except dns.resolver.NoAnswer:
|
||||
answer = None
|
||||
try:
|
||||
answers = self.resolver.query(qdomain, "AAAA", tcp=tcp)
|
||||
if answer is not None:
|
||||
answer.rrsets.append(answers.rrset)
|
||||
else:
|
||||
answer = Answer.ExtendedAnswer(answers)
|
||||
except dns.resolver.NoAnswer:
|
||||
pass
|
||||
# TODO: what if flags are different with A and AAAA? (Should not happen)
|
||||
if answer is None:
|
||||
query_end = datetime.now()
|
||||
self.delay = query_end - query_start
|
||||
formatter.format(None, qtype, 0, self)
|
||||
output = formatter.result(self)
|
||||
send_response(start_response, '200 OK', output, mtype)
|
||||
return [output]
|
||||
query_end = datetime.now()
|
||||
self.delay = query_end - query_start
|
||||
formatter.format(answer, qtype, answers.response.flags, self)
|
||||
output = formatter.result(self)
|
||||
send_response(start_response, '200 OK', output, mtype)
|
||||
except dns.resolver.NXDOMAIN:
|
||||
output = "Domain %s does not exist\n" % domain |