From 12841ffebe65efe686d63d8b073ebda6abbefc4b Mon Sep 17 00:00:00 2001 From: calvin Date: Tue, 20 Jul 2004 19:36:03 +0000 Subject: [PATCH] added git-svn-id: https://linkchecker.svn.sourceforge.net/svnroot/linkchecker/trunk/linkchecker@1377 e7d03fd6-7b0d-0410-9947-9c21f3af8025 --- bk/net/__init__.py | 100 +++++++ bk/net/dns/Class.py | 36 +++ bk/net/dns/Lib.py | 579 +++++++++++++++++++++++++++++++++++++++ bk/net/dns/Opcode.py | 31 +++ bk/net/dns/Status.py | 42 +++ bk/net/dns/Type.py | 54 ++++ bk/net/dns/__init__.py | 13 + bk/net/ip.py | 237 ++++++++++++++++ bk/net/nt.py | 87 ++++++ bk/net/posix.py | 132 +++++++++ bk/net/tests/__init__.py | 1 + bk/net/tests/test_dns.py | 52 ++++ bk/net/tests/test_ip.py | 92 +++++++ 13 files changed, 1456 insertions(+) create mode 100644 bk/net/__init__.py create mode 100644 bk/net/dns/Class.py create mode 100644 bk/net/dns/Lib.py create mode 100644 bk/net/dns/Opcode.py create mode 100644 bk/net/dns/Status.py create mode 100644 bk/net/dns/Type.py create mode 100644 bk/net/dns/__init__.py create mode 100644 bk/net/ip.py create mode 100644 bk/net/nt.py create mode 100644 bk/net/posix.py create mode 100644 bk/net/tests/__init__.py create mode 100644 bk/net/tests/test_dns.py create mode 100644 bk/net/tests/test_ip.py diff --git a/bk/net/__init__.py b/bk/net/__init__.py new file mode 100644 index 00000000..b9dfaab4 --- /dev/null +++ b/bk/net/__init__.py @@ -0,0 +1,100 @@ +# -*- coding: iso-8859-1 -*- +"""network utilities""" +# Copyright (C) 2004 Bastian Kleineidam +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + + +import os +import sets +import socket +import bk.log +import bk.containers +if os.name=='posix': + import posix as platform_net +elif os.name=='nt': + import nt as platform_net +else: + platform_net = None + +LOG_NET = "bk.net" + + +def get_localhosts (): + """get list of localhost names and ips""" + # XXX is this default list of localhost stuff complete? + localhosts = sets.Set([ + 'localhost', + 'loopback', + '127.0.0.1', + '::1', + 'ip6-localhost', + 'ip6-loopback', + ]) + add_addrinfo(localhosts, socket.gethostname()) + # add system specific hosts for all interfaces + if platform_net is not None: + for addr in platform_net.get_localaddrs(): + add_addrinfo(localhosts, addr) + else: + bk.log.warn(LOG_NET, "platform %r network not supported", os.name) + return localhosts + + +def add_addrinfo (hosts, host): + try: + addrinfo = socket.gethostbyaddr(host) + except socket.error: + hosts.add(host.lower()) + return + hosts.add(addrinfo[0].lower()) + for h in addrinfo[1]: + hosts.add(h.lower()) + for h in addrinfo[2]: + hosts.add(h.lower()) + + +class DnsConfig (object): + """DNS configuration storage""" + def __init__ (self): + self.nameservers = bk.containers.SetList() + self.search_domains = bk.containers.SetList() + self.search_patterns = ('www.%s.com', 'www.%s.net', 'www.%s.org') + + def __str__ (self): + return "nameservers: "+str(self.nameservers)+\ + "\nsearch domains: "+str(self.search_domains)+\ + "\nsearch_patterns: "+str(self.search_patterns) + + +def resolver_config (): + """dns resolver configuration""" + config = DnsConfig() + if platform_net is not None: + platform_net.resolver_config(config) + else: + bk.log.warn(LOG_NET, "platform %r network not supported", os.name) + if not config.search_domains: + config.search_domains.append('') + if not config.nameservers: + config.nameservers.append('127.0.0.1') + bk.log.debug(LOG_NET, "nameservers %s", config.nameservers) + bk.log.debug(LOG_NET, "search domains %s", config.search_domains) + return config + + +if __name__=='__main__': + print "localhosts:", get_localhosts() + print resolver_config() diff --git a/bk/net/dns/Class.py b/bk/net/dns/Class.py new file mode 100644 index 00000000..c8daa21d --- /dev/null +++ b/bk/net/dns/Class.py @@ -0,0 +1,36 @@ +# -*- coding: iso-8859-1 -*- +""" +$Id$ + +This file is part of the pydns project. +Homepage: http://pydns.sourceforge.net + +This code is covered by the standard Python License. + +CLASS values (section 3.2.4) +""" + + +IN = 1 # the Internet +CS = 2 # the CSNET class (Obsolete - used only for examples in + # some obsolete RFCs) +CH = 3 # the CHAOS class. When someone shows me python running on + # a Symbolics Lisp machine, I'll look at implementing this. +HS = 4 # Hesiod [Dyer 87] + +# QCLASS values (section 3.2.5) + +ANY = 255 # any class + + +# Construct reverse mapping dictionary + +_classmap = {} +for _name in dir(): + if not _name.startswith('_'): + _classmap[eval(_name)] = _name + +def classstr (klass): + """return string representation of DNS class""" + return _classmap.get(klass, repr(klass)) + diff --git a/bk/net/dns/Lib.py b/bk/net/dns/Lib.py new file mode 100644 index 00000000..ae8353a7 --- /dev/null +++ b/bk/net/dns/Lib.py @@ -0,0 +1,579 @@ +# -*- coding: iso-8859-1 -*- +""" + $Id$ + + This file is part of the pydns project. + Homepage: http://pydns.sourceforge.net + + This code is covered by the standard Python License. + + Library code. Largely this is packers and unpackers for various types. +""" + +# +# See RFC 1035: +# ------------------------------------------------------------------------ +# Network Working Group P. Mockapetris +# Request for Comments: 1035 ISI +# November 1987 +# Obsoletes: RFCs 882, 883, 973 +# +# DOMAIN NAMES - IMPLEMENTATION AND SPECIFICATION +# ------------------------------------------------------------------------ + + +import types +import Type +import Class +import Opcode +import Status + + +class DNSError (Exception): + """basic DNS error class""" + pass + + +class UnpackError (DNSError): + """DNS packet unpacking error""" + pass + + +class PackError (DNSError): + """DNS packet packing error""" + pass + + +# Low-level 16 and 32 bit integer packing and unpacking + +from struct import pack as struct_pack +from struct import unpack as struct_unpack + + +def pack16bit (n): + return struct_pack('!H', n) + + +def pack32bit (n): + return struct_pack('!L', n) + + +def pack128bit (n): + return struct_pack('!LLLL', n) + + +def unpack16bit (s): + return struct_unpack('!H', s)[0] + + +def unpack32bit (s): + return struct_unpack('!L', s)[0] + + +def unpack128bit (s): + return struct_unpack('!LLLL', s)[0] + + +def addr2bin (addr): + if type(addr) == type(0): + return addr + bytes = addr.split('.') + if len(bytes) != 4: raise ValueError, 'bad IP address' + n = 0L + for byte in bytes: + n = n<<8 | int(byte) + return n + + +def bin2addr (n): + return '%d.%d.%d.%d' % ((n>>24)&0xFF, (n>>16)&0xFF, + (n>>8)&0xFF, n&0xFF) + + +# Packing class + +class Packer (object): + " packer base class. supports basic byte/16bit/32bit/addr/string/name " + + def __init__ (self): + self.buf = '' + self.index = {} + + + def getbuf (self): + return self.buf + + + def addbyte (self, c): + if len(c) != 1: raise TypeError, 'one character expected' + self.buf += c + + + def addbytes (self, bytes): + self.buf += bytes + + + def add16bit (self, n): + self.buf += pack16bit(n) + + + def add32bit (self, n): + self.buf += pack32bit(n) + + + def addaddr (self, addr): + n = addr2bin(addr) + self.buf += pack32bit(n) + + + def addaddr6 (self, addr): + n = addr2bin(addr) + self.buf += pack128bit(n) + + + def addstring (self, s): + if len(s) > 255: + raise ValueError, "Can't encode string of length "+ \ + "%s (> 255)"%(len(s)) + self.addbyte(chr(len(s))) + self.addbytes(s) + + + def addname (self, name): + """Domain name packing (section 4.1.4) + Add a domain name to the buffer, possibly using pointers. + The case of the first occurrence of a name is preserved. + Redundant dots are ignored. + """ + lst = [] + for label in name.split('.'): + if label: + if len(label) > 63: + raise PackError, 'label too long' + lst.append(label) + keys = [] + for i in range(len(lst)): + key = '.'.join(lst[i:]).upper() + keys.append(key) + if self.index.has_key(key): + pointer = self.index[key] + break + else: + i = len(lst) + pointer = None + # Do it into temporaries first so exceptions don't + # mess up self.index and self.buf + buf = '' + offset = len(self.buf) + index = [] + for j in range(i): + label = lst[j] + n = len(label) + if offset + len(buf) < 0x3FFF: + index.append((keys[j], offset + len(buf))) + else: + print 'DNS.Lib.Packer.addname:', + print 'warning: pointer too big' + buf += (chr(n) + label) + if pointer: + buf += pack16bit(pointer | 0xC000) + else: + buf += '\0' + self.buf += buf + for key, value in index: + self.index[key] = value + + + def dump (self): + """print string dump of packer data""" + keys = self.index.keys() + keys.sort() + print '-'*40 + for key in keys: + print '%20s %3d' % (key, self.index[key]) + print '-'*40 + space = 1 + for i in range(0, len(self.buf)+1, 2): + if self.buf[i:i+2] == '**': + if not space: print + space = 1 + continue + space = 0 + print '%4d' % i, + for c in self.buf[i:i+2]: + if ' ' < c < '\177': + print ' %c' % c, + else: + print '%2d' % ord(c), + print + print '-'*40 + + +# Unpacking class + + +class Unpacker (object): + + def __init__ (self, buf): + self.buf = buf + self.offset = 0 + + + def getbyte (self): + if self.offset > len(self.buf): + raise UnpackError, "Ran off end of data" + c = self.buf[self.offset] + self.offset += 1 + return c + + + def getbytes (self, n): + s = self.buf[self.offset : self.offset + n] + if len(s) != n: raise UnpackError, 'not enough data left' + self.offset +=n + return s + + + def get16bit (self): + return unpack16bit(self.getbytes(2)) + + + def get32bit (self): + return unpack32bit(self.getbytes(4)) + + + def get128bit (self): + return unpack128bit(self.getbytes(16)) + + + def getaddr (self): + return bin2addr(self.get32bit()) + + + def getaddr6 (self): + return bin2addr(self.get128bit()) + + + def getstring (self): + return self.getbytes(ord(self.getbyte())) + + + def getname (self): + # Domain name unpacking (section 4.1.4) + c = self.getbyte() + i = ord(c) + if i & 0xC0 == 0xC0: + d = self.getbyte() + j = ord(d) + pointer = ((i<<8) | j) & ~0xC000 + save_offset = self.offset + try: + self.offset = pointer + domain = self.getname() + finally: + self.offset = save_offset + return domain + if i == 0: + return '' + domain = self.getbytes(i) + remains = self.getname() + if not remains: + return domain + else: + return domain + '.' + remains + + +# Pack/unpack RR toplevel format (section 3.2.1) + +class RRpacker (Packer): + + def __init__ (self): + Packer.__init__(self) + self.rdstart = None + + + def addRRheader (self, name, type, klass, ttl, *rest): + self.addname(name) + self.add16bit(type) + self.add16bit(klass) + self.add32bit(ttl) + if rest: + if rest[1:]: raise TypeError, 'too many args' + rdlength = rest[0] + else: + rdlength = 0 + self.add16bit(rdlength) + self.rdstart = len(self.buf) + + + def patchrdlength (self): + rdlength = unpack16bit(self.buf[self.rdstart-2:self.rdstart]) + if rdlength == len(self.buf) - self.rdstart: + return + rdata = self.buf[self.rdstart:] + save_buf = self.buf + ok = 0 + try: + self.buf = self.buf[:self.rdstart-2] + self.add16bit(len(rdata)) + self.buf = self.buf + rdata + ok = 1 + finally: + if not ok: self.buf = save_buf + + + def endRR (self): + if self.rdstart is not None: + self.patchrdlength() + self.rdstart = None + + + def getbuf (self): + if self.rdstart is not None: self.patchrdlength() + return Packer.getbuf(self) + # Standard RRs (section 3.3) + + + def addCNAME (self, name, klass, ttl, cname): + self.addRRheader(name, Type.CNAME, klass, ttl) + self.addname(cname) + self.endRR() + + + def addHINFO (self, name, klass, ttl, cpu, os): + self.addRRheader(name, Type.HINFO, klass, ttl) + self.addstring(cpu) + self.addstring(os) + self.endRR() + + + def addMX (self, name, klass, ttl, preference, exchange): + self.addRRheader(name, Type.MX, klass, ttl) + self.add16bit(preference) + self.addname(exchange) + self.endRR() + + + def addNS (self, name, klass, ttl, nsdname): + self.addRRheader(name, Type.NS, klass, ttl) + self.addname(nsdname) + self.endRR() + + + def addPTR (self, name, klass, ttl, ptrdname): + self.addRRheader(name, Type.PTR, klass, ttl) + self.addname(ptrdname) + self.endRR() + + + def addSOA (self, name, klass, ttl, + mname, rname, serial, refresh, retry, expire, minimum): + self.addRRheader(name, Type.SOA, klass, ttl) + self.addname(mname) + self.addname(rname) + self.add32bit(serial) + self.add32bit(refresh) + self.add32bit(retry) + self.add32bit(expire) + self.add32bit(minimum) + self.endRR() + + + def addTXT (self, name, klass, ttl, lst): + self.addRRheader(name, Type.TXT, klass, ttl) + if type(lst) is types.StringType: + lst = [lst] + for txtdata in lst: + self.addstring(txtdata) + self.endRR() + # Internet specific RRs (section 3.4) -- class = IN + + + def addA (self, name, klass, ttl, address): + self.addRRheader(name, Type.A, klass, ttl) + self.addaddr(address) + self.endRR() + + + def addAAAA (self, name, klass, ttl, address): + self.addRRheader(name, Type.A, klass, ttl) + self.addaddr6(address) + self.endRR() + + + def addWKS (self, name, ttl, address, protocol, bitmap): + self.addRRheader(name, Type.WKS, Class.IN, ttl) + self.addaddr(address) + self.addbyte(chr(protocol)) + self.addbytes(bitmap) + self.endRR() + + + def addSRV (self): + raise NotImplementedError + + +def prettyTime (seconds): + + if seconds<60: + return seconds,"%d seconds"%(seconds) + if seconds<3600: + return seconds,"%d minutes"%(seconds/60) + if seconds<86400: + return seconds,"%d hours"%(seconds/3600) + if seconds<604800: + return seconds,"%d days"%(seconds/86400) + else: + return seconds,"%d weeks"%(seconds/604800) + + +class RRunpacker (Unpacker): + + def __init__ (self, buf): + Unpacker.__init__(self, buf) + self.rdend = None + + + def getRRheader (self): + name = self.getname() + rrtype = self.get16bit() + klass = self.get16bit() + ttl = self.get32bit() + rdlength = self.get16bit() + self.rdend = self.offset + rdlength + return (name, rrtype, klass, ttl, rdlength) + + + def endRR (self): + if self.offset != self.rdend: + raise UnpackError, 'end of RR not reached' + + + def getCNAMEdata (self): + return self.getname() + + + def getHINFOdata (self): + return self.getstring(), self.getstring() + + + def getMXdata (self): + return self.get16bit(), self.getname() + + + def getNSdata (self): + return self.getname() + + + def getPTRdata (self): + return self.getname() + + + def getSOAdata (self): + return self.getname(), \ + self.getname(), \ + ('serial',)+(self.get32bit(),), \ + ('refresh ',)+prettyTime(self.get32bit()), \ + ('retry',)+prettyTime(self.get32bit()), \ + ('expire',)+prettyTime(self.get32bit()), \ + ('minimum',)+prettyTime(self.get32bit()) + + + def getTXTdata (self): + lst = [] + while self.offset != self.rdend: + lst.append(self.getstring()) + return lst + + + def getAdata (self): + return self.getaddr() + + + def getAAAAdata (self): + return self.getaddr6() + + + def getWKSdata (self): + address = self.getaddr() + protocol = ord(self.getbyte()) + bitmap = self.getbytes(self.rdend - self.offset) + return address, protocol, bitmap + + + def getSRVdata (self): + """ + _Service._Proto.Name TTL Class SRV Priority Weight Port Target + """ + priority = self.get16bit() + weight = self.get16bit() + port = self.get16bit() + target = self.getname() + #print '***priority, weight, port, target', priority, weight, port, target + return priority, weight, port, target + + +# Pack/unpack Message Header (section 4.1) + +class Hpacker (Packer): + + def addHeader (self, rid, qr, opcode, aa, tc, rd, ra, z, rcode, + qdcount, ancount, nscount, arcount): + self.add16bit(rid) + self.add16bit((qr&1)<<15 | (opcode&0xF)<<11 | (aa&1)<<10 + | (tc&1)<<9 | (rd&1)<<8 | (ra&1)<<7 + | (z&7)<<4 | (rcode&0xF)) + self.add16bit(qdcount) + self.add16bit(ancount) + self.add16bit(nscount) + self.add16bit(arcount) + + +class Hunpacker (Unpacker): + + def getHeader (self): + rid = self.get16bit() + flags = self.get16bit() + qr, opcode, aa, tc, rd, ra, z, rcode = ( + (flags>>15)&1, + (flags>>11)&0xF, + (flags>>10)&1, + (flags>>9)&1, + (flags>>8)&1, + (flags>>7)&1, + (flags>>4)&7, + (flags>>0)&0xF) + qdcount = self.get16bit() + ancount = self.get16bit() + nscount = self.get16bit() + arcount = self.get16bit() + return (rid, qr, opcode, aa, tc, rd, ra, z, rcode, + qdcount, ancount, nscount, arcount) + + +# Pack/unpack Question (section 4.1.2) + +class Qpacker (Packer): + + def addQuestion (self, qname, qtype, qclass): + self.addname(qname) + self.add16bit(qtype) + self.add16bit(qclass) + + +class Qunpacker (Unpacker): + + def getQuestion (self): + return self.getname(), self.get16bit(), self.get16bit() + + +# Pack/unpack Message(section 4) +# NB the order of the base classes is important for __init__()! + +class Mpacker (RRpacker, Qpacker, Hpacker): + pass + + +class Munpacker (RRunpacker, Qunpacker, Hunpacker): + pass + diff --git a/bk/net/dns/Opcode.py b/bk/net/dns/Opcode.py new file mode 100644 index 00000000..f3917ce7 --- /dev/null +++ b/bk/net/dns/Opcode.py @@ -0,0 +1,31 @@ +# -*- coding: iso-8859-1 -*- +""" + $Id$ + + This file is part of the pydns project. + Homepage: http://pydns.sourceforge.net + + This code is covered by the standard Python License. + + Opcode values in message header. RFC 1035, 1996, 2136. +""" + + + +QUERY = 0 +IQUERY = 1 +STATUS = 2 +NOTIFY = 4 +UPDATE = 5 + +# Construct reverse mapping dictionary + +_opcodemap = {} +for _name in dir(): + if not _name.startswith('_'): + _opcodemap[eval(_name)] = _name + +def opcodestr (opcode): + """return string representation of DNS opcode""" + return _opcodemap.get(opcode, repr(opcode)) + diff --git a/bk/net/dns/Status.py b/bk/net/dns/Status.py new file mode 100644 index 00000000..bc44f79e --- /dev/null +++ b/bk/net/dns/Status.py @@ -0,0 +1,42 @@ +# -*- coding: iso-8859-1 -*- +""" + $Id$ + + This file is part of the pydns project. + Homepage: http://pydns.sourceforge.net + + This code is covered by the standard Python License. + + Status values in message header +""" + +NOERROR = 0 # No Error [RFC 1035] +FORMERR = 1 # Format Error [RFC 1035] +SERVFAIL = 2 # Server Failure [RFC 1035] +NXDOMAIN = 3 # Non-Existent Domain [RFC 1035] +NOTIMP = 4 # Not Implemented [RFC 1035] +REFUSED = 5 # Query Refused [RFC 1035] +YXDOMAIN = 6 # Name Exists when it should not [RFC 2136] +YXRRSET = 7 # RR Set Exists when it should not [RFC 2136] +NXRRSET = 8 # RR Set that should exist does not [RFC 2136] +NOTAUTH = 9 # Server Not Authoritative for zone [RFC 2136] +NOTZONE = 10 # Name not contained in zone [RFC 2136] +BADVERS = 16 # Bad OPT Version [RFC 2671] +BADSIG = 16 # TSIG Signature Failure [RFC 2845] +BADKEY = 17 # Key not recognized [RFC 2845] +BADTIME = 18 # Signature out of time window [RFC 2845] +BADMODE = 19 # Bad TKEY Mode [RFC 2930] +BADNAME = 20 # Duplicate key name [RFC 2930] +BADALG = 21 # Algorithm not supported [RFC 2930] + +# Construct reverse mapping dictionary + +_statusmap = {} +for _name in dir(): + if not _name.startswith('_'): + _statusmap[eval(_name)] = _name + +def statusstr (status): + """return string representation of DNS status""" + return _statusmap.get(status, repr(status)) + diff --git a/bk/net/dns/Type.py b/bk/net/dns/Type.py new file mode 100644 index 00000000..fe3d2610 --- /dev/null +++ b/bk/net/dns/Type.py @@ -0,0 +1,54 @@ +# -*- coding: iso-8859-1 -*- +""" + $Id$ + + This file is part of the pydns project. + Homepage: http://pydns.sourceforge.net + + This code is covered by the standard Python License. + + TYPE values (section 3.2.2) +""" + +A = 1 # a host address +NS = 2 # an authoritative name server +MD = 3 # a mail destination (Obsolete - use MX) +MF = 4 # a mail forwarder (Obsolete - use MX) +CNAME = 5 # the canonical name for an alias +SOA = 6 # marks the start of a zone of authority +MB = 7 # a mailbox domain name (EXPERIMENTAL) +MG = 8 # a mail group member (EXPERIMENTAL) +MR = 9 # a mail rename domain name (EXPERIMENTAL) +NULL = 10 # a null RR (EXPERIMENTAL) +WKS = 11 # a well known service description +PTR = 12 # a domain name pointer +HINFO = 13 # host information +MINFO = 14 # mailbox or mail list information +MX = 15 # mail exchange +TXT = 16 # text strings +AAAA = 28 # IPv6 AAAA records (RFC 1886) +SRV = 33 # DNS RR for specifying the location of services (RFC 2782) + + +# Additional TYPE values from host.c source + +UNAME = 110 +MP = 240 + +# QTYPE values (section 3.2.3) + +AXFR = 252 # A request for a transfer of an entire zone +MAILB = 253 # A request for mailbox-related records (MB, MG or MR) +MAILA = 254 # A request for mail agent RRs (Obsolete - see MX) +ANY = 255 # A request for all records + +# Construct reverse mapping dictionary + +_typemap = {} +for _name in dir(): + if not _name.startswith('_'): + _typemap[eval(_name)] = _name + +def typestr (dnstype): + """return string representation of DNS type""" + return _typemap.get(dnstype, repr(dnstype)) diff --git a/bk/net/dns/__init__.py b/bk/net/dns/__init__.py new file mode 100644 index 00000000..7bf1ccb8 --- /dev/null +++ b/bk/net/dns/__init__.py @@ -0,0 +1,13 @@ +# -*- coding: iso-8859-1 -*- +"""Python DNS module""" + +# $Id$ +# +# This file is part of the pydns project. +# Homepage: http://pydns.sourceforge.net +# +# This code is covered by the standard Python License. +# + +# __init__.py for DNS class. + diff --git a/bk/net/ip.py b/bk/net/ip.py new file mode 100644 index 00000000..60355167 --- /dev/null +++ b/bk/net/ip.py @@ -0,0 +1,237 @@ +# -*- coding: iso-8859-1 -*- +""" ip related utility functions """ +# Copyright (C) 2003-2004 Bastian Kleineidam +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +import re +import socket +import struct +import math +import sets +import bk.log +import wc + + +# IP Adress regular expressions +_ipv4_num = r"\d{1,3}" +_ipv4_num_4 = r"%s\.%s\.%s\.%s" % ((_ipv4_num,)*4) +_ipv4_re = re.compile(r"^%s$" % _ipv4_num_4) +# see rfc2373 +_ipv6_num = r"[\da-f]{1,4}" +_ipv6_re = re.compile(r"^%s:%s:%s:%s:%s:%s:%s:%s$" % ((_ipv6_num,)*8)) +_ipv6_ipv4_re = re.compile(r"^%s:%s:%s:%s:%s:%s:" % ((_ipv6_num,)*6) + \ + r"%s$" % _ipv4_num_4) +_ipv6_abbr_re = re.compile(r"^((%s:){0,6}%s)?::((%s:){0,6}%s)?$" % \ + ((_ipv6_num,)*4)) +_ipv6_ipv4_abbr_re = re.compile(r"^((%s:){0,4}%s)?::((%s:){0,5})?" % \ + ((_ipv6_num,)*3) + \ + "%s$" % _ipv4_num_4) +# netmask regex +_host_netmask_re = re.compile(r"^%s/%s$" % (_ipv4_num_4, _ipv4_num_4)) +_host_bitmask_re = re.compile(r"^%s/\d{1,2}$" % _ipv4_num_4) + + +def expand_ipv6 (ip, num): + """expand an IPv6 address with included :: to num octets + raise ValueError on invalid IP addresses + """ + i = ip.find("::") + prefix = ip[:i] + suffix = ip[i+2:] + count = prefix.count(":") + suffix.count(":") + if prefix: + count += 1 + prefix = prefix+":" + if suffix: + count += 1 + suffix = ":"+suffix + if count>=num: raise ValueError("invalid ipv6 number: %s"%ip) + fill = (num-count-1)*"0:" + "0" + return prefix+fill+suffix + + +def expand_ip (ip): + """ipv6 addresses are expanded to full 8 octets, all other + addresses are left untouched + return a tuple (ip, num) where num==1 if ip is a numeric ip, 0 + otherwise. + """ + if _ipv4_re.match(ip) or \ + _ipv6_re.match(ip) or \ + _ipv6_ipv4_re.match(ip): + return (ip, 1) + if _ipv6_abbr_re.match(ip): + return (expand_ipv6(ip, 8), 1) + if _ipv6_ipv4_abbr_re.match(ip): + i = ip.rfind(":") + 1 + return (expand_ipv6(ip[:i], 6) + ip[i:], 1) + return (ip, 0) + + +def is_valid_ip (ip): + """Return True if given ip is a valid IPv4 or IPv6 address""" + return is_valid_ipv4(ip) or is_valid_ipv6(ip) + + +def is_valid_ipv4 (ip): + """Return True if given ip is a valid IPv4 address""" + if not _ipv4_re.match(ip): + return False + a,b,c,d = [int(i) for i in ip.split(".")] + return a<=255 and b<=255 and c<=255 and d<=255 + + +def is_valid_ipv6 (ip): + """Return True if given ip is a valid IPv6 address""" + # XXX this is not complete: check ipv6 and ipv4 semantics too here + if not (_ipv6_re.match(ip) or _ipv6_ipv4_re.match(ip) or + _ipv6_abbr_re.match(ip) or _ipv6_ipv4_abbr_re.match(ip)): + return False + return True + + +def is_valid_bitmask (mask): + """Return True if given mask is a valid network bitmask""" + return 1<=mask<=32 + + +def dq2num (ip): + "convert decimal dotted quad string to long integer" + return struct.unpack('!L', socket.inet_aton(ip))[0] + + +def num2dq (n): + "convert long int to dotted quad string" + return socket.inet_ntoa(struct.pack('!L', n)) + + +def suffix2mask (n): + "return a mask of n bits as a long integer" + return (1L << (32 - n)) - 1 + + +def mask2suffix (mask): + """return suff for given bit mask""" + return 32 - int(math.log(mask+1, 2)) + + +def dq2mask (ip): + "return a mask of bits as a long integer" + n = dq2num(ip) + return -((-n+1) | n) + + +def dq2net (ip, mask): + "return a tuple (network ip, network mask) for given ip and mask" + n = dq2num(ip) + net = n - (n & mask) + return (net, mask) + + +def dq_in_net (n, net, mask): + """return True iff numerical ip n is in given net with mask. + (net,mask) must be returned previously by ip2net""" + m = n - (n & mask) + return m==net + + +def host_in_set (ip, hosts, nets): + """return True if given ip is in host or network list""" + if ip in hosts: + return True + if is_valid_ipv4(ip): + n = dq2num(ip) + for net, mask in nets: + if dq_in_net(n, net, mask): + return True + return False + + +def strhosts2map (strhosts): + """convert a string representation of hosts and networks to + a tuple (hosts, networks)""" + return hosts2map([s.strip() for s in strhosts.split(",") if s]) + + +def hosts2map (hosts): + """return a set of named hosts, and a list of subnets (host/netmask + adresses). + Only IPv4 host/netmasks are supported. + """ + hostset = sets.Set() + nets = [] + for host in hosts: + if _host_bitmask_re.match(host): + host, mask = host.split("/") + mask = int(mask) + if not is_valid_bitmask(mask): + bk.log.error(LOG_NET, + "bitmask %d is not a valid network mask", mask) + continue + if not is_valid_ipv4(host): + bk.log.error(LOG_NET, + "host %r is not a valid ip address", host) + continue + nets.append(dq2net(host, suffix2mask(mask))) + elif _host_netmask_re.match(host): + host, mask = host.split("/") + if not is_valid_ipv4(host): + bk.log.error(LOG_NET, + "host %r is not a valid ip address", host) + continue + if not is_valid_ipv4(mask): + bk.log.error(LOG_NET, + "mask %r is not a valid ip network mask", mask) + continue + nets.append(dq2net(host, dq2mask(mask))) + elif is_valid_ip(host): + hostset.add(expand_ip(host)[0]) + else: + try: + hostset |= resolve_host(host) + except socket.gaierror: + bk.log.error(LOG_NET, "invalid host %r", host) + return (hostset, nets) + + +def map2hosts (hostmap): + """convert a tuple (hosts, networks) into a host/network list + suitable for storing in a config file""" + ret = hostmap[0].copy() + for net, mask in hostmap[1]: + ret.add("%s/%d" % (num2dq(net), mask2suffix(mask))) + return ret + + +def lookup_ips (ips): + """return set of host names that resolve to given ips""" + hosts = sets.Set() + for ip in ips: + try: + hosts.add(socket.gethostbyaddr(ip)[0]) + except socket.error: + hosts.add(ip) + return hosts + + +def resolve_host (host): + """return set of ip numbers for given host""" + ips = sets.Set() + for res in socket.getaddrinfo(host, None, 0, socket.SOCK_STREAM): + af, socktype, proto, canonname, sa = res + ips.add(sa[0]) + return ips + diff --git a/bk/net/nt.py b/bk/net/nt.py new file mode 100644 index 00000000..7889c2ef --- /dev/null +++ b/bk/net/nt.py @@ -0,0 +1,87 @@ +# -*- coding: iso-8859-1 -*- +"""network utilities for windows platforms""" +# Copyright (C) 2004 Bastian Kleineidam +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +import sets +import bk.winreg + + +def get_localaddrs (): + """all active interfaces' ip addresses""" + addrs = sets.Set() + try: # search interfaces + key = bk.winreg.key_handle(bk.winreg.HKEY_LOCAL_MACHINE, + r"SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces") + for subkey in key.subkeys(): + if subkey.get('EnableDHCP'): + ip = subkey.get('DhcpIPAddress', '') + else: + ip = subkey.get('IPAddress', '') + if not (isinstance(ip, basestring) and ip): + continue + addrs.add(str(ip).lower()) + except EnvironmentError: + pass + return addrs + + +def resolver_config (config): + """get DNS config from Windows registry settings""" + try: + key = bk.winreg.key_handle(bk.winreg.HKEY_LOCAL_MACHINE, + r"SYSTEM\CurrentControlSet\Services\Tcpip\Parameters") + except EnvironmentError: + try: # for Windows ME + key = bk.winreg.key_handle(bk.winreg.HKEY_LOCAL_MACHINE, + r"SYSTEM\CurrentControlSet\Services\VxD\MSTCP") + except EnvironmentError: + key = None + if key: + if key.get('EnableDHCP'): + servers = bk.winreg.stringdisplay(key.get("DhcpNameServer", "")) + domains = key.get("DhcpDomain", "").split() + else: + servers = bk.winreg.stringdisplay(key.get("NameServer", "")) + domains = key.get("SearchList", "").split() + config.nameservers.extend([ str(s).lower() for s in servers if s ]) + config.search_domains.extend([ str(d).lower() for d in domains if d ]) + try: # search adapters + key = bk.winreg.key_handle(bk.winreg.HKEY_LOCAL_MACHINE, + r"SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\DNSRegisteredAdapters") + except EnvironmentError: + key = None + if key: + for subkey in key.subkeys(): + values = subkey.get("DNSServerAddresses", "") + servers = bk.winreg.binipdisplay(values) + config.nameservers.extend([ str(s).lower() for s in servers if s ]) + try: # search interfaces + key = bk.winreg.key_handle(bk.winreg.HKEY_LOCAL_MACHINE, + r"SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces") + except EnvironmentError: + key = None + if key: + for subkey in key.subkeys(): + if subkey.get('EnableDHCP'): + servers = bk.winreg.stringdisplay(subkey.get('DhcpNameServer', '')) + domains = subkey.get("DhcpDomain", "").split() + else: + servers = bk.winreg.stringdisplay(subkey.get('NameServer', '')) + domains = subkey.get("SearchList", "").split() + config.nameservers.extend([ str(s).lower() for s in servers if s ]) + config.search_domains.extend([ str(d).lower() for d in domains if d ]) + diff --git a/bk/net/posix.py b/bk/net/posix.py new file mode 100644 index 00000000..3c90d2bf --- /dev/null +++ b/bk/net/posix.py @@ -0,0 +1,132 @@ +# -*- coding: iso-8859-1 -*- +"""network utilities""" + +import socket +import array +import fcntl +import os +import struct +import re +import wc +import bk.log + + +class IfConfig (object): + """Access to socket interfaces""" + + SIOCGIFNAME = 0x8910 + SIOCGIFCONF = 0x8912 + SIOCGIFFLAGS = 0x8913 + SIOCGIFADDR = 0x8915 + SIOCGIFBRDADDR = 0x8919 + SIOCGIFNETMASK = 0x891b + SIOCGIFCOUNT = 0x8938 + + IFF_UP = 0x1 # Interface is up. + IFF_BROADCAST = 0x2 # Broadcast address valid. + IFF_DEBUG = 0x4 # Turn on debugging. + IFF_LOOPBACK = 0x8 # Is a loopback net. + IFF_POINTOPOINT = 0x10 # Interface is point-to-point link. + IFF_NOTRAILERS = 0x20 # Avoid use of trailers. + IFF_RUNNING = 0x40 # Resources allocated. + IFF_NOARP = 0x80 # No address resolution protocol. + IFF_PROMISC = 0x100 # Receive all packets. + IFF_ALLMULTI = 0x200 # Receive all multicast packets. + IFF_MASTER = 0x400 # Master of a load balancer. + IFF_SLAVE = 0x800 # Slave of a load balancer. + IFF_MULTICAST = 0x1000 # Supports multicast. + IFF_PORTSEL = 0x2000 # Can set media type. + IFF_AUTOMEDIA = 0x4000 # Auto media select active. + + def __init__ (self): + # create a socket so we have a handle to query + self.sockfd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + + def _fcntl (self, func, args): + return fcntl.ioctl(self.sockfd.fileno(), func, args) + + def _getaddr (self, ifname, func): + ifreq = struct.pack("32s", ifname) + try: + result = self._fcntl(func, ifreq) + except IOError, msg: + bk.log.warn(LOG_NET, + "error getting addr for interface %r: %s", ifname, msg) + return None + return socket.inet_ntoa(result[20:24]) + + def getInterfaceList (self): + """ Get all interface names in a list + """ + # get interface list + buf = array.array('c', '\0' * 1024) + ifconf = struct.pack("iP", buf.buffer_info()[1], buf.buffer_info()[0]) + result = self._fcntl(self.SIOCGIFCONF, ifconf) + # loop over interface names + iflist = [] + size, ptr = struct.unpack("iP", result) + for idx in range(0, size, 32): + ifconf = buf.tostring()[idx:idx+32] + name, dummy = struct.unpack("16s16s", ifconf) + name, dummy = name.split('\0', 1) + iflist.append(name) + return iflist + + def getFlags (self, ifname): + """ Get the flags for an interface + """ + ifreq = struct.pack("32s", ifname) + try: + result = self._fcntl(self.SIOCGIFFLAGS, ifreq) + except IOError, msg: + bk.log.warn(LOG_NET, + "error getting flags for interface %r: %s", ifname, msg) + return 0 + # extract the interface's flags from the return value + flags, = struct.unpack('H', result[16:18]) + # return "UP" bit + return flags + + def getAddr (self, ifname): + """Get the inet addr for an interface""" + return self._getaddr(ifname, self.SIOCGIFADDR) + + def getMask (self, ifname): + """Get the netmask for an interface""" + return self._getaddr(ifname, self.SIOCGIFNETMASK) + + def getBroadcast (self, ifname): + """Get the broadcast addr for an interface""" + return self._getaddr(ifname, self.SIOCGIFBRDADDR) + + def isUp (self, ifname): + """Check whether interface 'ifname' is UP""" + return (self.getFlags(ifname) & self.IFF_UP) != 0 + + +ifc = IfConfig() + + +def get_localaddrs (): + """all active interfaces' ip addresses""" + return [ ifc.getAddr(iface) for iface in ifc.getInterfaceList() + if ifc.isUp(iface) ] + + +def resolver_config (config): + "Set up the DnsLookupConnection class with /etc/resolv.conf information" + if not os.path.exists('/etc/resolv.conf'): + return + for line in file('/etc/resolv.conf', 'r').readlines(): + line = line.strip() + if (not line) or line.startswith(';') or line.startswith('#'): + continue + m = re.match(r'^search\s+(\.?.+)$', line) + if m: + for domain in m.group(1).split(): + config.search_domains.append('.'+domain.lower()) + m = re.match(r'^nameserver\s+(\S+)\s*$', line) + if m: + config.nameservers.append(m.group(1).lower()) + + diff --git a/bk/net/tests/__init__.py b/bk/net/tests/__init__.py new file mode 100644 index 00000000..5d3edfa5 --- /dev/null +++ b/bk/net/tests/__init__.py @@ -0,0 +1 @@ +# -*- coding: iso-8859-1 -*- diff --git a/bk/net/tests/test_dns.py b/bk/net/tests/test_dns.py new file mode 100644 index 00000000..2dd18d93 --- /dev/null +++ b/bk/net/tests/test_dns.py @@ -0,0 +1,52 @@ +# -*- coding: iso-8859-1 -*- +import unittest +import bk.net +from bk.net.dns.Lib import Packer, Unpacker + + +class TestDns (unittest.TestCase): + """Test DNS routines""" + + def testPacker (self): + """Test packin/unpacking (see section 4.1.4 of RFC 1035)""" + p = Packer() + p.addaddr('192.168.0.1') + p.addbytes('*' * 20) + p.addname('f.ISI.ARPA') + p.addbytes('*' * 8) + p.addname('Foo.F.isi.arpa') + p.addbytes('*' * 18) + p.addname('arpa') + p.addbytes('*' * 26) + p.addname('') + u = Unpacker(p.buf) + self.assertEqual(u.getaddr(), '192.168.0.1') + self.assertEqual(u.getbytes(20), '*' * 20) + self.assertEqual(u.getname(), 'f.ISI.ARPA') + self.assertEqual(u.getbytes(8), '*' * 8) + self.assertEqual(u.getname(), 'Foo.f.ISI.ARPA') + self.assertEqual(u.getbytes(18), '*' * 18) + self.assertEqual(u.getname(), 'ARPA') + self.assertEqual(u.getbytes(26), '*' * 26) + self.assertEqual(u.getname(), '') + + def testHostLowercase (self): + for host in bk.net.get_localhosts(): + self.assertEqual(host, host.lower()) + config = bk.net.resolver_config() + for host in config.nameservers: + self.assertEqual(host, host.lower()) + for host in config.search_domains: + self.assertEqual(host, host.lower()) + for host in config.search_patterns: + self.assertEqual(host, host.lower()) + + +def test_suite (): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TestDns)) + return suite + +if __name__ == '__main__': + unittest.main() + diff --git a/bk/net/tests/test_ip.py b/bk/net/tests/test_ip.py new file mode 100644 index 00000000..864dedaa --- /dev/null +++ b/bk/net/tests/test_ip.py @@ -0,0 +1,92 @@ +# -*- coding: iso-8859-1 -*- + +import unittest +import bk.net.ip + + +class TestIp (unittest.TestCase): + + def testNames (self): + hosts, nets = bk.net.ip.hosts2map(["www.kampfesser.net", + "q2345qwer9 u2 42ß3 i34 uq3tu ", + "2.3.4", ".3.4", "3.4", ".4", "4", ""]) + for host in bk.net.ip.resolve_host("www.kampfesser.net"): + self.assert_(bk.net.ip.host_in_set(host, hosts, nets)) + self.assert_(not bk.net.ip.host_in_set("q2345qwer9 u2 42ß3 i34 uq3tu ", hosts, nets)) + self.assert_(not bk.net.ip.host_in_set("q2345qwer9", hosts, nets)) + self.assert_(not bk.net.ip.host_in_set("2.3.4", hosts, nets)) + self.assert_(not bk.net.ip.host_in_set(".3.4", hosts, nets)) + self.assert_(not bk.net.ip.host_in_set("3.4", hosts, nets)) + self.assert_(not bk.net.ip.host_in_set(".4", hosts, nets)) + self.assert_(not bk.net.ip.host_in_set("4", hosts, nets)) + self.assert_(not bk.net.ip.host_in_set("", hosts, nets)) + + def testIPv4_1 (self): + hosts, nets = bk.net.ip.hosts2map(["1.2.3.4"]) + self.assert_(bk.net.ip.host_in_set("1.2.3.4", hosts, nets)) + + def testNetwork1 (self): + hosts, nets = bk.net.ip.hosts2map(["192.168.1.1/8"]) + for i in range(255): + self.assert_(bk.net.ip.host_in_set("192.168.1.%d"%i, hosts, nets)) + + def testNetwork2 (self): + hosts, nets = bk.net.ip.hosts2map(["192.168.1.1/16"]) + for i in range(255): + for j in range(255): + self.assert_(bk.net.ip.host_in_set("192.168.%d.%d"%(i,j), hosts, nets)) + + #def testNetwork3 (self): + # hosts, nets = bk.net.ip.hosts2map(["192.168.1.1/24"]) + # for i in range(255): + # for j in range(255): + # for k in range(255): + # self.assert_(bk.net.ip.host_in_set("192.%d.%d.%d"%(i,j,k), hosts, nets)) + + def testNetwork4 (self): + hosts, nets = bk.net.ip.hosts2map(["192.168.1.1/255.255.255.0"]) + for i in range(255): + self.assert_(bk.net.ip.host_in_set("192.168.1.%d"%i, hosts, nets)) + + def testNetwork5 (self): + hosts, nets = bk.net.ip.hosts2map(["192.168.1.1/255.255.0.0"]) + for i in range(255): + for j in range(255): + self.assert_(bk.net.ip.host_in_set("192.168.%d.%d"%(i,j), hosts, nets)) + + #def testNetwork6 (self): + # hosts, nets = bk.net.ip.hosts2map(["192.168.1.1/255.0.0.0"]) + # for i in range(255): + # for j in range(255): + # for k in range(255): + # self.assert_(bk.net.ip.host_in_set("192.%d.%d.%d"%(i,j,k), hosts, nets)) + + def testIPv6_1 (self): + hosts, nets = bk.net.ip.hosts2map(["::0"]) + # XXX + + def testIPv6_2 (self): + hosts, nets = bk.net.ip.hosts2map(["1::"]) + # XXX + + def testIPv6_3 (self): + hosts, nets = bk.net.ip.hosts2map(["1::1"]) + # XXX + + def testIPv6_4 (self): + hosts, nets = bk.net.ip.hosts2map(["fe00::0"]) + # XXX + + def testNetmask (self): + for i in range(32): + hosts, nets = bk.net.ip.hosts2map(["144.145.146.1/%d"%i]) + + +def test_suite (): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TestIp)) + return suite + +if __name__ == '__main__': + unittest.main() +