3 from zope.interface import implements
4 from twisted.python import components
5 from twisted.application import service, strports
6 from twisted.internet import defer
7 from twisted.conch.ssh import factory, keys, session
8 from twisted.conch.interfaces import ISFTPServer, ISFTPFile, IConchUser
9 from twisted.conch.avatar import ConchUser
10 from twisted.conch.openssh_compat import primes
11 from twisted.conch import ls
12 from twisted.cred import portal
14 from allmydata.interfaces import IDirectoryNode, ExistingChildError, \
16 from allmydata.immutable.upload import FileHandle
17 from allmydata.util.consumer import download_to_data
21 def __init__(self, node):
23 def readChunk(self, offset, length):
24 d = download_to_data(self.node, offset, length)
32 print "GETATTRS(file)"
33 raise NotImplementedError
34 def setAttrs(self, attrs):
35 print "SETATTRS(file)", attrs
36 raise NotImplementedError
41 def __init__(self, parent, childname, convergence):
43 self.childname = childname
44 self.convergence = convergence
45 self.f = tempfile.TemporaryFile()
46 def writeChunk(self, offset, data):
51 u = FileHandle(self.f, self.convergence)
52 d = self.parent.add_file(self.childname, u)
56 print "GETATTRS(file)"
57 raise NotImplementedError
58 def setAttrs(self, attrs):
59 print "SETATTRS(file)", attrs
60 raise NotImplementedError
63 class NoParentError(Exception):
66 class PermissionError(Exception):
69 from twisted.conch.ssh.filetransfer import FileTransferServer, SFTPError, \
70 FX_NO_SUCH_FILE, FX_FILE_ALREADY_EXISTS, FX_OP_UNSUPPORTED, \
72 from twisted.conch.ssh.filetransfer import FXF_READ, FXF_WRITE, FXF_APPEND, FXF_CREAT, FXF_TRUNC, FXF_EXCL
74 class SFTPUser(ConchUser):
75 def __init__(self, client, rootnode, username, convergence):
76 ConchUser.__init__(self)
77 self.channelLookup["session"] = session.SSHSession
78 self.subsystemLookup["sftp"] = FileTransferServer
82 self.username = username
83 self.convergence = convergence
86 def __init__(self, items):
97 class BadRemoveRequest(Exception):
101 implements(ISFTPServer)
102 def __init__(self, user):
103 print "Creating SFTPHandler from", user
104 self.client = user.client
105 self.root = user.root
106 self.username = user.username
107 self.convergence = user.convergence
109 def gotVersion(self, otherVersion, extData):
112 def openFile(self, filename, flags, attrs):
113 f = "|".join([f for f in
114 [(flags & FXF_READ) and "FXF_READ" or None,
115 (flags & FXF_WRITE) and "FXF_WRITE" or None,
116 (flags & FXF_APPEND) and "FXF_APPEND" or None,
117 (flags & FXF_CREAT) and "FXF_CREAT" or None,
118 (flags & FXF_TRUNC) and "FXF_TRUNC" or None,
119 (flags & FXF_EXCL) and "FXF_EXCL" or None,
122 print "OPENFILE", filename, flags, f, attrs
123 # this is used for both reading and writing.
125 # createPlease = False
129 # if flags & FXF_READ == FXF_READ and flags & FXF_WRITE == 0:
130 # openFlags = os.O_RDONLY
131 # if flags & FXF_WRITE == FXF_WRITE and flags & FXF_READ == 0:
132 # createPlease = True
133 # openFlags = os.O_WRONLY
134 # if flags & FXF_WRITE == FXF_WRITE and flags & FXF_READ == FXF_READ:
135 # createPlease = True
136 # openFlags = os.O_RDWR
137 # if flags & FXF_APPEND == FXF_APPEND:
138 # createPlease = True
139 # openFlags |= os.O_APPEND
140 # if flags & FXF_CREAT == FXF_CREAT:
141 # createPlease = True
142 # openFlags |= os.O_CREAT
143 # if flags & FXF_TRUNC == FXF_TRUNC:
144 # openFlags |= os.O_TRUNC
145 # if flags & FXF_EXCL == FXF_EXCL:
148 # /usr/bin/sftp 'get' gives us FXF_READ, while 'put' on a new file
149 # gives FXF_WRITE,FXF_CREAT,FXF_TRUNC . I'm guessing that 'put' on an
150 # existing file gives the same.
152 path = self._convert_sftp_path(filename)
155 if flags & FXF_WRITE:
156 raise NotImplementedError
157 d = self._get_node_and_metadata_for_path(path)
158 d.addCallback(lambda (node,metadata): ReadFile(node))
159 d.addErrback(self._convert_error)
162 if flags & FXF_WRITE:
163 if not (flags & FXF_CREAT) or not (flags & FXF_TRUNC):
164 raise NotImplementedError
166 raise PermissionError("cannot STOR to root directory")
168 d = self._get_root(path)
169 def _got_root((root, path)):
171 raise PermissionError("cannot STOR to root directory")
172 return root.get_child_at_path(path[:-1])
173 d.addCallback(_got_root)
174 def _got_parent(parent):
175 return WriteFile(parent, childname, self.convergence)
176 d.addCallback(_got_parent)
178 raise NotImplementedError
180 def removeFile(self, path):
181 print "REMOVEFILE", path
182 path = self._convert_sftp_path(path)
183 return self._remove_thing(path, must_be_file=True)
185 def renameFile(self, oldpath, newpath):
186 print "RENAMEFILE", oldpath, newpath
187 fromPath = self._convert_sftp_path(oldpath)
188 toPath = self._convert_sftp_path(newpath)
189 # the target directory must already exist
190 d = self._get_parent(fromPath)
191 def _got_from_parent( (fromparent, childname) ):
192 d = self._get_parent(toPath)
193 d.addCallback(lambda (toparent, tochildname):
194 fromparent.move_child_to(childname,
195 toparent, tochildname,
198 d.addCallback(_got_from_parent)
199 d.addErrback(self._convert_error)
202 def makeDirectory(self, path, attrs):
203 print "MAKEDIRECTORY", path, attrs
204 # TODO: extract attrs["mtime"], use it to set the parent metadata.
205 # Maybe also copy attrs["ext_*"] .
206 path = self._convert_sftp_path(path)
207 d = self._get_root(path)
208 d.addCallback(lambda (root,path):
209 self._get_or_create_directories(root, path))
212 def _get_or_create_directories(self, node, path):
213 if not IDirectoryNode.providedBy(node):
214 # unfortunately it is too late to provide the name of the
215 # blocking directory in the error message.
216 raise ExistingChildError("cannot create directory because there "
217 "is a file in the way") # close enough
219 return defer.succeed(node)
220 d = node.get(path[0])
221 def _maybe_create(f):
222 f.trap(NoSuchChildError)
223 return node.create_subdirectory(path[0])
224 d.addErrback(_maybe_create)
225 d.addCallback(self._get_or_create_directories, path[1:])
228 def removeDirectory(self, path):
229 print "REMOVEDIRECTORY", path
230 path = self._convert_sftp_path(path)
231 return self._remove_thing(path, must_be_directory=True)
233 def _remove_thing(self, path, must_be_directory=False, must_be_file=False):
234 d = defer.maybeDeferred(self._get_parent, path)
235 def _convert_error(f):
236 f.trap(NoParentError)
237 raise PermissionError("cannot delete root directory")
238 d.addErrback(_convert_error)
239 def _got_parent( (parent, childname) ):
240 d = parent.get(childname)
241 def _got_child(child):
242 if must_be_directory and not IDirectoryNode.providedBy(child):
243 raise BadRemoveRequest("rmdir called on a file")
244 if must_be_file and IDirectoryNode.providedBy(child):
245 raise BadRemoveRequest("rmfile called on a directory")
246 return parent.delete(childname)
247 d.addCallback(_got_child)
248 d.addErrback(self._convert_error)
250 d.addCallback(_got_parent)
254 def openDirectory(self, path):
255 print "OPENDIRECTORY", path
256 path = self._convert_sftp_path(path)
257 d = self._get_node_and_metadata_for_path(path)
258 d.addCallback(lambda (dirnode,metadata): dirnode.list())
259 def _render(children):
261 for filename, (node, metadata) in children.iteritems():
263 if IDirectoryNode.providedBy(node):
268 s.st_size = node.get_size()
272 s.st_mtime = int(metadata.get("mtime", 0))
273 longname = ls.lsLine(filename.encode("utf-8"), s)
274 attrs = self._populate_attrs(node, metadata)
275 results.append( (filename.encode("utf-8"), longname, attrs) )
276 return StoppableList(results)
277 d.addCallback(_render)
280 def getAttrs(self, path, followLinks):
281 print "GETATTRS", path, followLinks
283 d = self._get_node_and_metadata_for_path(self._convert_sftp_path(path))
284 def _render((node,metadata)):
285 return self._populate_attrs(node, metadata)
286 d.addCallback(_render)
287 d.addErrback(self._convert_error)
294 def _convert_sftp_path(self, pathstring):
295 assert pathstring[0] == "/"
296 pathstring = pathstring.strip("/")
300 path = pathstring.split("/")
301 print "CONVERT", pathstring, path
302 path = [unicode(p) for p in path]
305 def _get_node_and_metadata_for_path(self, path):
306 d = self._get_root(path)
307 def _got_root((root,path)):
311 return root.get_child_and_metadata_at_path(path)
314 d.addCallback(_got_root)
317 def _get_root(self, path):
318 # return (root, remaining_path)
319 path = [unicode(p) for p in path]
320 if path and path[0] == "uri":
321 d = defer.maybeDeferred(self.client.create_node_from_uri,
323 d.addCallback(lambda root: (root, path[2:]))
325 d = defer.succeed((self.root,path))
328 def _populate_attrs(self, childnode, metadata):
333 attrs["mtime"] = int(metadata.get("mtime", 0))
334 isdir = bool(IDirectoryNode.providedBy(childnode))
337 # the permissions must have the extra bits (040000 or 0100000),
338 # otherwise the client will not call openDirectory
339 attrs["permissions"] = 040700 # S_IFDIR
341 attrs["size"] = childnode.get_size()
342 attrs["permissions"] = 0100600 # S_IFREG
345 def _convert_error(self, f):
346 if f.check(NoSuchChildError):
347 childname = f.value.args[0].encode("utf-8")
348 raise SFTPError(FX_NO_SUCH_FILE, childname)
349 if f.check(ExistingChildError):
350 msg = f.value.args[0].encode("utf-8")
351 raise SFTPError(FX_FILE_ALREADY_EXISTS, msg)
352 if f.check(PermissionError):
353 raise SFTPError(FX_PERMISSION_DENIED, str(f.value))
354 if f.check(NotImplementedError):
355 raise SFTPError(FX_OP_UNSUPPORTED, str(f.value))
359 def setAttrs(self, path, attrs):
360 print "SETATTRS", path, attrs
364 def readLink(self, path):
365 print "READLINK", path
366 raise NotImplementedError
368 def makeLink(self, linkPath, targetPath):
369 print "MAKELINK", linkPath, targetPath
370 raise NotImplementedError
372 def extendedRequest(self, extendedName, extendedData):
373 print "EXTENDEDREQUEST", extendedName, extendedData
374 # client 'df' command requires 'statvfs@openssh.com' extension
375 raise NotImplementedError
376 def realPath(self, path):
377 print "REALPATH", path
383 def _get_parent(self, path):
384 # fire with (parentnode, childname)
385 path = [unicode(p) for p in path]
389 d = self._get_root(path)
390 def _got_root((root, path)):
393 return root.get_child_at_path(path[:-1])
394 d.addCallback(_got_root)
395 def _got_parent(parent):
396 return (parent, childname)
397 d.addCallback(_got_parent)
401 # if you have an SFTPUser, and you want something that provides ISFTPServer,
402 # then you get SFTPHandler(user)
403 components.registerAdapter(SFTPHandler, SFTPUser, ISFTPServer)
405 from auth import AccountURLChecker, AccountFileChecker, NeedRootcapLookupScheme
408 implements(portal.IRealm)
409 def __init__(self, client):
412 def requestAvatar(self, avatarID, mind, interface):
413 assert interface == IConchUser
414 rootnode = self.client.create_node_from_uri(avatarID.rootcap)
415 convergence = self.client.convergence
416 s = SFTPUser(self.client, rootnode, avatarID.username, convergence)
418 return (interface, s, logout)
420 class SFTPServer(service.MultiService):
421 def __init__(self, client, accountfile, accounturl,
422 sftp_portstr, pubkey_file, privkey_file):
423 service.MultiService.__init__(self)
425 r = Dispatcher(client)
429 c = AccountFileChecker(self, accountfile)
432 c = AccountURLChecker(self, accounturl)
434 if not accountfile and not accounturl:
435 # we could leave this anonymous, with just the /uri/CAP form
436 raise NeedRootcapLookupScheme("must provide some translation")
438 pubkey = keys.Key.fromFile(pubkey_file)
439 privkey = keys.Key.fromFile(privkey_file)
440 class SSHFactory(factory.SSHFactory):
441 publicKeys = {pubkey.sshType(): pubkey}
442 privateKeys = {privkey.sshType(): privkey}
445 # if present, this enables diffie-hellman-group-exchange
446 return primes.parseModuliFile("/etc/ssh/moduli")
453 s = strports.service(sftp_portstr, f)
454 s.setServiceParent(self)