From 0e419aa4bf95a9251b88f7878d368c4d9b123cc7 Mon Sep 17 00:00:00 2001 From: Simo Sorce Date: Thu, 16 Aug 2007 18:00:16 -0400 Subject: Add a prototype client tool to configure a client of the IPA server Right now it does only discovery (or fallback) --- ipa-client/ipaclient/__init__.py | 23 ++ ipa-client/ipaclient/dnsclient.py | 445 +++++++++++++++++++++++++++++++++++ ipa-client/ipaclient/ipadiscovery.py | 239 +++++++++++++++++++ 3 files changed, 707 insertions(+) create mode 100644 ipa-client/ipaclient/__init__.py create mode 100644 ipa-client/ipaclient/dnsclient.py create mode 100644 ipa-client/ipaclient/ipadiscovery.py (limited to 'ipa-client/ipaclient') diff --git a/ipa-client/ipaclient/__init__.py b/ipa-client/ipaclient/__init__.py new file mode 100644 index 00000000..66a4eb14 --- /dev/null +++ b/ipa-client/ipaclient/__init__.py @@ -0,0 +1,23 @@ +#! /usr/bin/python -E +# Authors: Karl MacMillan +# see inline +# +# Copyright (C) 2007 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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; version 2 or later +# +# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +__all__ = ["ipadiscovery"] + diff --git a/ipa-client/ipaclient/dnsclient.py b/ipa-client/ipaclient/dnsclient.py new file mode 100644 index 00000000..bc8a229c --- /dev/null +++ b/ipa-client/ipaclient/dnsclient.py @@ -0,0 +1,445 @@ +# +# Copyright 2001, 2005 Red Hat, Inc. +# +# This 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 struct +import socket +import sys + +import acutil + +DNS_C_IN = 1 +DNS_C_CS = 2 +DNS_C_CHAOS = 3 +DNS_C_HS = 4 +DNS_C_ANY = 255 + +DNS_T_A = 1 +DNS_T_NS = 2 +DNS_T_CNAME = 5 +DNS_T_SOA = 6 +DNS_T_NULL = 10 +DNS_T_WKS = 11 +DNS_T_PTR = 12 +DNS_T_HINFO = 13 +DNS_T_MX = 15 +DNS_T_TXT = 16 +DNS_T_SRV = 33 +DNS_T_ANY = 255 + +DEBUG_DNSCLIENT = False + +class DNSQueryHeader: + FORMAT = "!HBBHHHH" + def __init__(self): + self.dns_id = 0 + self.dns_rd = 0 + self.dns_tc = 0 + self.dns_aa = 0 + self.dns_opcode = 0 + self.dns_qr = 0 + self.dns_rcode = 0 + self.dns_z = 0 + self.dns_ra = 0 + self.dns_qdcount = 0 + self.dns_ancount = 0 + self.dns_nscount = 0 + self.dns_arcount = 0 + + def pack(self): + return struct.pack(DNSQueryHeader.FORMAT, + self.dns_id, + (self.dns_rd & 1) | + (self.dns_tc & 1) << 1 | + (self.dns_aa & 1) << 2 | + (self.dns_opcode & 15) << 3 | + (self.dns_qr & 1) << 7, + (self.dns_rcode & 15) | + (self.dns_z & 7) << 4 | + (self.dns_ra & 1) << 7, + self.dns_qdcount, + self.dns_ancount, + self.dns_nscount, + self.dns_arcount) + + def unpack(self, data): + (self.dns_id, byte1, byte2, self.dns_qdcount, self.dns_ancount, + self.dns_nscount, self.dns_arcount) = struct.unpack(DNSQueryHeader.FORMAT, data[0:self.size()]) + self.dns_rd = byte1 & 1 + self.dns_tc = (byte1 >> 1) & 1 + self.dns_aa = (byte1 >> 2) & 1 + self.dns_opcode = (byte1 >> 3) & 15 + self.dns_qr = (byte1 >> 7) & 1 + self.dns_rcode = byte2 & 15 + self.dns_z = (byte2 >> 4) & 7 + self.dns_ra = (byte1 >> 7) & 1 + + def size(self): + return struct.calcsize(DNSQueryHeader.FORMAT) + +def unpackQueryHeader(data): + header = DNSQueryHeader() + header.unpack(data) + return header + +class DNSResult: + FORMAT = "!HHIH" + QFORMAT = "!HH" + def __init__(self): + self.dns_name = "" + self.dns_type = 0 + self.dns_class = 0 + self.dns_ttl = 0 + self.dns_rlength = 0 + self.rdata = None + + def unpack(self, data): + (self.dns_type, self.dns_class, self.dns_ttl, + self.dns_rlength) = struct.unpack(DNSResult.FORMAT, data[0:self.size()]) + + def qunpack(self, data): + (self.dns_type, self.dns_class) = struct.unpack(DNSResult.QFORMAT, data[0:self.qsize()]) + + def size(self): + return struct.calcsize(DNSResult.FORMAT) + + def qsize(self): + return struct.calcsize(DNSResult.QFORMAT) + +class DNSRData: + def __init__(self): + pass + +#typedef struct dns_rr_a { +# u_int32_t address; +#} dns_rr_a_t; +# +#typedef struct dns_rr_cname { +# const char *cname; +#} dns_rr_cname_t; +# +#typedef struct dns_rr_hinfo { +# const char *cpu, *os; +#} dns_rr_hinfo_t; +# +#typedef struct dns_rr_mx { +# u_int16_t preference; +# const char *exchange; +#} dns_rr_mx_t; +# +#typedef struct dns_rr_null { +# unsigned const char *data; +#} dns_rr_null_t; +# +#typedef struct dns_rr_ns { +# const char *nsdname; +#} dns_rr_ns_t; +# +#typedef struct dns_rr_ptr { +# const char *ptrdname; +#} dns_rr_ptr_t; +# +#typedef struct dns_rr_soa { +# const char *mname; +# const char *rname; +# u_int32_t serial; +# int32_t refresh; +# int32_t retry; +# int32_t expire; +# int32_t minimum; +#} dns_rr_soa_t; +# +#typedef struct dns_rr_txt { +# const char *data; +#} dns_rr_txt_t; +# +#typedef struct dns_rr_srv { +# const char *server; +# u_int16_t priority; +# u_int16_t weight; +# u_int16_t port; +#} dns_rr_srv_t; + +def dnsNameToLabel(name): + out = "" + name = name.split(".") + for part in name: + out += chr(len(part)) + part + return out + +def dnsFormatQuery(query, qclass, qtype): + header = DNSQueryHeader() + + header.dns_id = 0 # FIXME: id = 0 + header.dns_rd = 1 # don't know why the original code didn't request recursion for non SOA requests + header.dns_qr = 0 # query + header.dns_opcode = 0 # standard query + header.dns_qdcount = 1 # single query + + qlabel = dnsNameToLabel(query) + if not qlabel: + return "" + + out = header.pack() + qlabel + out += chr(qtype >> 8) + out += chr(qtype & 0xff) + out += chr(qclass >> 8) + out += chr(qclass & 0xff) + + return out + +def dnsParseLabel(label, base): + # returns (output, rest) + if not label: + return ("", None) + + update = 1 + rest = label + output = "" + skip = 0 + + try: + while ord(rest[0]): + if ord(rest[0]) & 0xc0: + rest = base[((ord(rest[0]) & 0x3f) << 8) + ord(rest[1]):] + if update: + skip += 2 + update = 0 + continue + output += rest[1:ord(rest[0]) + 1] + "." + if update: + skip += ord(rest[0]) + 1 + rest = rest[ord(rest[0]) + 1:] + except IndexError: + return ("", None) + return (label[skip+update:], output) + +def dnsParseA(data, base): + rdata = DNSRData() + if len(data) < 4: + rdata.address = 0 + return None + + rdata.address = (ord(data[0])<<24) | (ord(data[1])<<16) | (ord(data[2])<<8) | (ord(data[3])<<0) + + if DEBUG_DNSCLIENT: + print "A = %d.%d.%d.%d." % (ord(data[0]), ord(data[1]), ord(data[2]), ord(data[3])) + return rdata + +def dnsParseText(data): + if len(data) < 1: + return ("", None) + tlen = ord(data[0]) + if len(data) < tlen + 1: + return ("", None) + return (data[tlen+1:], data[1:tlen+1]) + +def dnsParseNS(data, base): + rdata = DNSRData() + (rest, rdata.nsdname) = dnsParseLabel(data, base) + if DEBUG_DNSCLIENT: + print "NS DNAME = \"%s\"." % (rdata.nsdname) + return rdata + +def dnsParseCNAME(data, base): + rdata = DNSRData() + (rest, rdata.cname) = dnsParseLabel(data, base) + if DEBUG_DNSCLIENT: + print "CNAME = \"%s\"." % (rdata.cname) + return rdata + +def dnsParseSOA(data, base): + rdata = DNSRData() + format = "!IIIII" + + (rest, rdata.mname) = dnsParseLabel(data, base) + if rdata.mname is None: + return None + (rest, rdata.rname) = dnsParseLabel(rest, base) + if rdata.rname is None: + return None + if len(rest) < struct.calcsize(format): + return None + + (rdata.serial, rdata.refresh, rdata.retry, rdata.expire, + rdata.minimum) = struct.unpack(format, rest[:struct.calcsize(format)]) + + if DEBUG_DNSCLIENT: + print "SOA(mname) = \"%s\"." % rdata.mname + print "SOA(rname) = \"%s\"." % rdata.rname + print "SOA(serial) = %d." % rdata.serial + print "SOA(refresh) = %d." % rdata.refresh + print "SOA(retry) = %d." % rdata.retry + print "SOA(expire) = %d." % rdata.expire + print "SOA(minimum) = %d." % rdata.minimum + return rdata + +def dnsParseNULL(data, base): + # um, yeah + return None + +def dnsParseWKS(data, base): + return None + +def dnsParseHINFO(data, base): + rdata = DNSRData() + (rest, rdata.cpu) = dnsParseText(data) + if rest: + (rest, rdata.os) = dnsParseText(rest) + if DEBUG_DNSCLIENT: + print "HINFO(cpu) = \"%s\"." % rdata.cpu + print "HINFO(os) = \"%s\"." % rdata.os + return rdata + +def dnsParseMX(data, base): + rdata = DNSRData() + if len(data) < 2: + return None + rdata.preference = (ord(data[0]) << 8) | ord(data[1]) + (rest, rdata.exchange) = dnsParseLabel(data[2:], base) + if DEBUG_DNSCLIENT: + print "MX(exchanger) = \"%s\"." % rdata.exchange + print "MX(preference) = %d." % rdata.preference + return rdata + +def dnsParseTXT(data, base): + rdata = DNSRData() + (rest, rdata.data) = dnsParseText(data) + if DEBUG_DNSCLIENT: + print "TXT = \"%s\"." % rdata.data + return rdata + +def dnsParsePTR(data, base): + rdata = DNSRData() + (rest, rdata.ptrdname) = dnsParseLabel(data, base) + if DEBUG_DNSCLIENT: + print "PTR = \"%s\"." % rdata.ptrdname + +def dnsParseSRV(data, base): + rdata = DNSRData() + format = "!HHH" + flen = struct.calcsize(format) + if len(data) < flen: + return None + + (rdata.priority, rdata.weight, rdata.port) = struct.unpack(format, data[:flen]) + (rest, rdata.server) = dnsParseLabel(data[flen:], base) + if DEBUG_DNSCLIENT: + print "SRV(server) = \"%s\"." % rdata.server + print "SRV(weight) = %d." % rdata.weight + print "SRV(priority) = %d." % rdata.priority + print "SRV(port) = %d." % rdata.port + return rdata + +def dnsParseResults(results): + try: + header = unpackQueryHeader(results) + except struct.error: + return [] + + if header.dns_qr != 1: # should be a response + return [] + + if header.dns_rcode != 0: # should be no error + return [] + + rest = results[header.size():] + + rrlist = [] + + for i in xrange(header.dns_qdcount): + if not rest: + return [] + + rr = DNSResult() + + (rest, label) = dnsParseLabel(rest, results) + if label is None: + return [] + + if len(rest) < rr.qsize(): + return [] + + rr.qunpack(rest) + + rest = rest[rr.qsize():] + + if DEBUG_DNSCLIENT: + print "Queried for '%s', class = %d, type = %d." % (label, + rr.dns_class, rr.dns_type) + + for i in xrange(header.dns_ancount + header.dns_nscount + header.dns_arcount): + (rest, label) = dnsParseLabel(rest, results) + if label is None: + return [] + + rr = DNSResult() + + rr.dns_name = label + + if len(rest) < rr.size(): + return [] + + rr.unpack(rest) + + rest = rest[rr.size():] + + if DEBUG_DNSCLIENT: + print "Answer %d for '%s', class = %d, type = %d, ttl = %d." % (i, + rr.dns_name, rr.dns_class, rr.dns_type, + rr.dns_ttl) + + if len(rest) < rr.dns_rlength: + if DEBUG_DNSCLIENT: + print "Answer too short." + return [] + + fmap = { DNS_T_A: dnsParseA, DNS_T_NS: dnsParseNS, + DNS_T_CNAME: dnsParseCNAME, DNS_T_SOA: dnsParseSOA, + DNS_T_NULL: dnsParseNULL, DNS_T_WKS: dnsParseWKS, + DNS_T_PTR: dnsParsePTR, DNS_T_HINFO: dnsParseHINFO, + DNS_T_MX: dnsParseMX, DNS_T_TXT: dnsParseTXT, + DNS_T_SRV: dnsParseSRV} + + if not rr.dns_type in fmap: + if DEBUG_DNSCLIENT: + print "Don't know how to parse RR type %d!" % rr.dns_type + else: + rr.rdata = fmap[rr.dns_type](rest[:rr.dns_rlength], results) + + rest = rest[rr.dns_rlength:] + rrlist += [rr] + + if not rrlist: + rrlist = [rr] + return rrlist + +def query(query, qclass, qtype): + qdata = dnsFormatQuery(query, qclass, qtype) + if not qdata: + return [] + answer = acutil.res_send(qdata) + if not answer: + return [] + return dnsParseResults(answer) + +if __name__ == '__main__': + DEBUG_DNSCLIENT = True + print "Sending query." + rr = query(len(sys.argv) > 1 and sys.argv[1] or "devserv.devel.redhat.com.", + DNS_C_IN, DNS_T_ANY) + sys.exit(0) diff --git a/ipa-client/ipaclient/ipadiscovery.py b/ipa-client/ipaclient/ipadiscovery.py new file mode 100644 index 00000000..312c8ba4 --- /dev/null +++ b/ipa-client/ipaclient/ipadiscovery.py @@ -0,0 +1,239 @@ +#! /usr/bin/python -E +# Authors: Simo Sorce +# +# Copyright (C) 2007 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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; version 2 or later +# +# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +import socket +import logging +import dnsclient +import ldap +from ldap import LDAPError + +class IPADiscovery: + + def __init__(self): + self.realm = None + self.domain = None + self.server = None + + def getServerName(self): + return str(self.server) + + def getDomainName(self): + return str(self.domain) + + def getRealmName(self): + return str(self.realm) + + def search(self, domain = "", server = ""): + hostname = "" + qname = "" + results = [] + result = [] + krbret = [] + ldapret = [] + + if not server: + + if not domain: #domain not provided do full DNS discovery + + # get the local host name + hostname = socket.getfqdn() + if not hostname: + return -10 #bad host configuration + + # first, check for an LDAP server for the local domain + p = hostname.find(".") + if p == -1: #no domain name + return -1 + domain = hostname[p+1:] + + while not self.server: + logging.debug("[ipadnssearchldap("+domain+")]") + self.server = self.ipadnssearchldap(domain) + if self.server: + self.domain = domain + else: + p = domain.find(".") + if p == -1: #no ldap server found and last component of the domain already tested + return -1 + domain = domain[p+1:] + else: + logging.debug("[ipadnssearchldap]") + self.server = self.ipadnssearchldap(domain) + if self.server: + self.domain = domain + else: + return -2 #no ldap server found + + + #search for kerberos TODO: move this after ipacheckldap() + logging.debug("[ipadnssearchkrb]") + krbret = self.ipadnssearchkrb(self.domain) + if not krbret: + return -3 #no krb server found + + self.realm = krbret[0] + + else: #server forced on us, this means DNS doesn't work :/ + + self.domain = domain + self.server = server + + logging.debug("[ipacheckldap]") + # check ldap now + ldapret = self.ipacheckldap(self.server, self.realm); + + if not ldapret: + return -4 # not an IPA server (or broken config) + + self.server = ldapret[0] + self.realm = ldapret[1] + + return 0 + + def ipacheckldap(self, thost, trealm): + + lret = [] + lres = [] + lattr = "" + linfo = "" + lrealms = [] + + i = 0 + + #now verify the server is really an IPA server + try: + logging.debug("Init ldap with: ldap://"+thost+":389") + lh = ldap.initialize("ldap://"+thost+":389") + lh.simple_bind_s("","") + + logging.debug("Search rootdse") + lret = lh.search_s("", ldap.SCOPE_BASE, "(objectClass=*)") + for lattr in lret[0][1]: + if lattr.lower() == "namingcontexts": + lbase = lret[0][1][lattr][0] + + logging.debug("Search for (info=*) in "+lbase+"(base)") + lret = lh.search_s(lbase, ldap.SCOPE_BASE, "(info=IPA*)") + if not lret: + return [] + logging.debug("Found: "+str(lret)) + + for lattr in lret[0][1]: + if lattr.lower() == "info": + linfo = lret[0][1][lattr][0].lower() + break + + if not linfo: + return [] + + #search and return known realms + logging.debug("Search for (objectClass=krbRealmContainer) in "+lbase+"(sub)") + lret = lh.search_s("cn=kerberos,"+lbase, ldap.SCOPE_SUBTREE, "(objectClass=krbRealmContainer)") + if not lret: + #something very wrong + return [] + logging.debug("Found: "+str(lret)) + + for lres in lret: + for lattr in lres[1]: + if lattr.lower() == "cn": + lrealms.append(lres[1][lattr][0]) + + + if trealm: + for r in lrealms: + if trealm == r: + return [thost, trealm] + # must match or something is very wrong + return [] + else: + if len(lrealms) != 1: + #which one? we can't attach to a multi-realm server without DNS working + return [] + else: + return [thost, lrealms[0]] + + #we shouldn't get here + return [] + + except LDAPError, err: + #no good + logging.error("Ldap Error: "+str(err)) + return [] + + + def ipadnssearchldap(self, tdomain): + servers = "" + rserver = "" + + qname = "_ldap._tcp."+tdomain + # terminate the name + if not qname.endswith("."): + qname += "." + results = dnsclient.query(qname, dnsclient.DNS_C_IN, dnsclient.DNS_T_SRV) + + for result in results: + if result.dns_type == dnsclient.DNS_T_SRV: + rserver = result.rdata.server.rstrip(".") + if result.rdata.port and result.rdata.port != 389: + rserver += ":" + str(result.rdata.port) + if servers: + servers += "," + rserver + else: + servers = rserver + break + + return servers + + def ipadnssearchkrb(self, tdomain): + realm = "" + kdc = "" + # now, check for a Kerberos realm the local host or domain is in + qname = "_kerberos." + tdomain + # terminate the name + if not qname.endswith("."): + qname += "." + results = dnsclient.query(qname, dnsclient.DNS_C_IN, dnsclient.DNS_T_TXT) + + for result in results: + if result.dns_type == dnsclient.DNS_T_TXT: + realm = result.rdata.data + if realm: + break + + if realm: + # now fetch server information for the realm + qname = "_kerberos._udp." + tdomain + # terminate the name + if not qname.endswith("."): + qname += "." + results = dnsclient.query(qname, dnsclient.DNS_C_IN, dnsclient.DNS_T_SRV) + for result in results: + if result.dns_type == dnsclient.DNS_T_SRV: + qname = result.rdata.server.rstrip(".") + if result.rdata.port and result.rdata.port != 88: + qname += ":" + str(result.rdata.port) + if kdc: + kdc += "," + qname + else: + kdc = qname + + print "["+realm+", "+kdc+"]" + return [realm, kdc] -- cgit