456 lines
16 KiB
Python
Raw Normal View History

import tempfile
from zope.interface import implements
from twisted.python import components
from twisted.application import service, strports
from twisted.internet import defer
from twisted.conch.ssh import factory, keys, session
from twisted.conch.interfaces import ISFTPServer, ISFTPFile, IConchUser
from twisted.conch.avatar import ConchUser
from twisted.conch.openssh_compat import primes
from twisted.conch import ls
from twisted.cred import portal
from allmydata.interfaces import IDirectoryNode, ExistingChildError, \
NoSuchChildError
from allmydata.immutable.upload import FileHandle
from allmydata.util.consumer import download_to_data
class ReadFile:
implements(ISFTPFile)
def __init__(self, node):
self.node = node
def readChunk(self, offset, length):
d = download_to_data(self.node, offset, length)
def _got(data):
return data
d.addCallback(_got)
return d
def close(self):
pass
def getAttrs(self):
print "GETATTRS(file)"
raise NotImplementedError
def setAttrs(self, attrs):
print "SETATTRS(file)", attrs
raise NotImplementedError
class WriteFile:
implements(ISFTPFile)
def __init__(self, parent, childname, convergence):
self.parent = parent
self.childname = childname
self.convergence = convergence
self.f = tempfile.TemporaryFile()
def writeChunk(self, offset, data):
self.f.seek(offset)
self.f.write(data)
def close(self):
u = FileHandle(self.f, self.convergence)
d = self.parent.add_file(self.childname, u)
return d
def getAttrs(self):
print "GETATTRS(file)"
raise NotImplementedError
def setAttrs(self, attrs):
print "SETATTRS(file)", attrs
raise NotImplementedError
class NoParentError(Exception):
pass
class PermissionError(Exception):
pass
from twisted.conch.ssh.filetransfer import FileTransferServer, SFTPError, \
FX_NO_SUCH_FILE, FX_FILE_ALREADY_EXISTS, FX_OP_UNSUPPORTED, \
FX_PERMISSION_DENIED
from twisted.conch.ssh.filetransfer import FXF_READ, FXF_WRITE, FXF_APPEND, FXF_CREAT, FXF_TRUNC, FXF_EXCL
class SFTPUser(ConchUser):
def __init__(self, client, rootnode, username, convergence):
ConchUser.__init__(self)
self.channelLookup["session"] = session.SSHSession
self.subsystemLookup["sftp"] = FileTransferServer
self.client = client
self.root = rootnode
self.username = username
self.convergence = convergence
class StoppableList:
def __init__(self, items):
self.items = items
def __iter__(self):
for i in self.items:
yield i
def close(self):
pass
class FakeStat:
pass
class BadRemoveRequest(Exception):
pass
class SFTPHandler:
implements(ISFTPServer)
def __init__(self, user):
print "Creating SFTPHandler from", user
self.client = user.client
self.root = user.root
self.username = user.username
self.convergence = user.convergence
def gotVersion(self, otherVersion, extData):
return {}
def openFile(self, filename, flags, attrs):
2008-11-05 13:45:11 -07:00
f = "|".join([f for f in
[(flags & FXF_READ) and "FXF_READ" or None,
(flags & FXF_WRITE) and "FXF_WRITE" or None,
(flags & FXF_APPEND) and "FXF_APPEND" or None,
(flags & FXF_CREAT) and "FXF_CREAT" or None,
(flags & FXF_TRUNC) and "FXF_TRUNC" or None,
(flags & FXF_EXCL) and "FXF_EXCL" or None,
]
if f])
print "OPENFILE", filename, flags, f, attrs
# this is used for both reading and writing.
# createPlease = False
# exclusive = False
# openFlags = 0
#
# if flags & FXF_READ == FXF_READ and flags & FXF_WRITE == 0:
# openFlags = os.O_RDONLY
# if flags & FXF_WRITE == FXF_WRITE and flags & FXF_READ == 0:
# createPlease = True
# openFlags = os.O_WRONLY
# if flags & FXF_WRITE == FXF_WRITE and flags & FXF_READ == FXF_READ:
# createPlease = True
# openFlags = os.O_RDWR
# if flags & FXF_APPEND == FXF_APPEND:
# createPlease = True
# openFlags |= os.O_APPEND
# if flags & FXF_CREAT == FXF_CREAT:
# createPlease = True
# openFlags |= os.O_CREAT
# if flags & FXF_TRUNC == FXF_TRUNC:
# openFlags |= os.O_TRUNC
# if flags & FXF_EXCL == FXF_EXCL:
# exclusive = True
# /usr/bin/sftp 'get' gives us FXF_READ, while 'put' on a new file
# gives FXF_WRITE,FXF_CREAT,FXF_TRUNC . I'm guessing that 'put' on an
# existing file gives the same.
path = self._convert_sftp_path(filename)
if flags & FXF_READ:
if flags & FXF_WRITE:
raise NotImplementedError
d = self._get_node_and_metadata_for_path(path)
d.addCallback(lambda (node,metadata): ReadFile(node))
d.addErrback(self._convert_error)
return d
if flags & FXF_WRITE:
if not (flags & FXF_CREAT) or not (flags & FXF_TRUNC):
raise NotImplementedError
if not path:
raise PermissionError("cannot STOR to root directory")
childname = path[-1]
d = self._get_root(path)
def _got_root((root, path)):
if not path:
raise PermissionError("cannot STOR to root directory")
return root.get_child_at_path(path[:-1])
d.addCallback(_got_root)
def _got_parent(parent):
return WriteFile(parent, childname, self.convergence)
d.addCallback(_got_parent)
return d
raise NotImplementedError
def removeFile(self, path):
print "REMOVEFILE", path
path = self._convert_sftp_path(path)
return self._remove_thing(path, must_be_file=True)
def renameFile(self, oldpath, newpath):
print "RENAMEFILE", oldpath, newpath
fromPath = self._convert_sftp_path(oldpath)
toPath = self._convert_sftp_path(newpath)
# the target directory must already exist
d = self._get_parent(fromPath)
def _got_from_parent( (fromparent, childname) ):
d = self._get_parent(toPath)
d.addCallback(lambda (toparent, tochildname):
fromparent.move_child_to(childname,
toparent, tochildname,
overwrite=False))
return d
d.addCallback(_got_from_parent)
d.addErrback(self._convert_error)
return d
def makeDirectory(self, path, attrs):
print "MAKEDIRECTORY", path, attrs
# TODO: extract attrs["mtime"], use it to set the parent metadata.
# Maybe also copy attrs["ext_*"] .
path = self._convert_sftp_path(path)
d = self._get_root(path)
d.addCallback(lambda (root,path):
self._get_or_create_directories(root, path))
return d
def _get_or_create_directories(self, node, path):
if not IDirectoryNode.providedBy(node):
# unfortunately it is too late to provide the name of the
# blocking directory in the error message.
raise ExistingChildError("cannot create directory because there "
"is a file in the way") # close enough
if not path:
return defer.succeed(node)
d = node.get(path[0])
def _maybe_create(f):
f.trap(NoSuchChildError)
return node.create_subdirectory(path[0])
d.addErrback(_maybe_create)
d.addCallback(self._get_or_create_directories, path[1:])
return d
def removeDirectory(self, path):
print "REMOVEDIRECTORY", path
path = self._convert_sftp_path(path)
return self._remove_thing(path, must_be_directory=True)
def _remove_thing(self, path, must_be_directory=False, must_be_file=False):
d = defer.maybeDeferred(self._get_parent, path)
def _convert_error(f):
f.trap(NoParentError)
raise PermissionError("cannot delete root directory")
d.addErrback(_convert_error)
def _got_parent( (parent, childname) ):
d = parent.get(childname)
def _got_child(child):
if must_be_directory and not IDirectoryNode.providedBy(child):
raise BadRemoveRequest("rmdir called on a file")
if must_be_file and IDirectoryNode.providedBy(child):
raise BadRemoveRequest("rmfile called on a directory")
return parent.delete(childname)
d.addCallback(_got_child)
d.addErrback(self._convert_error)
return d
d.addCallback(_got_parent)
return d
def openDirectory(self, path):
print "OPENDIRECTORY", path
path = self._convert_sftp_path(path)
d = self._get_node_and_metadata_for_path(path)
d.addCallback(lambda (dirnode,metadata): dirnode.list())
def _render(children):
results = []
for filename, (node, metadata) in children.iteritems():
s = FakeStat()
if IDirectoryNode.providedBy(node):
s.st_mode = 040700
s.st_size = 0
else:
s.st_mode = 0100600
s.st_size = node.get_size()
s.st_nlink = 1
s.st_uid = 0
s.st_gid = 0
s.st_mtime = int(metadata.get("mtime", 0))
longname = ls.lsLine(filename.encode("utf-8"), s)
attrs = self._populate_attrs(node, metadata)
results.append( (filename.encode("utf-8"), longname, attrs) )
return StoppableList(results)
d.addCallback(_render)
return d
def getAttrs(self, path, followLinks):
print "GETATTRS", path, followLinks
# from ftp.stat
d = self._get_node_and_metadata_for_path(self._convert_sftp_path(path))
def _render((node,metadata)):
return self._populate_attrs(node, metadata)
d.addCallback(_render)
d.addErrback(self._convert_error)
def _done(res):
print " DONE", res
return res
d.addBoth(_done)
return d
def _convert_sftp_path(self, pathstring):
assert pathstring[0] == "/"
pathstring = pathstring.strip("/")
if pathstring == "":
path = []
else:
path = pathstring.split("/")
print "CONVERT", pathstring, path
path = [unicode(p) for p in path]
return path
def _get_node_and_metadata_for_path(self, path):
d = self._get_root(path)
def _got_root((root,path)):
print "ROOT", root
print "PATH", path
if path:
return root.get_child_and_metadata_at_path(path)
else:
return (root,{})
d.addCallback(_got_root)
return d
def _get_root(self, path):
# return (root, remaining_path)
path = [unicode(p) for p in path]
if path and path[0] == "uri":
d = defer.maybeDeferred(self.client.create_node_from_uri,
str(path[1]))
d.addCallback(lambda root: (root, path[2:]))
else:
d = defer.succeed((self.root,path))
return d
def _populate_attrs(self, childnode, metadata):
attrs = {}
attrs["uid"] = 1000
attrs["gid"] = 1000
attrs["atime"] = 0
attrs["mtime"] = int(metadata.get("mtime", 0))
isdir = bool(IDirectoryNode.providedBy(childnode))
if isdir:
attrs["size"] = 1
# the permissions must have the extra bits (040000 or 0100000),
# otherwise the client will not call openDirectory
attrs["permissions"] = 040700 # S_IFDIR
else:
attrs["size"] = childnode.get_size()
attrs["permissions"] = 0100600 # S_IFREG
return attrs
def _convert_error(self, f):
if f.check(NoSuchChildError):
childname = f.value.args[0].encode("utf-8")
raise SFTPError(FX_NO_SUCH_FILE, childname)
if f.check(ExistingChildError):
msg = f.value.args[0].encode("utf-8")
raise SFTPError(FX_FILE_ALREADY_EXISTS, msg)
if f.check(PermissionError):
raise SFTPError(FX_PERMISSION_DENIED, str(f.value))
if f.check(NotImplementedError):
raise SFTPError(FX_OP_UNSUPPORTED, str(f.value))
return f
def setAttrs(self, path, attrs):
print "SETATTRS", path, attrs
# ignored
return None
def readLink(self, path):
print "READLINK", path
raise NotImplementedError
def makeLink(self, linkPath, targetPath):
print "MAKELINK", linkPath, targetPath
raise NotImplementedError
def extendedRequest(self, extendedName, extendedData):
print "EXTENDEDREQUEST", extendedName, extendedData
# client 'df' command requires 'statvfs@openssh.com' extension
raise NotImplementedError
def realPath(self, path):
print "REALPATH", path
if path == ".":
return "/"
return path
def _get_parent(self, path):
# fire with (parentnode, childname)
path = [unicode(p) for p in path]
if not path:
raise NoParentError
childname = path[-1]
d = self._get_root(path)
def _got_root((root, path)):
if not path:
raise NoParentError
return root.get_child_at_path(path[:-1])
d.addCallback(_got_root)
def _got_parent(parent):
return (parent, childname)
d.addCallback(_got_parent)
return d
# if you have an SFTPUser, and you want something that provides ISFTPServer,
# then you get SFTPHandler(user)
components.registerAdapter(SFTPHandler, SFTPUser, ISFTPServer)
2010-02-26 01:14:33 -07:00
from allmydata.frontends.auth import AccountURLChecker, AccountFileChecker, NeedRootcapLookupScheme
class Dispatcher:
implements(portal.IRealm)
def __init__(self, client):
self.client = client
def requestAvatar(self, avatarID, mind, interface):
assert interface == IConchUser
rootnode = self.client.create_node_from_uri(avatarID.rootcap)
convergence = self.client.convergence
s = SFTPUser(self.client, rootnode, avatarID.username, convergence)
def logout(): pass
return (interface, s, logout)
class SFTPServer(service.MultiService):
def __init__(self, client, accountfile, accounturl,
sftp_portstr, pubkey_file, privkey_file):
service.MultiService.__init__(self)
r = Dispatcher(client)
p = portal.Portal(r)
if accountfile:
c = AccountFileChecker(self, accountfile)
p.registerChecker(c)
if accounturl:
c = AccountURLChecker(self, accounturl)
p.registerChecker(c)
if not accountfile and not accounturl:
# we could leave this anonymous, with just the /uri/CAP form
raise NeedRootcapLookupScheme("must provide some translation")
pubkey = keys.Key.fromFile(pubkey_file)
privkey = keys.Key.fromFile(privkey_file)
class SSHFactory(factory.SSHFactory):
publicKeys = {pubkey.sshType(): pubkey}
privateKeys = {privkey.sshType(): privkey}
def getPrimes(self):
try:
# if present, this enables diffie-hellman-group-exchange
return primes.parseModuliFile("/etc/ssh/moduli")
except IOError:
return None
f = SSHFactory()
f.portal = p
s = strports.service(sftp_portstr, f)
s.setServiceParent(self)