Jordy wrote:
> Michael L Torrie wrote:
> [chop]
>> For developing some kind of tcp/ip server/client system, Python wins,
>> hands down.  Twisted for networking, or django for a web frontend (or
>> Turbo Gears.  Whatever).  A simple, asynchronous, event-driven server
>> complete with a custom protocol can be created with about 10-20 lines of
>> code.
> 
> As an up-and-coming python hacker, I've been intrigued by what I've read 
> about twisted.  Does anyone here have experience with it?  Can you 
> recommend it / advise against it for writing network servers in python?

Twisted has a very steep learning curve.  Incredibly steep.  Plus
Twisted is a good example of where duck typing can be a real problem.
For example, trying to analyze someone's existing Twisted app can be
very difficult.  To even figure out their simple LDAP proxy demo, I had
to run it in a debugger to see just how control was moving through the
program and what the various callback parameters were.  Things are
complicated because callback parameters are often marshalled ahead of
time in a closure.  Plus the theory of callbacks (deferreds) takes some
thinking to wrap one's head around.

BUT, once you do grasp Twisted, it because a very powerful and quick way
to prototype all kinds of network services.  For example I just
installed some new Sharp copiers that have LDAP integration built in.
But here were three problems.  First their LDAP bind DNs were not
RFC-compliant (thank MS for promoting this kind of crap), and second we
didn't want everyone in our LDAP database to be able to use the copier.
So enter Twisted.  Second, we didn't want all users in LDAP to work on
the copier and third, Sharp used a really silly search filter when
searching for e-mail addresses that made it impossible, for example, to
search for last names.

In just a few lines of code I was able to make an LDAP proxy server that
rewrote the BindDN to have a proper dn, and also to make sure that only
certain user types could access the copier.  I also intercepted search
requests and fixed the filter.

If anyone cares, I've attached it as an example for the record.  Much of
the logic is contained in the ldaptor proxy class that I am building off
of.  It took me may hour before I figured out what the
self._whenConnected() method call was doing.  Then I figured out that it
was a very creative way of dealing with the fact that the client may
send multiple requests to the proxy before the proxy has managed to
establish a connection to the real server.

cheers,
Michael




> 


-- 
Michael Torrie
Assistant CSR, System Administrator
Chemistry and Biochemistry Department
Brigham Young University
Provo, UT 84602
+1.801.422.5771


from ldaptor.protocols.ldap import proxy
from ldaptor.protocols.ldap import ldapsyntax, ldaperrors
from ldaptor.protocols import pureldap
from ldaptor.ldapfilter import parseFilter
import os
import re

baddn=re.compile(r'^[\w]+$')
removeextra=re.compile(r'(\+[\w]*)')
badsearch=re.compile(r'cn=[\w]+\*')
badsearch_replace=re.compile(r'(cn=)')

gids = [ 
         3000,
         4000,
         5000,
         6000,
         6010,
         6020,
         6100,
         8000,
       ]

# filters to control who can bind to this ldap proxy server
# if there are more than one, they are ANDed together.
binddn_filters = [ 
                   # only allow certain gids (in above list) to bind
                   "(|" + "".join([ '(gidNumber=%d)' % g for g in gids]) + ")",
                   # "(directoryRole=copier)", #example of required attribute
                 ]

# if the binddn is bad, use this format string to rewrite it:
dn_format = "uid=%s,cn=users,dc=chem,dc=byu,dc=edu"

if len(binddn_filters) > 1:
    binddn_ldap_filter = parseFilter("(&" + "".join(binddn_filters) + ")")
else:
    binddn_ldap_filter = parseFilter(binddn_filters[0])

class SharpLdapProxy(proxy.Proxy):
    """
    An LDAP proxy for use with the Sharp Copiers.

    - BindRequests are intercepted and bad BindDNs are fixed
    - Searches intercepted and filters for (cn=blah*) are converted 
      to (cn=*blah*)
    - Only BindDNs matching certain criteria (defined as LDAP search
      filters) are allowed to bind
    """

    def __init__(self,
                 *a,
                 **kw):
        proxy.Proxy.__init__(self, *a, **kw)

    def handle_LDAPSearchRequest(self, request, controls, reply):
        filter=request.filter.asText()
        if(badsearch.search(filter)):
            newfilter=badsearch_replace.sub('cn=*',filter)
            request.filter=parseFilter(newfilter)
            print request.filter.__repr__()


        return self._whenConnected(self.handleUnknown, request, controls, reply)

    def handle_LDAPBindRequest(self, request, controls, reply):
        if request.version != 3:
            raise ldaperrors.LDAPProtocolError, \
                  'Version %u not supported' % request.version

        self.checkControls(controls)

        if request.dn == '':
            # anonymous bind
            return self._whenConnected(self.handleUnknown,request, controls, reply)
        else:
            # if we get a broken bind dn
            request.dn = removeextra.sub('',request.dn)
            if baddn.match(request.dn): 
                # fix it.
                request.dn = dn_format % request.dn

            return self._whenConnected(self._get_uid_detail,request,controls, reply)

    def _get_uid_detail(self, request, controls, reply):

        # initiate search, setting the search dn to the dn of the binddn
        baseEntry = ldapsyntax.LDAPEntryWithClient(client=self.client, dn=request.dn)

        # check for simple DN presence presence
        d = baseEntry.search(filterObject=binddn_ldap_filter)

        d.addCallback(self._got_uid_detail, request, controls, reply)
        # handle error here -- errors trigger error scripts
        return d


    def _got_uid_detail(self, entries, request,controls, reply):
        if len(entries):
            # we found the user! - should only ever return one user since
            # we searched on the user's DN to begin with.

            # process the bind normally.  The BindDN is already fixed at this point
            print "%s is authorized!" % request.dn
            return self._whenConnected(self.handleUnknown, request, controls, reply)
            
        else:
            # he didn't match our strict criteria!
            print "%s is NOT authorized!" % request.dn
            # create an error message to reply with
            raise ldaperrors.LDAPInvalidCredentials, \
                  'You are not authorized to use this machine'


if __name__ == '__main__':
    """
    Sharp Printer proxy; fixes bind requests and passes them on to the real server.
    Note that when this is run as a daemon, the settings are in the .tac file, not
    here.
    """
    from twisted.internet import reactor, protocol
    from twisted.python import log
    import sys
    log.startLogging(sys.stderr)
    from ldaptor import config

    factory = protocol.ServerFactory()
    cfg = config.LDAPConfig(serviceLocationOverrides={
        '': ('galadriel', 389),
        })
    factory.protocol = lambda : SharpLdapProxy(config=cfg,
                                                    )
    if os.getuid() == 0:
        reactor.listenTCP(389, factory)
    else:
        reactor.listenTCP(10389, factory)

    reactor.run()

/*
PLUG: http://plug.org, #utah on irc.freenode.net
Unsubscribe: http://plug.org/mailman/options/plug
Don't fear the penguin.
*/

Reply via email to