From ad41fff034cde5b508497e2c8ae175fcf7c609e0 Mon Sep 17 00:00:00 2001 From: cyli Date: Tue, 3 Jul 2012 00:09:42 -0700 Subject: [PATCH 1/3] Files and classes are renamed ess --- .gitignore | 3 + {csftp => ess}/__init__.py | 0 {csftp => ess}/_openSSHConfig.py | 2 + csftp/server.py => ess/essftp.py | 69 ++--- {csftp => ess}/filepath.py | 0 {csftp => ess}/shelless.py | 15 +- ess/test/__init__.py | 0 .../test_csftp.py => ess/test/test_essftp.py | 240 ++---------------- ess/test/test_filepath.py | 160 ++++++++++++ {csftp => ess}/test/test_openSSHConfig.py | 0 {csftp => ess}/test/test_shelless.py | 46 ++-- .../{csftp_plugin.py => essftp_plugin.py} | 28 +- 12 files changed, 237 insertions(+), 326 deletions(-) create mode 100644 .gitignore rename {csftp => ess}/__init__.py (100%) rename {csftp => ess}/_openSSHConfig.py (99%) rename csftp/server.py => ess/essftp.py (94%) rename {csftp => ess}/filepath.py (100%) rename {csftp => ess}/shelless.py (99%) create mode 100644 ess/test/__init__.py rename csftp/test/test_csftp.py => ess/test/test_essftp.py (68%) create mode 100644 ess/test/test_filepath.py rename {csftp => ess}/test/test_openSSHConfig.py (100%) rename {csftp => ess}/test/test_shelless.py (91%) rename twisted/plugins/{csftp_plugin.py => essftp_plugin.py} (85%) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0941d06 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.pyc +*_trial_temp +*coverage_html_report diff --git a/csftp/__init__.py b/ess/__init__.py similarity index 100% rename from csftp/__init__.py rename to ess/__init__.py diff --git a/csftp/_openSSHConfig.py b/ess/_openSSHConfig.py similarity index 99% rename from csftp/_openSSHConfig.py rename to ess/_openSSHConfig.py index 0030387..64a4d00 100644 --- a/csftp/_openSSHConfig.py +++ b/ess/_openSSHConfig.py @@ -102,6 +102,7 @@ HostKeyAlias %(hostname)s HostName localhost CheckHostIP no + UserKnownHostsFile /dev/null StrictHostKeyChecking no IdentityFile %(clientPrivkeyFile)s Port %(port)d @@ -111,6 +112,7 @@ GSSAPIDelegateCredentials no """ + def setupConfig(directoryPath, port): f = FilePath(directoryPath) hostkey = f.child('hostkey') diff --git a/csftp/server.py b/ess/essftp.py similarity index 94% rename from csftp/server.py rename to ess/essftp.py index 6f9ad99..5f41b05 100644 --- a/csftp/server.py +++ b/ess/essftp.py @@ -1,14 +1,15 @@ import os -from csftp import shelless -from csftp.filepath import FilePath - -from zope.interface import implements -from twisted.cred import portal from twisted.conch.interfaces import ISFTPServer, ISFTPFile -from twisted.python import components -from twisted.conch.ssh import filetransfer from twisted.conch.ls import lsLine +from twisted.conch.ssh import filetransfer +from twisted.cred import portal +from twisted.python import components + +from zope.interface import implements + +from ess import shelless +from ess.filepath import FilePath def _simplifyAttributes(filePath): @@ -24,11 +25,10 @@ def _simplifyAttributes(filePath): "mtime": filePath.getModificationTime()} - -class ChrootedSFTPServer: +class EssFTPServer: implements(ISFTPServer) """ - A Chrooted SFTP server based on twisted.python.filepath.FilePath. + An SFTP server based on twisted.python.filepath.FilePath. This prevents users from connecting to a path above the set root path. It ignores permissions, since everything is executed as whatever user the SFTP server is executed as (it does not need to @@ -39,7 +39,6 @@ def __init__(self, avatar): self.avatar = avatar self.root = FilePath(self.avatar.root) - def _getFilePath(self, path): """ Takes a string path and returns a FilePath object corresponding @@ -61,7 +60,6 @@ def _getFilePath(self, path): assert fp.path.startswith(self.root.path) return fp - def _getRelativePath(self, filePath): """ Takes a FilePath and returns a path string relative to the root @@ -74,21 +72,17 @@ def _getRelativePath(self, filePath): return "/" return "/" + "/".join(filePath.segmentsFrom(self.root)) - def _islink(self, fp): if fp.islink() and fp.realpath().path.startswith(self.root.path): return True return False - def gotVersion(self, otherVersion, extData): return {} - def openFile(self, filename, flags, attrs): fp = self._getFilePath(filename) - return ChrootedSFTPFile(fp, flags, attrs) - + return ChrootedFile(fp, flags, attrs) def removeFile(self, filename): """ @@ -104,7 +98,6 @@ def removeFile(self, filename): raise IOError("%s is a directory" % filename) fp.remove() - def renameFile(self, oldname, newname): """ Rename the given file/directory/link. @@ -120,7 +113,6 @@ def renameFile(self, oldname, newname): raise IOError("%s does not exist" % oldname) oldFP.moveTo(newFP) - def makeDirectory(self, path, attrs=None): """ Make a directory. Ignores the attributes. @@ -130,7 +122,6 @@ def makeDirectory(self, path, attrs=None): raise IOError("%s already exists." % path) fp.createDirectory() - def removeDirectory(self, path): """ Remove a directory non-recursively. @@ -152,14 +143,12 @@ def removeDirectory(self, path): raise IOError("%s is not empty.") fp.remove() - def openDirectory(self, path): fp = self._getFilePath(path) if not fp.isdir(): raise IOError("%s is not a directory." % path) return ChrootedDirectory(self, fp) - def getAttrs(self, path, followLinks=True): """ Get attributes of the path. @@ -172,11 +161,9 @@ def getAttrs(self, path, followLinks=True): fp.restat(followLink=followLinks) return _simplifyAttributes(fp) - def setAttrs(self, path, attrs): raise NotImplementedError - def readLink(self, path): """ Returns the target of a symbolic link (relative to the root), so @@ -195,7 +182,6 @@ def readLink(self, path): return self._getRelativePath(rp) raise IOError("%s is not a link." % path) - def makeLink(self, linkPath, targetPath): """ Create a symbolic link from linkPath to targetPath. @@ -211,7 +197,6 @@ def makeLink(self, linkPath, targetPath): raise IOError("%s does not exist." % targetPath) tp.linkTo(lp) - def realPath(self, path): """ Despite what the interface says, this function will only return @@ -226,16 +211,14 @@ def realPath(self, path): fp = fp.realpath() return self._getRelativePath(fp) - def extendedRequest(self, extendedName, extendedData): raise NotImplementedError - #Figure out a way to test this class ChrootedDirectory: """ - A Chrooted SFTP directory based on twisted.python.filepath.FilePath. It + A "chrooted" directory based on twisted.python.filepath.FilePath. It does not expose uid and gid, and hides the fact that "fake directories" and "fake files" are links. """ @@ -248,15 +231,12 @@ def __init__(self, server, filePath): self.server = server self.files = filePath.children() - def __iter__(self): return self - def has_next(self): return len(self.files) > 0 - def next(self): # TODO: problem - what if the user that logs in is not a user in the # system? @@ -272,15 +252,13 @@ def next(self): longname = longname[:15] + longname[32:] # remove uid and gid return (f.basename(), longname, _simplifyAttributes(f)) - def close(self): self.files = None - -class ChrootedSFTPFile: +class ChrootedFile: """ - A Chrooted SFTP file based on twisted.python.filepath.FilePath. + A "chrooted" file based on twisted.python.filepath.FilePath. """ implements(ISFTPFile) @@ -292,7 +270,6 @@ def __init__(self, filePath, flags, attrs=None): self.filePath = filePath self.fd = self.filePath.open(flags=self.flagTranslator(flags)) - def flagTranslator(self, flags): """ Translate filetransfer flags to Python file opening modes @@ -330,11 +307,9 @@ def isInFlags(lookingFor): return newflags - def close(self): self.fd.close() - def readChunk(self, offset, length): """ Read a chunk of data from the file @@ -345,7 +320,6 @@ def readChunk(self, offset, length): self.fd.seek(offset) return self.fd.read(length) - def writeChunk(self, offset, data): """ Write data to the file at the given offset @@ -356,11 +330,9 @@ def writeChunk(self, offset, data): self.fd.seek(offset) self.fd.write(data) - def getAttrs(self): return _simplifyAttributes(self.filePath) - def setAttrs(self, attrs=None): """ This must return something, in order for certain write to be able to @@ -370,24 +342,21 @@ def setAttrs(self, attrs=None): #raise NotImplementedError - -class ChrootedSSHRealm(object): +class EssFTPRealm(object): """ - A realm that returns a ChrootedUser as an avatar + A realm that returns a EssSFTPUser as an avatar """ implements(portal.IRealm) def __init__(self, root): self.root = root - def requestAvatar(self, avatarID, mind, *interfaces): - user = ChrootedUser(self.root) + user = EssFTPUser(self.root) return interfaces[0], user, user.logout - -class ChrootedUser(shelless.ShelllessUser): +class EssFTPUser(shelless.ShelllessUser): """ A shell-less user that does not answer any global requests. """ @@ -397,5 +366,5 @@ def __init__(self, root): self.root = root -components.registerAdapter(ChrootedSFTPServer, ChrootedUser, +components.registerAdapter(EssFTPServer, EssFTPUser, filetransfer.ISFTPServer) diff --git a/csftp/filepath.py b/ess/filepath.py similarity index 100% rename from csftp/filepath.py rename to ess/filepath.py diff --git a/csftp/shelless.py b/ess/shelless.py similarity index 99% rename from csftp/shelless.py rename to ess/shelless.py index 576c865..04c28e5 100644 --- a/csftp/shelless.py +++ b/ess/shelless.py @@ -1,8 +1,9 @@ -from zope import interface -from twisted.cred import portal -from twisted.python import log from twisted.conch.avatar import ConchUser from twisted.conch.ssh import session +from twisted.cred import portal +from twisted.python import log + +from zope import interface class ShelllessSSHRealm: @@ -13,7 +14,6 @@ def requestAvatar(self, avatarID, mind, *interfaces): return interfaces[0], user, user.logout - class ShelllessUser(ConchUser): """ A shell-less user that does not answer any global requests. @@ -22,12 +22,10 @@ def __init__(self, root=None): ConchUser.__init__(self) self.channelLookup["session"] = ShelllessSession - def logout(self): pass # nothing to do - class ShelllessSession(session.SSHSession): name = 'shellessSession' @@ -35,7 +33,6 @@ class ShelllessSession(session.SSHSession): def __init__(self, *args, **kw): session.SSHSession.__init__(self, *args, **kw) - def _noshell(self): if not self.closing: self.write("This server does not provide shells " @@ -43,22 +40,18 @@ def _noshell(self): self.loseConnection() return 0 - def request_shell(self, data): log.msg("shell request rejected") return self._noshell() - def request_exec(self, data): log.msg("execution request rejected") return self._noshell() - def request_pty_req(self, data): log.msg("pty request rejected") return self._noshell() - def request_window_change(self, data): log.msg("window change request rejected") return 0 diff --git a/ess/test/__init__.py b/ess/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/csftp/test/test_csftp.py b/ess/test/test_essftp.py similarity index 68% rename from csftp/test/test_csftp.py rename to ess/test/test_essftp.py index 6661f16..2b48570 100644 --- a/csftp/test/test_csftp.py +++ b/ess/test/test_essftp.py @@ -1,180 +1,10 @@ -from csftp import server, filepath -from test_shelless import execCommand, TestSecured - import os - -from twisted.trial import unittest -#from twisted.python.filepath import FilePath -from twisted.test import test_paths from twisted.conch.ssh import filetransfer +from twisted.trial import unittest - -class TestFilePath(test_paths.FilePathTestCase): - - def setUpLinks(self): - test_paths.FilePathTestCase.setUp(self) - os.symlink(self.path.child("sub1").path, self._mkpath("sub1.link")) - os.symlink(self.path.child("sub1").child("file2").path, - self._mkpath("file2.link")) - self.all.sort() - - - def test_openWithModeAndFlags(self): - """ - Verify that passing both modes and flags will raise an error - """ - self.path = filepath.FilePath(self.path.path) - self.assertRaises(ValueError, self.path.child('file1').open, - mode='w+', flags=os.O_RDWR) - - - def test_openWithFlags_Nonexisting(self): - """ - Verify that nonexistant file opened without the create flag will fail - """ - self.path = filepath.FilePath(self.path.path) - created = self.path.child('createdFile') - self.assertRaises((OSError, IOError), created.open, flags=os.O_RDWR) - - - def test_openWithFlags_NoFlags(self): - """ - Verify that file opened with no read/write flags will default to - reading - """ - self.path = filepath.FilePath(self.path.path) - created = self.path.child('createdFile') - f = created.open(flags=os.O_CREAT) - self.failUnless(created.exists()) - self.assertEquals(f.read(), '') - f.close() - - - def test_openWithFlags_CannotRead(self): - """ - Verify that file opened write only will not be readable. - """ - self.path = filepath.FilePath(self.path.path) - f = self.path.child("file1").open(flags=os.O_WRONLY) - f.write('writeonly') - self.assertRaises((OSError, IOError), f.read) - f.close() - - - def test_openWithFlags_CannotWrite(self): - """ - Verify that file opened with no write can be read from but not - written to - """ - self.path = filepath.FilePath(self.path.path) - f = self.path.child("file1").open(flags=os.O_APPEND) - self.assertEquals(f.read(), 'file 1') - self.assertRaises((OSError, IOError), f.write, "append") - f.close() - - - def test_openWithFlags_AppendWrite(self): - """ - Verify that file opened with the append flag and the writeonly - flag can be appended to but not overwritten, and not read from - """ - self.path = filepath.FilePath(self.path.path) - f = self.path.child("file1").open(flags=os.O_WRONLY | os.O_APPEND) - self.assertRaises((OSError, IOError), f.read) - f.write('append') - f.seek(0) - f.write('append2') - f.close() - f = self.path.child("file1").open() - self.assertEquals(f.read(), self.f1content + 'appendappend2') - f.close() - - - def test_openWithFlags_AppendRead(self): - """ - Verify that file opened with readwrite and append is both appendable - to and readable from - """ - self.path = filepath.FilePath(self.path.path) - f = self.path.child("file1").open(flags=os.O_RDWR | os.O_APPEND) - f.write('append') - f.seek(0) - f.write('append2') - f.seek(0) - self.assertEquals(f.read(), self.f1content + 'appendappend2') - f.close() - - - def test_openWithFlags_ReadWrite_NoTruncate(self): - """ - Verify that file opened readwrite is both readable and writable. - """ - self.path = filepath.FilePath(self.path.path) - created = self.subfile("createdFile") - created.write("0000000000000000") - created.close() - created = self.path.child("createdFile") - f = created.open(flags=os.O_RDWR) - f.write('readwrite') - f.seek(0) - self.assertEquals(f.read(), 'readwrite0000000') - f.close() - - - def test_openWithFlags_Truncate(self): - """ - Verify that file called with truncate will be overwritten - """ - self.path = filepath.FilePath(self.path.path) - f = self.path.child("file1").open(flags=os.O_RDWR | os.O_TRUNC) - f.write('overwrite') - f.seek(0) - self.assertEquals(f.read(), 'overwrite') - f.close() - - - def test_openWithFlags_Exclusive(self): - """ - Verify that file opened with the exclusive flag will raise an error - """ - self.path = filepath.FilePath(self.path.path) - self.assertRaises((OSError, IOError), self.path.child("file1").open, - flags=(os.O_RDWR | os.O_CREAT | os.O_EXCL)) - - - def test_walk(self): # isn't a replacement exactly - self.setUpLinks() - self.path = filepath.FilePath(self.path.path) - test_paths.FilePathTestCase.test_walk(self) - - - def testRealpath(self): - """ - Verify that a symlink is correctly normalized - """ - self.setUpLinks() - self.path = filepath.FilePath(self.path.path) - self.assertEquals(self.path.child("sub1.link").realpath(), - self.path.child("sub1")) - self.assertEquals(self.path.child("sub1").realpath(), - self.path.child("sub1")) - - - def testStatCache(self): - self.setUpLinks() - self.path = filepath.FilePath(self.path.path) - test_paths.FilePathTestCase.testStatCache(self) - sub = self.path.child("sub1") - sublink = self.path.child("sub1.link") - sub.restat() - sublink.restat(followLink=False) - self.assertNotEquals(sub.statinfo, sublink.statinfo) - sublink.restat() - self.assertEquals(sub.statinfo, sublink.statinfo) - sub.restat(followLink=False) - self.assertEquals(sub.statinfo, sublink.statinfo) - +from ess import essftp, filepath +from ess.test.test_shelless import execCommand, TestSecured class TestAvatar: @@ -182,7 +12,6 @@ def __init__(self, root): self.root = root - class TestChrooted: def setUp(self): @@ -214,11 +43,10 @@ def makeFile(fp): makeFile(altdir.child("fileAlt")) altdir.child("fileAlt").linkTo(self.rootdir.child("fileAltLink")) - self.server = server.ChrootedSFTPServer(TestAvatar(self.rootdir.path)) - + self.server = essftp.EssFTPServer(TestAvatar(self.rootdir.path)) -class TestChrootedSFTPServer(TestChrooted, unittest.TestCase): +class TestEssFTPServer(TestChrooted, unittest.TestCase): def test_getFilePath(self): """ @@ -232,7 +60,6 @@ def test_getFilePath(self): self.assertEquals(self.server.root.child("0"), self.server._getFilePath(p)) - def test_getRelativePath(self): """ Verify that _getRelativePath will return a path relative to the root @@ -243,7 +70,6 @@ def test_getRelativePath(self): for expected, subject in mappings: self.assertEquals(self.server._getRelativePath(subject), expected) - def test_islink(self): """ Verify that _islink returns false if it's a fake directory, that is, @@ -255,7 +81,6 @@ def test_islink(self): self.server._getFilePath("fileRootLink"))) self.failIf(self.server._islink(self.server._getFilePath("altlink"))) - def testRealPath(self): """ Verify that realpath will normalize a symlink iff the target of the @@ -282,7 +107,6 @@ def testRealPath(self): "/" + "/".join(target.segmentsFrom(self.rootdir)), msg) - def testReadLink(self): """ Verify that readLink will fail when the path passed is not a link, @@ -293,7 +117,6 @@ def testReadLink(self): self.assertRaises(IOError, self.server.readLink, failureCase) self.assertEquals(self.server.readLink("fileRootLink"), "/fileRoot") - def testMakeLink(self): """ Verify that makeLink will create a symbolic link when the link @@ -305,7 +128,6 @@ def testMakeLink(self): for ln, tg in (("fileRootLink", "fileRoot"), ("fakeLink", "fake")): self.assertRaises(IOError, self.server.makeLink, ln, tg) - def testRemoveFile(self): """ Verify that removeFile only removes files and links @@ -319,7 +141,6 @@ def testRemoveFile(self): for fp in self.server.root.walk(): self.failUnless(fp.isdir()) - def testRemoveDirectory(self): """ Verify that removeDirectory only removes directories if they are @@ -345,7 +166,6 @@ def testRemoveDirectory(self): except IOError: self.fail("Removing empty 'fake directory' failed.") - def testMakeDirectory(self): """ Verify that makeDirectory creates a directory if it doesn't @@ -358,7 +178,6 @@ def testMakeDirectory(self): except IOError: self.fail("Creating a directory failed.") - def testRenameFile(self): """ Verify that renaming files, links, and directories work @@ -375,7 +194,6 @@ def testRenameFile(self): self.failUnless(newfp.exists() or newfp.islink(), "%s does not exist" % (oldname + ".ren")) - def testGetAttrs(self): """ Since this basically just returns information from FilePath, @@ -392,7 +210,6 @@ def testGetAttrs(self): self.server.getAttrs("fileRootLink", False)) self.assertEquals(fileAttrs, linkAttrs) - def testSetAttrs(self): """ Currently, this is not supported so make sure it raises an error. @@ -400,7 +217,6 @@ def testSetAttrs(self): self.assertRaises(NotImplementedError, self.server.setAttrs, "fileRoot", {}) - def testExtendedRequest(self): """ Not supported @@ -408,7 +224,6 @@ def testExtendedRequest(self): self.assertRaises(NotImplementedError, self.server.extendedRequest, None, None) - def testOpenDirectory(self): """ Make sure that what yielded is an iterable, and that trying to open @@ -423,7 +238,6 @@ def testOpenDirectory(self): self.assertEquals(1, count) - class TestChrootedDirectory(TestChrooted, unittest.TestCase): """ Makes sure that a ChrootedDirectory returns an iterable that yields @@ -435,26 +249,24 @@ def testIterable(self): """ Make sure that it is iterable and yields the subdirectory children """ - dirlist = server.ChrootedDirectory(self.server, self.rootdir) + dirlist = essftp.ChrootedDirectory(self.server, self.rootdir) self.assertEquals(len(dirlist.files), len(self.rootdir.children())) for path, longname, attrs in dirlist: self.failUnless(self.rootdir.child(path).exists()) - def testOpacity(self): """ Make sure that fake directories and files do not seem as such """ - dirlist = server.ChrootedDirectory(self.server, self.rootdir) + dirlist = essftp.ChrootedDirectory(self.server, self.rootdir) for path, longname, attrs in dirlist: if path in ("altlink", "fileAltLink"): self.assertNotEquals(attrs, self.server.getAttrs(path, False)) - -class TestChrootedSFTPFile(TestChrooted, unittest.TestCase): +class TestChrootedFile(TestChrooted, unittest.TestCase): """ - Makes sure that a ChrootedSFTPFile meets the ISFTPFile interface + Makes sure that a ChrootedFile meets the ISFTPFile interface """ read = filetransfer.FXF_READ write = filetransfer.FXF_WRITE @@ -465,22 +277,19 @@ class TestChrootedSFTPFile(TestChrooted, unittest.TestCase): def setUp(self): TestChrooted.setUp(self) - self.sftpf = server.ChrootedSFTPFile( + self.sftpf = essftp.ChrootedFile( self.rootdir.child("fileRoot"), self.read) self.flagTester = self.sftpf.flagTranslator - def bitIn(self, lookingFor, flags): return flags & lookingFor == lookingFor - def test_flagTranslator_noReadOrWrite(self): """ Make sure that translation without read or write raises an error """ self.assertRaises(ValueError, self.flagTester, self.trunc) - def test_flagTranslator_readonly(self): """ Make sure that translation of read without write -> read only @@ -489,7 +298,6 @@ def test_flagTranslator_readonly(self): self.assertTrue(self.bitIn(os.O_RDONLY, self.flagTester(self.read | self.append))) - def test_flagTranslator_writeonly(self): """ Make sure that translation of write without read -> write only @@ -498,7 +306,6 @@ def test_flagTranslator_writeonly(self): self.assertTrue(self.bitIn(os.O_WRONLY, self.flagTester(self.write | self.creat))) - def test_flagTranslator_readwrite(self): """ Make sure that translation of read + write -> readwrite @@ -507,7 +314,6 @@ def test_flagTranslator_readwrite(self): self.assertEquals(rdwr, os.O_RDWR) self.assertFalse(self.bitIn(os.O_WRONLY, rdwr)) - def test_flagTranslator_otherflags(self): """ Make sure that translation of other flags are correct @@ -521,7 +327,6 @@ def test_flagTranslator_otherflags(self): self.assertTrue( self.bitIn(osflag, self.flagTester(self.read | fflag))) - def test_closable(self): """ Make sure it's closable @@ -529,8 +334,7 @@ def test_closable(self): try: self.sftpf.close() except IOError: - self.fail("Opening/closing ChrootedSFTPFile fails") - + self.fail("Opening/closing ChrootedFile fails") def test_readChunk(self): """ @@ -540,13 +344,12 @@ def test_readChunk(self): self.assertEquals(self.sftpf.readChunk(0, 10), fp.path[:10]) self.assertEquals(self.sftpf.readChunk(5, 8), fp.path[5:13]) - def test_writeChunk(self): """ Make sure that it's writable """ fp = self.rootdir.child("fileRoot") - sftpf = server.ChrootedSFTPFile(fp, self.read | self.write) + sftpf = essftp.ChrootedFile(fp, self.read | self.write) sftpf.writeChunk(5, "NEWDATA") sftpf.close() f = fp.open() @@ -554,18 +357,17 @@ def test_writeChunk(self): f.close() - -class TestChrootedSFTP(TestSecured, unittest.TestCase): - +class TestEssFTP(TestSecured, unittest.TestCase): + """ + More of an integration test, to see if sftp works + """ def __init__(self, *args, **kwargs): unittest.TestCase.__init__(self, *args, **kwargs) - def realmFactory(self): self.rootdir = filepath.FilePath(self.mktemp()) self.rootdir.createDirectory() - return server.ChrootedSSHRealm(self.rootdir.path) - + return essftp.EssFTPRealm(self.rootdir.path) def setUp(self): TestSecured.setUp(self) @@ -574,8 +376,9 @@ def setUp(self): fp.create() fp.setContent(fp.path) - execCommand(self.ssht, "sftp -oPort=%d localhost" % self.port) - + options = "-oUserKnownHostsFile=/dev/null -oStrictHostKeyChecking=no" + execCommand(self.ssht, "sftp %s -oPort=%d localhost" % ( + options, self.port)) def test_SFTPSubsystemExists(self): """ @@ -585,7 +388,6 @@ def test_SFTPSubsystemExists(self): self.ssht.finish() return self.ssht.deferred - def _ls_tester(self, path): """ Tests ls @@ -605,14 +407,12 @@ def compare(data): self.ssht.finish() return self.ssht.deferred - def test_lsWorks(self): """ Make sure ls works correctly """ return self._ls_tester(".") - def test_chrooted(self): """ Make sure that it is chrooted by trying to ls the root diff --git a/ess/test/test_filepath.py b/ess/test/test_filepath.py new file mode 100644 index 0000000..bc6577b --- /dev/null +++ b/ess/test/test_filepath.py @@ -0,0 +1,160 @@ +""" +Tests for the overriden FilePath class +""" +import os +from twisted.test import test_paths + +from ess import filepath + + +class TestFilePath(test_paths.FilePathTestCase): + + def setUpLinks(self): + test_paths.FilePathTestCase.setUp(self) + os.symlink(self.path.child("sub1").path, self._mkpath("sub1.link")) + os.symlink(self.path.child("sub1").child("file2").path, + self._mkpath("file2.link")) + self.all.sort() + + def test_openWithModeAndFlags(self): + """ + Verify that passing both modes and flags will raise an error + """ + self.path = filepath.FilePath(self.path.path) + self.assertRaises(ValueError, self.path.child('file1').open, + mode='w+', flags=os.O_RDWR) + + def test_openWithFlags_Nonexisting(self): + """ + Verify that nonexistant file opened without the create flag will fail + """ + self.path = filepath.FilePath(self.path.path) + created = self.path.child('createdFile') + self.assertRaises((OSError, IOError), created.open, flags=os.O_RDWR) + + def test_openWithFlags_NoFlags(self): + """ + Verify that file opened with no read/write flags will default to + reading + """ + self.path = filepath.FilePath(self.path.path) + created = self.path.child('createdFile') + f = created.open(flags=os.O_CREAT) + self.failUnless(created.exists()) + self.assertEquals(f.read(), '') + f.close() + + def test_openWithFlags_CannotRead(self): + """ + Verify that file opened write only will not be readable. + """ + self.path = filepath.FilePath(self.path.path) + f = self.path.child("file1").open(flags=os.O_WRONLY) + f.write('writeonly') + self.assertRaises((OSError, IOError), f.read) + f.close() + + def test_openWithFlags_CannotWrite(self): + """ + Verify that file opened with no write can be read from but not + written to + """ + self.path = filepath.FilePath(self.path.path) + f = self.path.child("file1").open(flags=os.O_APPEND) + self.assertEquals(f.read(), 'file 1') + self.assertRaises((OSError, IOError), f.write, "append") + f.close() + + def test_openWithFlags_AppendWrite(self): + """ + Verify that file opened with the append flag and the writeonly + flag can be appended to but not overwritten, and not read from + """ + self.path = filepath.FilePath(self.path.path) + f = self.path.child("file1").open(flags=os.O_WRONLY | os.O_APPEND) + self.assertRaises((OSError, IOError), f.read) + f.write('append') + f.seek(0) + f.write('append2') + f.close() + f = self.path.child("file1").open() + self.assertEquals(f.read(), self.f1content + 'appendappend2') + f.close() + + def test_openWithFlags_AppendRead(self): + """ + Verify that file opened with readwrite and append is both appendable + to and readable from + """ + self.path = filepath.FilePath(self.path.path) + f = self.path.child("file1").open(flags=os.O_RDWR | os.O_APPEND) + f.write('append') + f.seek(0) + f.write('append2') + f.seek(0) + self.assertEquals(f.read(), self.f1content + 'appendappend2') + f.close() + + def test_openWithFlags_ReadWrite_NoTruncate(self): + """ + Verify that file opened readwrite is both readable and writable. + """ + self.path = filepath.FilePath(self.path.path) + created = self.subfile("createdFile") + created.write("0000000000000000") + created.close() + created = self.path.child("createdFile") + f = created.open(flags=os.O_RDWR) + f.write('readwrite') + f.seek(0) + self.assertEquals(f.read(), 'readwrite0000000') + f.close() + + def test_openWithFlags_Truncate(self): + """ + Verify that file called with truncate will be overwritten + """ + self.path = filepath.FilePath(self.path.path) + f = self.path.child("file1").open(flags=os.O_RDWR | os.O_TRUNC) + f.write('overwrite') + f.seek(0) + self.assertEquals(f.read(), 'overwrite') + f.close() + + def test_openWithFlags_Exclusive(self): + """ + Verify that file opened with the exclusive flag will raise an error + """ + self.path = filepath.FilePath(self.path.path) + self.assertRaises((OSError, IOError), self.path.child("file1").open, + flags=(os.O_RDWR | os.O_CREAT | os.O_EXCL)) + + def test_walk(self): # isn't a replacement exactly + self.setUpLinks() + self.path = filepath.FilePath(self.path.path) + test_paths.FilePathTestCase.test_walk(self) + + def testRealpath(self): + """ + Verify that a symlink is correctly normalized + """ + self.setUpLinks() + self.path = filepath.FilePath(self.path.path) + self.assertEquals(self.path.child("sub1.link").realpath(), + self.path.child("sub1")) + self.assertEquals(self.path.child("sub1").realpath(), + self.path.child("sub1")) + + def testStatCache(self): + self.setUpLinks() + self.path = filepath.FilePath(self.path.path) + test_paths.FilePathTestCase.testStatCache(self) + sub = self.path.child("sub1") + sublink = self.path.child("sub1.link") + sub.restat() + sublink.restat(followLink=False) + self.assertNotEquals(sub.statinfo, sublink.statinfo) + sublink.restat() + self.assertEquals(sub.statinfo, sublink.statinfo) + sub.restat(followLink=False) + self.assertEquals(sub.statinfo, sublink.statinfo) diff --git a/csftp/test/test_openSSHConfig.py b/ess/test/test_openSSHConfig.py similarity index 100% rename from csftp/test/test_openSSHConfig.py rename to ess/test/test_openSSHConfig.py diff --git a/csftp/test/test_shelless.py b/ess/test/test_shelless.py similarity index 91% rename from csftp/test/test_shelless.py rename to ess/test/test_shelless.py index 6b1b316..bf257cc 100644 --- a/csftp/test/test_shelless.py +++ b/ess/test/test_shelless.py @@ -2,19 +2,19 @@ Note - this test actually fails - runShelless in scratch works. """ -from csftp import shelless -from csftp import _openSSHConfig - import os +from twisted.conch.manhole_ssh import ConchFactory +from twisted.cred import portal, credentials, checkers +from twisted.internet import reactor, defer, protocol from twisted.python.filepath import FilePath from twisted.trial import unittest -from twisted.internet import reactor, defer, protocol -from twisted.cred import portal, credentials, checkers -from twisted.conch.manhole_ssh import ConchFactory from zope.interface import implements +from ess import shelless +from ess import _openSSHConfig + def execCommand(process, command): args = command.split() @@ -22,7 +22,6 @@ def execCommand(process, command): return process.deferred - class AlwaysAllow: implements(checkers.ICredentialsChecker) credentialInterfaces = (credentials.IUsernamePassword, @@ -32,7 +31,6 @@ def requestAvatarId(self, credentials): return defer.succeed(credentials.username) - class SSHTester(protocol.ProcessProtocol): def __init__(self): @@ -42,7 +40,6 @@ def __init__(self): self.connected = False self.queue = [] - def connectionMade(self): self.connected = True for item in self.queue: @@ -51,7 +48,6 @@ def connectionMade(self): else: self.write(item) - def write(self, data): if not self.connected: self.queue.append(data) @@ -61,22 +57,18 @@ def write(self, data): self.error = "" self.transport.write(data) - def finish(self): if not self.connected: self.queue.append(None) else: self.transport.closeStdin() - def outReceived(self, data): self.data += data - def errReceived(self, data): self.error += data - def processEnded(self, reason): self.error = "\n".join( [x for x in self.error.split("\n") @@ -87,7 +79,6 @@ def processEnded(self, reason): self.deferred.callback(self.data) - class TestTester(unittest.TestCase): """ Make sure I actually know what I'm doing here - test against a regular @@ -106,8 +97,10 @@ class MyPP(protocol.ProcessProtocol): def __init__(self): self.readyDeferred = defer.Deferred() self.deferred = defer.Deferred() + def processEnded(self, reason): self.deferred.callback("None") + def errReceived(self, data): # because openSSH prints on stderr if "Server listening" in data: self.readyDeferred.callback("Ready") @@ -117,11 +110,9 @@ def errReceived(self, data): # because openSSH prints on stderr "/usr/sbin/sshd %s" % (self.serverOptions,)) self.ssht = SSHTester() - def tearDown(self): return self.pp.deferred - def test_regSSH(self): def printstuff(stuff): print stuff @@ -133,7 +124,6 @@ def runSSH(ignore): self.ssht.deferred.addCallback(self.assertEqual, "Hello\n") return self.ssht.deferred - def test_regSFTP(self): def runSFTP(ignore): execCommand(self.ssht, "sftp %s" % self.clientOptions) @@ -144,13 +134,11 @@ def runSFTP(ignore): return self.ssht.deferred - class TestSecured: def realmFactory(self): raise NotImplementedError - def setUp(self): """ Starts a shelless SSH server. Subclasses need to specify @@ -165,37 +153,33 @@ def setUp(self): self.server = reactor.listenTCP(self.port, f) self.ssht = SSHTester() - def tearDown(self): return defer.maybeDeferred(self.server.stopListening) - class TestShelllessSSH(TestSecured, unittest.TestCase): def __init__(self, *args, **kwargs): unittest.TestCase.__init__(self, *args, **kwargs) - + self.options = ("-oUserKnownHostsFile=/dev/null " + "-oStrictHostKeyChecking=no " + "-oLogLevel=ERROR") def realmFactory(self): return shelless.ShelllessSSHRealm() - def _checkFailure(self, failure): - error = failure.value expected = 'does not provide shells or allow command execution' - print error - self.assertTrue(expected in error.value or expected in error.data) - + self.assertEqual(expected, failure.getErrorMessage()) def test_noshell(self): - d = execCommand(self.ssht, "ssh -p %d localhost" % self.port) + d = execCommand(self.ssht, "ssh %s -p %d localhost" % + (self.options, self.port)) d.addErrback(self._checkFailure) return d - def test_noexec(self): d = execCommand(self.ssht, - 'ssh -p %d localhost "ls"' % self.port) + 'ssh %s -p %d localhost "ls"' % (self.options, self.port)) d.addErrback(self._checkFailure) return d diff --git a/twisted/plugins/csftp_plugin.py b/twisted/plugins/essftp_plugin.py similarity index 85% rename from twisted/plugins/csftp_plugin.py rename to twisted/plugins/essftp_plugin.py index a70a4b1..87da3e5 100644 --- a/twisted/plugins/csftp_plugin.py +++ b/twisted/plugins/essftp_plugin.py @@ -1,16 +1,16 @@ -from csftp import server - -from zope.interface import implements - -from twisted.internet import defer -from twisted.python import usage -from twisted.plugin import IPlugin from twisted.application.service import IServiceMaker from twisted.application import internet from twisted.conch.checkers import SSHPublicKeyDatabase from twisted.conch.openssh_compat.factory import OpenSSHFactory from twisted.conch.manhole_ssh import ConchFactory from twisted.cred import credentials, checkers, portal, strcred +from twisted.internet import defer +from twisted.python import usage +from twisted.plugin import IPlugin + +from zope.interface import implements + +from ess import essftp class AlwaysAllow(object): @@ -23,11 +23,11 @@ def requestAvatarId(self, credentials): class Options(usage.Options, strcred.AuthOptionMixin): synopsis = "[options]" - longdesc = ("Makes a Chrooted SFTP Server. If --root is not passed as a " + longdesc = ("Makes an EssFTP essftp. If --root is not passed as a " "parameter, uses the current working directory. If no auth service " "is specified, it will allow anyone in.") optParameters = [ - ["root", "r", './', "Root directory"], + ["root", "r", './', "Root directory, as seen by clients"], ["port", "p", "8888", "Port on which to listen"], ["keyDirectory", "k", None, "Directory to look for host keys in. " "If this is not provided, fake keys will be used."], @@ -41,10 +41,10 @@ class Options(usage.Options, strcred.AuthOptionMixin): }) -class CSFTPServiceMaker(object): +class EssFTPServiceMaker(object): implements(IServiceMaker, IPlugin) - tapname = "csftp" - description = "Chrooted SFTP Server" + tapname = "essftp" + description = "EssFTP Server (SFTP server, without the shell)" options = Options def makeService(self, options): @@ -52,7 +52,7 @@ def makeService(self, options): Construct a TCPServer from a factory defined in myproject. """ _portal = portal.Portal( - server.ChrootedSSHRealm(server.FilePath(options['root']).path), + essftp.EssRealm(essftp.FilePath(options['root']).path), options.get('credCheckers', [SSHPublicKeyDatabase()])) if options['keyDirectory']: @@ -71,4 +71,4 @@ def makeService(self, options): # The name of this variable is irrelevant, as long as there is *some* # name bound to a provider of IPlugin and IServiceMaker. -serviceMaker = CSFTPServiceMaker() +serviceMaker = EssFTPServiceMaker() From 3b08e7ce1402a0b4b8c7bee1d289438f4c657989 Mon Sep 17 00:00:00 2001 From: cyli Date: Tue, 3 Jul 2012 00:26:42 -0700 Subject: [PATCH 2/3] Fix plugin rename problems, make scratch files work again --- scratch/runSFTP.py | 14 +++++++------- scratch/runShelless.py | 17 ++++------------- twisted/plugins/essftp_plugin.py | 2 +- 3 files changed, 12 insertions(+), 21 deletions(-) diff --git a/scratch/runSFTP.py b/scratch/runSFTP.py index 235975f..3a35fca 100644 --- a/scratch/runSFTP.py +++ b/scratch/runSFTP.py @@ -1,12 +1,13 @@ -from csftp import server - from twisted.conch.manhole_ssh import ConchFactory -from zope import interface -from twisted.internet import defer from twisted.cred import credentials, checkers, portal -from twisted.internet import reactor +from twisted.internet import defer, reactor from twisted.python.log import startLogging +from zope import interface + +from ess import essftp + + class AlwaysAllow(object): credentialInterfaces = credentials.IUsernamePassword, interface.implements(checkers.ICredentialsChecker) @@ -15,9 +16,8 @@ def requestAvatarId(self, credentials): return defer.succeed(credentials.username) -p = portal.Portal(server.ChrootedSSHRealm('TEMP/ROOT')) +p = portal.Portal(essftp.EssFTPRealm('TEMP/ROOT')) p.registerChecker(AlwaysAllow()) reactor.listenTCP(2222, ConchFactory(p)) startLogging(open('log.txt', 'w+')) reactor.run() - diff --git a/scratch/runShelless.py b/scratch/runShelless.py index b33a930..0837509 100644 --- a/scratch/runShelless.py +++ b/scratch/runShelless.py @@ -1,28 +1,25 @@ -from csftp import shelless - import sys -from twisted.conch.ssh import transport, userauth, connection, channel, common from twisted.conch.manhole_ssh import ConchFactory -from twisted.internet import defer +from twisted.conch.ssh import transport, userauth, connection, channel, common from twisted.cred import credentials, checkers, portal -from twisted.internet import protocol, reactor +from twisted.internet import defer, protocol, reactor from twisted.python import log from zope import interface +from ess import shelless + class ClientTransport(transport.SSHClientTransport): def verifyHostKey(self, pubKey, fingerprint): return defer.succeed(1) - def connectionSecure(self): self.requestService(ClientUserAuth('cyli', ClientConnection())) - class ClientUserAuth(userauth.SSHUserAuthClient): def getPassword(self, prompt=None): @@ -36,14 +33,12 @@ def getPassword(self, prompt=None): # # set pr when testing client - class ClientConnection(connection.SSHConnection): def serviceStarted(self): self.openChannel(CatChannel(conn=self)) - class CatChannel(channel.SSHChannel): name = 'session' @@ -52,25 +47,21 @@ def channelOpen(self, data): self.catData = data self.conn.sendRequest(self, 'exec', common.NS('ls'), wantReply=1) - def dataReceived(self, data): self.catData += data - def closed(self): print 'We got this from "ls":', self.catData self.loseConnection() reactor.stop() - def testClient(host, port): protocol.ClientCreator(reactor, ClientTransport).connectTCP(host, port) log.startLogging(sys.stdout) reactor.run() - class AlwaysAllow: credentialInterfaces = credentials.IUsernamePassword, interface.implements(checkers.ICredentialsChecker) diff --git a/twisted/plugins/essftp_plugin.py b/twisted/plugins/essftp_plugin.py index 87da3e5..598c6b8 100644 --- a/twisted/plugins/essftp_plugin.py +++ b/twisted/plugins/essftp_plugin.py @@ -52,7 +52,7 @@ def makeService(self, options): Construct a TCPServer from a factory defined in myproject. """ _portal = portal.Portal( - essftp.EssRealm(essftp.FilePath(options['root']).path), + essftp.EssFTPRealm(essftp.FilePath(options['root']).path), options.get('credCheckers', [SSHPublicKeyDatabase()])) if options['keyDirectory']: From 2f0bc83f06dddf1168aada2b2321b449590abba2 Mon Sep 17 00:00:00 2001 From: cyli Date: Tue, 3 Jul 2012 00:45:32 -0700 Subject: [PATCH 3/3] Fix openssh_config test, and add items to .gitignore --- .gitignore | 2 ++ ess/test/test_openSSHConfig.py | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 0941d06..95fe102 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ *.pyc *_trial_temp *coverage_html_report +*.coverage +*dropin.cache diff --git a/ess/test/test_openSSHConfig.py b/ess/test/test_openSSHConfig.py index c3d157f..bff3e8d 100644 --- a/ess/test/test_openSSHConfig.py +++ b/ess/test/test_openSSHConfig.py @@ -1,14 +1,15 @@ -import openSSHConfig +from ess import _openSSHConfig as openSSHConfig + from twisted.trial import unittest from twisted.python.filepath import FilePath + class TestOpenSSHConfig(unittest.TestCase): def setUp(self): self.directory = FilePath(self.mktemp()) self.directory.createDirectory() - def test_files(self): openSSHConfig.setupConfig(self.directory.path, 2222) for file in self.directory.children(): @@ -18,7 +19,6 @@ def test_files(self): self.assertTrue("%" not in contents) self.assertEquals(len(self.directory.children()), 5) - def test_commandOptions(self): for option in openSSHConfig.setupConfig(self.directory.path, 2222): self.assertTrue("%" not in option)