Skip to content

Instantly share code, notes, and snippets.

@gfranxman
Last active February 5, 2018 12:58
Show Gist options
  • Save gfranxman/7087471 to your computer and use it in GitHub Desktop.
Save gfranxman/7087471 to your computer and use it in GitHub Desktop.
twisted ftp server with hook
"""
Twisted Sports FTP server, simple auth and post upload hook.
NOTE: In the spirit of doing it the hard way first, it now feels like I should have used
reactor.addWriter to install the HookFileWriter instead of these acrobatics.
Does the reactor really hold the filewriter for these tasks?
"""
from twisted.protocols.ftp import FTPFactory, FTPRealm, errnoToFailure
from twisted.cred.portal import Portal
from twisted.cred.checkers import AllowAnonymousAccess, FilePasswordDB
from twisted.internet import reactor
from twisted.protocols.ftp import IFTPShell, FTPAnonymousShell, FTPShell, _FileWriter, checkers, defer
def myHook( obj ):
print "processing", obj.fObj.name
return "done"
class HookFileWriter( _FileWriter ):
def __init__( self, fObj, callback ):
super( HookFileWriter, self ).__init__( fObj )
self.callback = callback
def close( self ):
res = super( HookFileWriter, self ).close()
print "********************** calling the hook ********************"
if( self.callback ):
cbres = self.callback( self )
print "callback res", cbres
return res
class HookShell( FTPShell ):
""" this shell extends the normal shell, but it's writer will run a hook after the transfer is complete.
"""
def __init__( self, filesystemRoot, callback ):
super( HookShell, self ).__init__( filesystemRoot )
self.callback = callback
def openForWriting(self, path):
"""
Open C{path} for writing.
@param path: The path, as a list of segments, to open.
@type path: C{list} of C{unicode}
@return: A L{Deferred} is returned that will fire with an object
implementing L{IWriteFile} if the file is successfully opened. If
C{path} is a directory, or if an exception is raised while trying
to open the file, the L{Deferred} will fire with an error.
"""
p = self._path(path)
if p.isdir():
# Normally, we would only check for EISDIR in open, but win32
# returns EACCES in this case, so we check before
return defer.fail(IsADirectoryError(path))
try:
fObj = p.open('w')
except (IOError, OSError), e:
return errnoToFailure(e.errno, path)
except:
return defer.fail()
print "about to defer to the _FileWriter, should use the HookedFileWriter"
#return defer.succeed(_FileWriter(fObj))
return defer.succeed( HookFileWriter(fObj, self.callback) )
class HookRealm( FTPRealm ):
""" the realm creates the FTPShell which has the _path(p) method that saves files.
we are going to use our own Shell that has a hook for processing the upload.
"""
def __init__( self, anonymousRoot, userHome="/home", callback=None ):
#super( HookRealm, self).__init__( anonymousRoot, userHome=userHome )
FTPRealm.__init__( self, anonymousRoot, userHome=userHome )
self.callback = callback
def requestAvatar(self, avatarId, mind, *interfaces):
for iface in interfaces:
if iface is IFTPShell:
if avatarId is checkers.ANONYMOUS:
avatar = FTPAnonymousShell(self.anonymousRoot)
else:
#avatar = FTPShell(self.getHomeDirectory(avatarId))
avatar = HookShell(self.getHomeDirectory(avatarId), self.callback)
return (IFTPShell, avatar,
getattr(avatar, 'logout', lambda: None))
raise NotImplementedError(
"Only IFTPShell interface is supported by this realm")
############## boilerplate server using our HookRealm and callback
p = Portal( HookRealm('/no_anon_access/', userHome="/tmp/", callback=myHook),
#[AllowAnonymousAccess(), FilePasswordDB("pass.dat")])
[FilePasswordDB("pass.dat"), ])
#
# Once the portal is set up, start up the FTPFactory and pass the portal to
# it on startup. FTPFactory will start up a twisted.protocols.ftp.FTP()
# handler for each incoming OPEN request. Business as usual in Twisted land.
#
f = FTPFactory( p )
f.allowAnonymous = False
f.welcomeMessage = "TwistedSportsFTP 0.1"
#
# You know this part. Point the reactor to port 21 coupled with the above factory,
# and start the event loop.
#
reactor.listenTCP(9021, f)
reactor.run()
# Donez0rs
@gfranxman
Copy link
Author

This uses a local file called pass.dat which should be plaintext [user]:[password]
and there should be a directory called /tmp/[user]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment