2008-11-04 18:00:22 -07:00
|
|
|
|
|
|
|
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
|
2008-11-05 19:25:58 -07:00
|
|
|
from twisted.cred import portal
|
2008-11-04 18:00:22 -07:00
|
|
|
|
|
|
|
from allmydata.interfaces import IDirectoryNode, ExistingChildError, \
|
|
|
|
NoSuchChildError
|
|
|
|
from allmydata.immutable.upload import FileHandle
|
2009-12-01 17:44:35 -05:00
|
|
|
from allmydata.util.consumer import download_to_data
|
2008-11-04 18:00:22 -07:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2009-02-22 17:24:26 -07:00
|
|
|
class BadRemoveRequest(Exception):
|
|
|
|
pass
|
|
|
|
|
2008-11-04 18:00:22 -07:00
|
|
|
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])
|
2008-11-04 18:00:22 -07:00
|
|
|
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)
|
2009-10-12 19:15:20 -07:00
|
|
|
return node.create_subdirectory(path[0])
|
2008-11-04 18:00:22 -07:00
|
|
|
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):
|
2009-02-22 17:24:26 -07:00
|
|
|
raise BadRemoveRequest("rmdir called on a file")
|
2008-11-04 18:00:22 -07:00
|
|
|
if must_be_file and IDirectoryNode.providedBy(child):
|
2009-02-22 17:24:26 -07:00
|
|
|
raise BadRemoveRequest("rmfile called on a directory")
|
2008-11-04 18:00:22 -07:00
|
|
|
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):
|
2009-04-11 11:19:06 -07:00
|
|
|
assert pathstring[0] == "/"
|
2008-11-04 18:00:22 -07:00
|
|
|
pathstring = pathstring.strip("/")
|
2009-04-11 11:19:06 -07:00
|
|
|
if pathstring == "":
|
2008-11-04 18:00:22 -07:00
|
|
|
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
|
2008-11-05 19:25:58 -07:00
|
|
|
|
2008-11-04 18:00:22 -07:00
|
|
|
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)
|
|
|
|
|
2008-11-05 19:25:58 -07:00
|
|
|
r = Dispatcher(client)
|
|
|
|
p = portal.Portal(r)
|
|
|
|
|
2008-11-04 18:00:22 -07:00
|
|
|
if accountfile:
|
|
|
|
c = AccountFileChecker(self, accountfile)
|
2008-11-05 19:25:58 -07:00
|
|
|
p.registerChecker(c)
|
|
|
|
if accounturl:
|
2008-11-04 18:00:22 -07:00
|
|
|
c = AccountURLChecker(self, accounturl)
|
2008-11-05 19:25:58 -07:00
|
|
|
p.registerChecker(c)
|
|
|
|
if not accountfile and not accounturl:
|
2008-11-04 18:00:22 -07:00
|
|
|
# we could leave this anonymous, with just the /uri/CAP form
|
2009-02-22 17:24:26 -07:00
|
|
|
raise NeedRootcapLookupScheme("must provide some translation")
|
2008-11-04 18:00:22 -07:00
|
|
|
|
|
|
|
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)
|
|
|
|
|