ftpdav.py   [plain text]


# this is the ftp-related stuff that doesn't belong in the protocol itself
# -- Cred Objects --

import os
import time
import string
import types
import re
from cStringIO import StringIO

# Twisted Imports
from twisted.internet import reactor, protocol, error, defer
from twisted.internet.interfaces import IProducer, IConsumer, IProtocol, IFinishableConsumer
from twisted.internet.protocol import ClientFactory, ServerFactory, Protocol, ConsumerToProtocolAdapter
from twisted.cred import error, portal, checkers, credentials
from twisted import application, python
from twisted.python import failure, log, components

# import my sandbox ftp
import ftp

try:
    import pwd, grp
except ImportError:
    print "sorry, currently ftpdav only works with linux and linux variants"
    raise SystemExit("ftpdav doesn't do windows")

def _callWithDefault(default, _f, *_a, **_kw):
    try:
        return _f(*_a, **_kw)
    except KeyError:
        return default

def _memberGIDs(gid):
    """returns a list of all gid's that are a member of group with id
    """
    gr_mem = 3
    return grp.getgrgid(gid)[gr_mem]

def _testPermissions(uid, gid, spath, mode='r'):
    """checks to see if uid has proper permissions to access path with mode
    @param uid: numeric user id
    @type uid: int
    @param gid: numeric group id
    @type gid: int
    @param spath: the path on the server to test
    @type spath: string
    @param mode: 'r' or 'w' (read or write)
    @type mode: string
    @returns: a True if the uid can access path
    @rval: Boolean
    """
    import os.path as osp 
    import stat
    if mode not in ['r', 'w']:
        raise ValueError("mode argument must be 'r' or 'w'")
    
    readMasks = {'usr': stat.S_IRUSR, 'grp': stat.S_IRGRP, 'oth': stat.S_IROTH}
    writeMasks = {'usr': stat.S_IWUSR, 'grp': stat.S_IWGRP, 'oth': stat.S_IWOTH}
    modes = {'r': readMasks, 'w': writeMasks}
    log.msg('running _testPermissions')
    if osp.exists(spath):
        s = os.lstat(spath)
        if uid == 0:    # root is superman, can access everything
            log.msg('uid == root, can do anything!')
            return True
        elif modes[mode]['usr'] & s.st_mode > 0 and uid == s.st_uid:
            log.msg('usr has proper permissions')
            return True
        elif ((modes[mode]['grp'] & s.st_mode > 0) and 
                (gid == s.st_gid or gid in _memberGIDs(gid))):
            log.msg('grp has proper permissions')
            return True
        elif modes[mode]['oth'] & s.st_mode > 0:
            log.msg('oth has proper permissions')
            return True
    return False   

class AnonymousShell(object):
    """"""
    __implements__ = (ftp.IShell,)

    uid      = None        # uid of anonymous user for shell
    gid      = None        # gid of anonymous user for shell
    clientwd = '/'
    filepath = None

    def __init__(self, user=None, tld=None):
        """Constructor
        @param user: the name of the user whose permissions we'll be using
        @type user: string
        """
        self.user     = user        # user name
        self.tld      = tld
        self.debug    = True

        # TODO: self.user needs to be set to something!!!
        if self.user is None:
            uid = os.getuid()
            self.user = pwd.getpwuid(os.getuid())[0]
            self.getUserUIDAndGID()
        #if self.tld is not None:
            #self.filepath = python.FilePath(self.tld)

    def getUserUIDAndGID(self):
        """used to set up permissions checking. finds the uid and gid of 
        the shell.user. called during __init__
        """
        log.msg('getUserUIDAndGID')
        pw_name, pw_passwd, pw_uid, pw_gid, pw_dir = range(5)
        try:
            p = pwd.getpwnam(self.user)
            self.uid, self.gid = p[pw_uid], p[pw_gid]
            log.debug("set (uid,gid) for file-permissions checking to (%s,%s)" % (self.uid,self.gid))
        except KeyError, (e,):
            log.msg("""
COULD NOT SET ANONYMOUS UID! Name %s could not be found.
We will continue using the user %s.
""" % (self.user, pwd.getpwuid(os.getuid())[pw_name]))


    def pwd(self):
        return self.clientwd

    def myjoin(self, lpath, rpath):
        """does a dumb join between two path elements, ensuring
        there is only one '/' between them. pays no attention to the
        filesystem, unlike os.path.join
        
        @param lpath: path element to the left of the '/' in the result
        @type lpath: string
        @param rpath: path element to the right of the '/' in the result
        @type rpath: string
        """
        if lpath and lpath[-1] == os.sep:
            lpath = lpath[:-1]
        if rpath and rpath[0] == os.sep:
            rpath = rpath[1:]
        return "%s%s%s" % (lpath, os.sep, rpath)

    def mapCPathToSPath(self, rpath):
        if not rpath or rpath[0] != '/':      # if this is not an absolute path
            # add the clients working directory to the requested path
            mappedClientPath = self.myjoin(self.clientwd, rpath) 
        else:
            mappedClientPath = rpath
        # next add the client's top level directory to the requested path
        mappedServerPath = self.myjoin(self.tld, mappedClientPath)
        ncpath, nspath = os.path.normpath(mappedClientPath), os.path.normpath(mappedServerPath)
        common = os.path.commonprefix([self.tld, nspath])
        if common != self.tld:
            raise PathBelowTLDError('Cannot access below / directory')
        if not os.path.exists(nspath):
            raise FileNotFoundError(nspath)
        return (mappedClientPath, mappedServerPath)
 
    def cwd(self, path):
        cpath, spath = self.mapCPathToSPath(path)
        log.debug(cpath, spath)
        if os.path.exists(spath) and os.path.isdir(spath):
            self.clientwd = cpath
        else:
            raise FileNotFoundError(cpath)
       
    def cdup(self):
        self.cwd('..')

    def dele(self, path):
        raise AnonUserDeniedError()
        
    def mkd(self, path):
        raise AnonUserDeniedError()
        
    def rmd(self, path):
        raise AnonUserDeniedError()
 
    def retr(self, path):
        import os.path as osp
        cpath, spath = self.mapCPathToSPath(path)
        if not osp.isfile(spath):
            raise FileNotFoundError(cpath)
        #if not _testPermissions(self.uid, self.gid, spath):
            #raise PermissionDeniedError(cpath)
        try:
            return (file(spath, 'rb'), os.path.getsize(spath))
        except (IOError, OSError), (e,):
            log.debug(e)
            raise OperationFailedError('An error occurred %s' % e)

    def stor(self, params):
        raise AnonUserDeniedError()

    def getUnixLongListString(self, spath):
        """generates the equivalent output of a unix ls -l path, but
        using python-native code. 

        @param path: the path to return the listing for
        @type path: string
        @attention: this has only been tested on posix systems, I don't
            know at this point whether or not it will work on win32
        """
        import pwd, grp, time

        TYPE, PMSTR, NLINKS, OWN, GRP, SZ, MTIME, NAME = range(8)

        if os.path.isdir(spath):
            log.debug('list path isdir')
            dlist = os.listdir(spath)
            log.debug(dlist)
            dlist.sort()
        else:
            log.debug('list path is not dir')
            dlist = [spath]

        pstat = None
        result = []
        sio = StringIO()
        maxNameWidth, maxOwnWidth, maxGrpWidth, maxSizeWidth, maxNlinksWidth = 0, 0, 0, 0, 0
        

        for item in dlist:
            try:
                pstat = os.lstat(os.path.join(spath, item))

                # this is exarkun's bit of magic
                fmt = 'pld----'
                pmask = lambda mode: ''.join([mode & (256 >> n) and 'rwx'[n % 3] or '-' for n in range(9)])
                dtype = lambda mode: [fmt[i] for i in range(7) if (mode >> 12) & (1 << i)][0]

                type = dtype(pstat.st_mode)
                pmstr = pmask(pstat.st_mode)
                nlinks = str(pstat.st_nlink)
                owner = _callWithDefault([str(pstat.st_uid)], pwd.getpwuid, pstat.st_uid)[0]
                group = _callWithDefault([str(pstat.st_gid)], grp.getgrgid, pstat.st_gid)[0]
                size = str(pstat.st_size)
                mtime = time.strftime('%b %d %I:%M', time.gmtime(pstat.st_mtime))
                name = os.path.split(item)[1]
                unixpms = "%s%s" % (type,pmstr)
            except (OSError, KeyError), e:
                log.debug(e)
                continue
            if len(name) > maxNameWidth:
                maxNameWidth = len(name)
            if len(owner) > maxOwnWidth:
                maxOwnWidth = len(owner)
            if len(group) > maxGrpWidth:
                maxGrpWidth = len(group)
            if len(size) > maxSizeWidth:
                maxSizeWidth = len(size)
            if len(nlinks) > maxNlinksWidth:
                maxNlinksWidth = len(nlinks)
            result.append([type, pmstr, nlinks, owner, group, size, mtime, name])

        for r in result:
            r[OWN]  = r[OWN].ljust(maxOwnWidth)
            r[GRP]  = r[GRP].ljust(maxGrpWidth)
            r[SZ]   = r[SZ].rjust(maxSizeWidth)
            #r[NAME] = r[NAME].ljust(maxNameWidth)
            r[NLINKS] = r[NLINKS].rjust(maxNlinksWidth)
            sio.write('%s%s %s %s %s %s %8s %s\n' % tuple(r))

        sio.seek(0)
        return sio
       
    def list(self, path):
        cpath, spath = self.mapCPathToSPath(path)
        log.debug('cpath: %s,   spath:%s' % (cpath, spath))
        #if not _testPermissions(self.uid, self.gid, spath):
            #raise PermissionDeniedError(cpath)
        sio = self.getUnixLongListString(spath)
        return (sio, len(sio.getvalue()))

    def mdtm(self, path):
        from stat import ST_MTIME
        cpath, spath = self.mapCPathToSPath(path)
        if not os.path.isfile(spath):
            raise FileNotFoundError(spath)
        try:
            dtm = time.strftime("%Y%m%d%H%M%S", time.gmtime(os.stat(spath)[ST_MTIME]))
        except OSError, (e,):
            log.err(e)
            raise OperationFailedError(e)
        else:
            return dtm

    def size(self, path):
        """returns the size in bytes of path"""
        cpath, spath = self.mapCPathToSPath(path)
        if not os.path.isfile(spath):
            raise FileNotFoundError(spath)
        return os.path.getsize(spath)
   
    def nlist(self, path):
        raise CmdNotImplementedError()

class Shell(AnonymousShell):
    def dele(self, path):
        pass

    def mkd(self, path):
        pass

    def rmd(self, path):
        pass

    def stor(self, path):
        cpath, spath = self.mapCPathToSPath(path)
        if os.access(spath, os.W_OK):
            try:
                return file(spath, 'wb')
            except (IOError, OSError), (e,):
                log.debug(e)
                raise OperationFailedError('An error occurred %s' % e)
        raise PermissionDeniedError('Could not write file %s' % cpath)


class Realm:
    __implements__ = (portal.IRealm,)
    clientwd = '/'
    user = 'anonymous'
    logout = None
    tld = None          

    def __init__(self, tld=None, logout=None):
        """constructor
        @param tld: the top-level (i.e. root) directory on the server
        @type tld: string
        @attention: you *must* set tld somewhere before using the avatar!!
        @param logout: a special logout routine you want to be run when the user
            logs out (cleanup)
        @type logout: a function/method object
        """
        self.tld = tld
        self.logout = logout

    def requestAvatar(self, avatarId, mind, *interfaces):
        if ftp.IShell in interfaces:
            if self.tld is None:
                raise ftp.TLDNotSetInRealmError("you must set FTPRealm's tld to a non-None value before creating avatars!!!")
            avatar = AnonymousShell(user=self.user, tld=self.tld)
            avatar.clientwd = self.clientwd
            avatar.logout = self.logout
            return ftp.IShell, avatar, avatar.logout
        log.msg('interfaces %s' % interfaces)
        raise NotImplementedError("Only IShell interface is supported by this realm")