2 $Id: Base.py,v 1.12.2.15 2011/03/19 22:15:01 customdesigned Exp $
4 This file is part of the pydns project.
5 Homepage: http://pydns.sourceforge.net
7 This code is covered by the standard Python License. See LICENSE for details.
9 Base functionality. Request and Response classes, that sort of thing.
12 import socket, string, types, time, select
13 import Type,Class,Opcode
16 # This random generator is used for transaction ids and port selection. This
17 # is important to prevent spurious results from lost packets, and malicious
18 # cache poisoning. This doesn't matter if you are behind a caching nameserver
19 # or your app is a primary DNS server only. To install your own generator,
20 # replace DNS.Base.random. SystemRandom uses /dev/urandom or similar source.
23 from random import SystemRandom
24 random = SystemRandom()
28 class DNSError(Exception): pass
30 # Lib uses DNSError, so import after defining.
33 defaults= { 'protocol':'udp', 'port':53, 'opcode':Opcode.QUERY,
34 'qtype':Type.A, 'rd':1, 'timing':1, 'timeout': 30,
39 def ParseResolvConf(resolv_path="/etc/resolv.conf"):
40 "parses the /etc/resolv.conf file and sets defaults for name servers"
42 lines=open(resolv_path).readlines()
44 line = string.strip(line)
45 if not line or line[0]==';' or line[0]=='#':
47 fields=string.split(line)
50 if fields[0]=='domain' and len(fields) > 1:
51 defaults['domain']=fields[1]
52 if fields[0]=='search':
54 if fields[0]=='options':
56 if fields[0]=='sortlist':
58 if fields[0]=='nameserver':
59 defaults['server'].append(fields[1])
61 def DiscoverNameServers():
63 if sys.platform in ('win32', 'nt'):
65 defaults['server']=win32dns.RegistryResolve()
67 return ParseResolvConf()
70 """ high level Request object """
71 def __init__(self,*name,**args):
75 self.argparse(name,args)
76 self.defaults = self.args
79 def argparse(self,name,args):
80 if not name and self.defaults.has_key('name'):
81 args['name'] = self.defaults['name']
82 if type(name) is types.StringType:
88 if defaults['server_rotate'] and \
89 type(defaults['server']) == types.ListType:
90 defaults['server'] = defaults['server'][1:]+defaults['server'][:1]
91 for i in defaults.keys():
92 if not args.has_key(i):
93 if self.defaults.has_key(i):
94 args[i]=self.defaults[i]
97 if type(args['server']) == types.StringType:
98 args['server'] = [args['server']]
101 def socketInit(self,a,b):
102 self.s = socket.socket(a,b)
104 def processUDPReply(self):
106 r,w,e = select.select([self.s],[],[],self.timeout)
108 raise DNSError, 'Timeout'
109 (self.reply, self.from_address) = self.s.recvfrom(65535)
110 self.time_finish=time.time()
111 self.args['server']=self.ns
112 return self.processReply()
114 def _readall(self,f,count):
116 while len(res) < count:
118 # should we restart timeout everytime we get a dribble of data?
119 rem = self.time_start + self.timeout - time.time()
120 if rem <= 0: raise DNSError,'Timeout'
121 self.s.settimeout(rem)
122 buf = f.read(count - len(res))
124 raise DNSError,'incomplete reply - %d of %d read' % (len(res),count)
128 def processTCPReply(self):
130 self.s.settimeout(self.timeout)
132 self.s.settimeout(None)
133 f = self.s.makefile('r')
134 header = self._readall(f,2)
135 count = Lib.unpack16bit(header)
136 self.reply = self._readall(f,count)
137 self.time_finish=time.time()
138 self.args['server']=self.ns
139 return self.processReply()
141 def processReply(self):
142 self.args['elapsed']=(self.time_finish-self.time_start)*1000
143 u = Lib.Munpacker(self.reply)
144 r=Lib.DnsResult(u,self.args)
146 #self.args=None # mark this DnsRequest object as used.
148 #### TODO TODO TODO ####
149 # if protocol == 'tcp' and qtype == Type.AXFR:
152 # if len(header) < 2:
153 # print '========== EOF =========='
155 # count = Lib.unpack16bit(header)
157 # print '========== ZERO COUNT =========='
159 # print '========== NEXT =========='
160 # reply = f.read(count)
161 # if len(reply) != count:
162 # print '*** Incomplete reply ***'
164 # u = Lib.Munpacker(reply)
168 "Pick random source port to avoid DNS cache poisoning attack."
171 source_port = random.randint(1024,65535)
172 self.s.bind(('', source_port))
174 except socket.error, msg:
175 # Error 98, 'Address already in use'
176 if msg[0] != 98: raise
180 self.s.connect((self.ns,self.port))
182 def req(self,*name,**args):
183 " needs a refactoring "
184 self.argparse(name,args)
186 # raise DNSError,'reinitialize request before reuse'
187 protocol = self.args['protocol']
188 self.port = self.args['port']
189 self.tid = random.randint(0,65535)
190 self.timeout = self.args['timeout'];
191 opcode = self.args['opcode']
193 server=self.args['server']
194 if type(self.args['qtype']) == types.StringType:
196 qtype = getattr(Type, string.upper(self.args['qtype']))
197 except AttributeError:
198 raise DNSError,'unknown query type'
200 qtype=self.args['qtype']
201 if not self.args.has_key('name'):
203 raise DNSError,'nothing to lookup'
204 qname = self.args['name']
205 if qtype == Type.AXFR:
206 print 'Query type AXFR, protocol forced to TCP'
208 #print 'QTYPE %d(%s)' % (qtype, Type.typestr(qtype))
210 # jesus. keywords and default args would be good. TODO.
211 m.addHeader(self.tid,
212 0, opcode, 0, 0, rd, 0, 0, 0,
214 m.addQuestion(qname, qtype, Class.IN)
215 self.request = m.getbuf()
217 if protocol == 'udp':
218 self.sendUDPRequest(server)
220 self.sendTCPRequest(server)
221 except socket.error, reason:
222 raise DNSError, reason
226 if not self.response:
227 raise DNSError,'no working nameservers found'
230 def sendUDPRequest(self, server):
233 for self.ns in server:
234 #print "trying udp",self.ns
236 if self.ns.count(':'):
237 if hasattr(socket,'has_ipv6') and socket.has_ipv6:
238 self.socketInit(socket.AF_INET6, socket.SOCK_DGRAM)
241 self.socketInit(socket.AF_INET, socket.SOCK_DGRAM)
243 # TODO. Handle timeouts &c correctly (RFC)
244 self.time_start=time.time()
247 self.s.send(self.request)
248 r=self.processUDPReply()
249 # Since we bind to the source port and connect to the
250 # destination port, we don't need to check that here,
251 # but do make sure it's actually a DNS request that the
252 # packet is in reply to.
253 while r.header['id'] != self.tid \
254 or self.from_address[1] != self.port:
255 r=self.processUDPReply()
257 # FIXME: check waiting async queries
265 def sendTCPRequest(self, server):
266 " do the work of sending a TCP request "
268 for self.ns in server:
269 #print "trying tcp",self.ns
271 if self.ns.count(':'):
272 if hasattr(socket,'has_ipv6') and socket.has_ipv6:
273 self.socketInit(socket.AF_INET6, socket.SOCK_STREAM)
276 self.socketInit(socket.AF_INET, socket.SOCK_STREAM)
278 # TODO. Handle timeouts &c correctly (RFC)
279 self.time_start=time.time()
281 buf = Lib.pack16bit(len(self.request))+self.request
282 # Keep server from making sendall hang
283 self.s.setblocking(0)
284 # FIXME: throws WOULDBLOCK if request too large to fit in
287 # SHUT_WR breaks blocking IO with google DNS (8.8.8.8)
288 #self.s.shutdown(socket.SHUT_WR)
289 r=self.processTCPReply()
290 if r.header['id'] == self.tid:
298 #class DnsAsyncRequest(DnsRequest):
299 class DnsAsyncRequest(DnsRequest,asyncore.dispatcher_with_send):
300 " an asynchronous request object. out of date, probably broken "
301 def __init__(self,*name,**args):
302 DnsRequest.__init__(self, *name, **args)
304 if args.has_key('done') and args['done']:
305 self.donefunc=args['done']
307 self.donefunc=self.showResult
308 #self.realinit(name,args) # XXX todo
312 self.connect((self.ns,self.port))
313 self.time_start=time.time()
314 if self.args.has_key('start') and self.args['start']:
315 asyncore.dispatcher.go(self)
316 def socketInit(self,a,b):
317 self.create_socket(a,b)
318 asyncore.dispatcher.__init__(self)
320 def handle_read(self):
321 if self.args['protocol'] == 'udp':
322 self.response=self.processUDPReply()
324 apply(self.donefunc,(self,))
325 def handle_connect(self):
326 self.send(self.request)
327 def handle_write(self):
329 def showResult(self,*s):
334 # Revision 1.12.2.15 2011/03/19 22:15:01 customdesigned
335 # Added rotation of name servers - SF Patch ID: 2795929
337 # Revision 1.12.2.14 2011/03/17 03:46:03 customdesigned
338 # Simple test for google DNS with tcp
340 # Revision 1.12.2.13 2011/03/17 03:08:03 customdesigned
341 # Use blocking IO with timeout for TCP replies.
343 # Revision 1.12.2.12 2011/03/16 17:50:00 customdesigned
344 # Fix non-blocking TCP replies. (untested)
346 # Revision 1.12.2.11 2010/01/02 16:31:23 customdesigned
347 # Handle large TCP replies (untested).
349 # Revision 1.12.2.10 2008/08/01 03:58:03 customdesigned
350 # Don't try to close socket when never opened.
352 # Revision 1.12.2.9 2008/08/01 03:48:31 customdesigned
353 # Fix more breakage from port randomization patch. Support Ipv6 queries.
355 # Revision 1.12.2.8 2008/07/31 18:22:59 customdesigned
356 # Wait until tcp response at least starts coming in.
358 # Revision 1.12.2.7 2008/07/28 01:27:00 customdesigned
359 # Check configured port.
361 # Revision 1.12.2.6 2008/07/28 00:17:10 customdesigned
362 # Randomize source ports.
364 # Revision 1.12.2.5 2008/07/24 20:10:55 customdesigned
365 # Randomize tid in requests, and check in response.
367 # Revision 1.12.2.4 2007/05/22 20:28:31 customdesigned
370 # Revision 1.12.2.3 2007/05/22 20:25:52 customdesigned
371 # Use socket.inetntoa,inetaton.
373 # Revision 1.12.2.2 2007/05/22 20:21:46 customdesigned
376 # Revision 1.12.2.1 2007/05/22 20:19:35 customdesigned
377 # Skip bogus but non-empty lines in resolv.conf
379 # Revision 1.12 2002/04/23 06:04:27 anthonybaxter
380 # attempt to refactor the DNSRequest.req method a little. after doing a bit
381 # of this, I've decided to bite the bullet and just rewrite the puppy. will
382 # be checkin in some design notes, then unit tests and then writing the sod.
384 # Revision 1.11 2002/03/19 13:05:02 anthonybaxter
385 # converted to class based exceptions (there goes the python1.4 compatibility :)
387 # removed a quite gross use of 'eval()'.
389 # Revision 1.10 2002/03/19 12:41:33 anthonybaxter
390 # tabnannied and reindented everything. 4 space indent, no tabs.
393 # Revision 1.9 2002/03/19 12:26:13 anthonybaxter
394 # death to leading tabs.
396 # Revision 1.8 2002/03/19 10:30:33 anthonybaxter
397 # first round of major bits and pieces. The major stuff here (summarised
398 # from my local, off-net CVS server :/ this will cause some oddities with
401 # tests/testPackers.py:
402 # a large slab of unit tests for the packer and unpacker code in DNS.Lib
405 # placeholder for addSRV.
406 # added 'klass' to addA, make it the same as the other A* records.
407 # made addTXT check for being passed a string, turn it into a length 1 list.
408 # explicitly check for adding a string of length > 255 (prohibited).
409 # a bunch of cleanups from a first pass with pychecker
410 # new code for pack/unpack. the bitwise stuff uses struct, for a smallish
411 # (disappointly small, actually) improvement, while addr2bin is much
415 # added DiscoverNameServers. This automatically does the right thing
416 # on unix/ win32. No idea how MacOS handles this. *sigh*
417 # Incompatible change: Don't use ParseResolvConf on non-unix, use this
419 # a bunch of cleanups from a first pass with pychecker
421 # Revision 1.5 2001/08/09 09:22:28 anthonybaxter
422 # added what I hope is win32 resolver lookup support. I'll need to try
423 # and figure out how to get the CVS checkout onto my windows machine to
424 # make sure it works (wow, doing something other than games on the
427 # Code from Wolfgang.Strobl@gmd.de
429 # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66260
431 # Really, ParseResolvConf() should be renamed "FindNameServers" or
434 # Revision 1.4 2001/08/09 09:08:55 anthonybaxter
435 # added identifying header to top of each file
437 # Revision 1.3 2001/07/19 07:20:12 anthony
438 # Handle blank resolv.conf lines.
439 # Patch from Bastian Kleineidam
441 # Revision 1.2 2001/07/19 06:57:07 anthony