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.internet.interfaces import IConsumer
8 from twisted.conch.ssh import factory, keys, session
9 from twisted.conch.interfaces import ISFTPServer, ISFTPFile, IConchUser
10 from twisted.conch.avatar import ConchUser
11 from twisted.conch.openssh_compat import primes
12 from twisted.conch import ls
13 from twisted.cred import portal
15 from allmydata.interfaces import IDirectoryNode, ExistingChildError, \
17 from allmydata.immutable.upload import FileHandle
24 def registerProducer(self, p, streaming):
26 # call resumeProducing once to start things off
31 def write(self, data):
32 self.chunks.append(data)
33 def unregisterProducer(self):
36 def download_to_data(n, offset=0, size=None):
37 d = n.read(MemoryConsumer(), offset, size)
38 d.addCallback(lambda mc: "".join(mc.chunks))
43 def __init__(self, node):
45 def readChunk(self, offset, length):
46 d = download_to_data(self.node, offset, length)
54 print "GETATTRS(file)"
55 raise NotImplementedError
56 def setAttrs(self, attrs):
57 print "SETATTRS(file)", attrs
58 raise NotImplementedError
63 def __init__(self, parent, childname, convergence):
65 self.childname = childname
66 self.convergence = convergence
67 self.f = tempfile.TemporaryFile()
68 def writeChunk(self, offset, data):
73 u = FileHandle(self.f, self.convergence)
74 d = self.parent.add_file(self.childname, u)
78 print "GETATTRS(file)"
79 raise NotImplementedError
80 def setAttrs(self, attrs):
81 print "SETATTRS(file)", attrs
82 raise NotImplementedError
85 class NoParentError(Exception):
88 class PermissionError(Exception):
91 from twisted.conch.ssh.filetransfer import FileTransferServer, SFTPError, \
92 FX_NO_SUCH_FILE, FX_FILE_ALREADY_EXISTS, FX_OP_UNSUPPORTED, \
94 from twisted.conch.ssh.filetransfer import FXF_READ, FXF_WRITE, FXF_APPEND, FXF_CREAT, FXF_TRUNC, FXF_EXCL
96 class SFTPUser(ConchUser):
97 def __init__(self, client, rootnode, username, convergence):
98 ConchUser.__init__(self)
99 self.channelLookup["session"] = session.SSHSession
100 self.subsystemLookup["sftp"] = FileTransferServer
104 self.username = username
105 self.convergence = convergence
108 def __init__(self, items):
119 class BadRemoveRequest(Exception):
123 implements(ISFTPServer)
124 def __init__(self, user):
125 print "Creating SFTPHandler from", user
126 self.client = user.client
127 self.root = user.root
128 self.username = user.username
129 self.convergence = user.convergence
131 def gotVersion(self, otherVersion, extData):
134 def openFile(self, filename, flags, attrs):
135 f = "|".join([f for f in
136 [(flags & FXF_READ) and "FXF_READ" or None,
137 (flags & FXF_WRITE) and "FXF_WRITE" or None,
138 (flags & FXF_APPEND) and "FXF_APPEND" or None,
139 (flags & FXF_CREAT) and "FXF_CREAT" or None,
140 (flags & FXF_TRUNC) and "FXF_TRUNC" or None,
141 (flags & FXF_EXCL) and "FXF_EXCL" or None,
144 print "OPENFILE", filename, flags, f, attrs
145 # this is used for both reading and writing.
147 # createPlease = False
151 # if flags & FXF_READ == FXF_READ and flags & FXF_WRITE == 0:
152 # openFlags = os.O_RDONLY
153 # if flags & FXF_WRITE == FXF_WRITE and flags & FXF_READ == 0:
154 # createPlease = True
155 # openFlags = os.O_WRONLY
156 # if flags & FXF_WRITE == FXF_WRITE and flags & FXF_READ == FXF_READ:
157 # createPlease = True
158 # openFlags = os.O_RDWR
159 # if flags & FXF_APPEND == FXF_APPEND:
160 # createPlease = True
161 # openFlags |= os.O_APPEND
162 # if flags & FXF_CREAT == FXF_CREAT:
163 # createPlease = True
164 # openFlags |= os.O_CREAT
165 # if flags & FXF_TRUNC == FXF_TRUNC:
166 # openFlags |= os.O_TRUNC
167 # if flags & FXF_EXCL == FXF_EXCL:
170 # /usr/bin/sftp 'get' gives us FXF_READ, while 'put' on a new file
171 # gives FXF_WRITE,FXF_CREAT,FXF_TRUNC . I'm guessing that 'put' on an
172 # existing file gives the same.
174 path = self._convert_sftp_path(filename)
177 if flags & FXF_WRITE:
178 raise NotImplementedError
179 d = self._get_node_and_metadata_for_path(path)
180 d.addCallback(lambda (node,metadata): ReadFile(node))
181 d.addErrback(self._convert_error)
184 if flags & FXF_WRITE:
185 if not (flags & FXF_CREAT) or not (flags & FXF_TRUNC):
186 raise NotImplementedError
188 raise PermissionError("cannot STOR to root directory")
190 d = self._get_root(path)
191 def _got_root((root, path)):
193 raise PermissionError("cannot STOR to root directory")
194 return root.get_child_at_path(path[:-1])
195 d.addCallback(_got_root)
196 def _got_parent(parent):
197 return WriteFile(parent, childname, self.convergence)
198 d.addCallback(_got_parent)
200 raise NotImplementedError
202 def removeFile(self, path):
203 print "REMOVEFILE", path
204 path = self._convert_sftp_path(path)
205 return self._remove_thing(path, must_be_file=True)
207 def renameFile(self, oldpath, newpath):
208 print "RENAMEFILE", oldpath, newpath
209 fromPath = self._convert_sftp_path(oldpath)
210 toPath = self._convert_sftp_path(newpath)
211 # the target directory must already exist
212 d = self._get_parent(fromPath)
213 def _got_from_parent( (fromparent, childname) ):
214 d = self._get_parent(toPath)
215 d.addCallback(lambda (toparent, tochildname):
216 fromparent.move_child_to(childname,
217 toparent, tochildname,
220 d.addCallback(_got_from_parent)
221 d.addErrback(self._convert_error)
224 def makeDirectory(self, path, attrs):
225 print "MAKEDIRECTORY", path, attrs
226 # TODO: extract attrs["mtime"], use it to set the parent metadata.
227 # Maybe also copy attrs["ext_*"] .
228 path = self._convert_sftp_path(path)
229 d = self._get_root(path)
230 d.addCallback(lambda (root,path):
231 self._get_or_create_directories(root, path))
234 def _get_or_create_directories(self, node, path):
235 if not IDirectoryNode.providedBy(node):
236 # unfortunately it is too late to provide the name of the
237 # blocking directory in the error message.
238 raise ExistingChildError("cannot create directory because there "
239 "is a file in the way") # close enough
241 return defer.succeed(node)
242 d = node.get(path[0])
243 def _maybe_create(f):
244 f.trap(NoSuchChildError)
245 return node.create_empty_directory(path[0])
246 d.addErrback(_maybe_create)
247 d.addCallback(self._get_or_create_directories, path[1:])
250 def removeDirectory(self, path):
251 print "REMOVEDIRECTORY", path
252 path = self._convert_sftp_path(path)
253 return self._remove_thing(path, must_be_directory=True)
255 def _remove_thing(self, path, must_be_directory=False, must_be_file=False):
256 d = defer.maybeDeferred(self._get_parent, path)
257 def _convert_error(f):
258 f.trap(NoParentError)
259 raise PermissionError("cannot delete root directory")
260 d.addErrback(_convert_error)
261 def _got_parent( (parent, childname) ):
262 d = parent.get(childname)
263 def _got_child(child):
264 if must_be_directory and not IDirectoryNode.providedBy(child):
265 raise BadRemoveRequest("rmdir called on a file")
266 if must_be_file and IDirectoryNode.providedBy(child):
267 raise BadRemoveRequest("rmfile called on a directory")
268 return parent.delete(childname)
269 d.addCallback(_got_child)
270 d.addErrback(self._convert_error)
272 d.addCallback(_got_parent)
276 def openDirectory(self, path):
277 print "OPENDIRECTORY", path
278 path = self._convert_sftp_path(path)
279 d = self._get_node_and_metadata_for_path(path)
280 d.addCallback(lambda (dirnode,metadata): dirnode.list())
281 def _render(children):
283 for filename, (node, metadata) in children.iteritems():
285 if IDirectoryNode.providedBy(node):
290 s.st_size = node.get_size()
294 s.st_mtime = int(metadata.get("mtime", 0))
295 longname = ls.lsLine(filename.encode("utf-8"), s)
296 attrs = self._populate_attrs(node, metadata)
297 results.append( (filename.encode("utf-8"), longname, attrs) )
298 return StoppableList(results)
299 d.addCallback(_render)
302 def getAttrs(self, path, followLinks):
303 print "GETATTRS", path, followLinks
305 d = self._get_node_and_metadata_for_path(self._convert_sftp_path(path))
306 def _render((node,metadata)):
307 return self._populate_attrs(node, metadata)
308 d.addCallback(_render)
309 d.addErrback(self._convert_error)
316 def _convert_sftp_path(self, pathstring):
317 pathstring = pathstring.strip("/")
318 if pathstring == "" or ".":
321 path = pathstring.split("/")
322 print "CONVERT", pathstring, path
323 path = [unicode(p) for p in path]
326 def _get_node_and_metadata_for_path(self, path):
327 d = self._get_root(path)
328 def _got_root((root,path)):
332 return root.get_child_and_metadata_at_path(path)
335 d.addCallback(_got_root)
338 def _get_root(self, path):
339 # return (root, remaining_path)
340 path = [unicode(p) for p in path]
341 if path and path[0] == "uri":
342 d = defer.maybeDeferred(self.client.create_node_from_uri,
344 d.addCallback(lambda root: (root, path[2:]))
346 d = defer.succeed((self.root,path))
349 def _populate_attrs(self, childnode, metadata):
354 attrs["mtime"] = int(metadata.get("mtime", 0))
355 isdir = bool(IDirectoryNode.providedBy(childnode))
358 # the permissions must have the extra bits (040000 or 0100000),
359 # otherwise the client will not call openDirectory
360 attrs["permissions"] = 040700 # S_IFDIR
362 attrs["size"] = childnode.get_size()
363 attrs["permissions"] = 0100600 # S_IFREG
366 def _convert_error(self, f):
367 if f.check(NoSuchChildError):
368 childname = f.value.args[0].encode("utf-8")
369 raise SFTPError(FX_NO_SUCH_FILE, childname)
370 if f.check(ExistingChildError):
371 msg = f.value.args[0].encode("utf-8")
372 raise SFTPError(FX_FILE_ALREADY_EXISTS, msg)
373 if f.check(PermissionError):
374 raise SFTPError(FX_PERMISSION_DENIED, str(f.value))
375 if f.check(NotImplementedError):
376 raise SFTPError(FX_OP_UNSUPPORTED, str(f.value))
380 def setAttrs(self, path, attrs):
381 print "SETATTRS", path, attrs
385 def readLink(self, path):
386 print "READLINK", path
387 raise NotImplementedError
389 def makeLink(self, linkPath, targetPath):
390 print "MAKELINK", linkPath, targetPath
391 raise NotImplementedError
393 def extendedRequest(self, extendedName, extendedData):
394 print "EXTENDEDREQUEST", extendedName, extendedData
395 # client 'df' command requires 'statvfs@openssh.com' extension
396 raise NotImplementedError
397 def realPath(self, path):
398 print "REALPATH", path
404 def _get_parent(self, path):
405 # fire with (parentnode, childname)
406 path = [unicode(p) for p in path]
410 d = self._get_root(path)
411 def _got_root((root, path)):
414 return root.get_child_at_path(path[:-1])
415 d.addCallback(_got_root)
416 def _got_parent(parent):
417 return (parent, childname)
418 d.addCallback(_got_parent)
422 # if you have an SFTPUser, and you want something that provides ISFTPServer,
423 # then you get SFTPHandler(user)
424 components.registerAdapter(SFTPHandler, SFTPUser, ISFTPServer)
426 from auth import AccountURLChecker, AccountFileChecker, NeedRootcapLookupScheme
429 implements(portal.IRealm)
430 def __init__(self, client):
433 def requestAvatar(self, avatarID, mind, interface):
434 assert interface == IConchUser
435 rootnode = self.client.create_node_from_uri(avatarID.rootcap)
436 convergence = self.client.convergence
437 s = SFTPUser(self.client, rootnode, avatarID.username, convergence)
439 return (interface, s, logout)
441 class SFTPServer(service.MultiService):
442 def __init__(self, client, accountfile, accounturl,
443 sftp_portstr, pubkey_file, privkey_file):
444 service.MultiService.__init__(self)
446 r = Dispatcher(client)
450 c = AccountFileChecker(self, accountfile)
453 c = AccountURLChecker(self, accounturl)
455 if not accountfile and not accounturl:
456 # we could leave this anonymous, with just the /uri/CAP form
457 raise NeedRootcapLookupScheme("must provide some translation")
459 pubkey = keys.Key.fromFile(pubkey_file)
460 privkey = keys.Key.fromFile(privkey_file)
461 class SSHFactory(factory.SSHFactory):
462 publicKeys = {pubkey.sshType(): pubkey}
463 privateKeys = {privkey.sshType(): privkey}
466 # if present, this enables diffie-hellman-group-exchange
467 return primes.parseModuliFile("/etc/ssh/moduli")
474 s = strports.service(sftp_portstr, f)
475 s.setServiceParent(self)