[Checkins] SVN: zc.buildoutsftp/branches/jim-dev/ Added support for:

jim cvs-admin at zope.org
Fri Jun 29 15:15:50 UTC 2012


Log message for revision 127193:
  Added support for:
  
  - Global-configuration settings.
  
  - Global known-hosts files.
  
  - Host-specific ssh keys.
  
  Added mock-based tests for unix-like systems.  Unfortunately, these
  tests will fail for Windows and windows support, while present, is
  untested.
  

Changed:
  U   zc.buildoutsftp/branches/jim-dev/README.txt
  U   zc.buildoutsftp/branches/jim-dev/src/zc/buildoutsftp/__init__.py
  U   zc.buildoutsftp/branches/jim-dev/src/zc/buildoutsftp/main.test
  U   zc.buildoutsftp/branches/jim-dev/src/zc/buildoutsftp/tests.py

-=-
Modified: zc.buildoutsftp/branches/jim-dev/README.txt
===================================================================
--- zc.buildoutsftp/branches/jim-dev/README.txt	2012-06-29 14:47:56 UTC (rev 127192)
+++ zc.buildoutsftp/branches/jim-dev/README.txt	2012-06-29 15:15:45 UTC (rev 127193)
@@ -55,10 +55,24 @@
 Status and Change History
 =========================
 
-This package is experimental.  It seems to work based on manual
-testing. I'm still trying to figure out how to write automated tests
-for this.
+This package has been used for years on Linux and Mac OS X.  The
+author doesn't use it on Windows, but, presumably, other people do.
 
+1.0.0 (2012/06/29)
+------------------
+
+Added support for:
+
+- Global-configuration settings.
+
+- Global known-hosts files.
+
+- Host-specific ssh keys.
+
+Added mock-based tests for unix-like systems.  Unfortunately, these
+tests will fail for Windows and windows support, while present, is
+untested.
+
 0.6.1 (2010/03/17)
 ------------------
 

Modified: zc.buildoutsftp/branches/jim-dev/src/zc/buildoutsftp/__init__.py
===================================================================
--- zc.buildoutsftp/branches/jim-dev/src/zc/buildoutsftp/__init__.py	2012-06-29 14:47:56 UTC (rev 127192)
+++ zc.buildoutsftp/branches/jim-dev/src/zc/buildoutsftp/__init__.py	2012-06-29 15:15:45 UTC (rev 127193)
@@ -40,13 +40,18 @@
     '(?:' '([^@:]+)(?::([^@]*))?@' ')?'
     '([^:]*)(?::(\d+))?$').match
 
+def deunixpath(path):
+    return os.path.join(*path.split('/'))
+
 _configs = None
 def _get_config(host):
     global _configs
     if _configs is None:
         _configs = []
-        for path in ('/etc/ssh/ssh_config', '/etc/ssh/ssh_config',
-                     os.path.expanduser('~/.ssh/config')):
+        for path in (
+            deunixpath('/etc/ssh/ssh_config'), deunixpath('/etc/ssh_config'),
+            os.path.expanduser(deunixpath('~/.ssh/config')),
+            ):
             if os.path.exists(path):
                 config = paramiko.SSHConfig()
                 with open(path) as f:
@@ -91,15 +96,15 @@
             host_keys = paramiko.HostKeys(user_host_keys)
         else:
             host_keys = {}
-        global_host_keys = config.get('GlobalKnownHostsFile')
+        global_host_keys = config.get('globalknownhostsfile')
         if not global_host_keys:
-            for path in ('/etc/ssh/GlobalKnownHostsFile',
+            for path in ('/etc/ssh/ssh_known_hosts',
                          '/etc/ssh_known_hosts'):
                 if os.path.exists(path):
                     global_host_keys = path
                     break
         if global_host_keys:
-            host_keys.update(paramiko.HostKeys(user_host_keys))
+            host_keys.update(paramiko.HostKeys(global_host_keys))
         return host_keys
 
 class Result:
@@ -139,6 +144,9 @@
         if trans is not False:
             trans.close()
 
+    global _configs
+    _configs = None
+
 atexit.register(cleanup)
 
 class SFTPHandler(urllib2.BaseHandler):
@@ -188,9 +196,9 @@
                     raise
         else:
             keys = list(paramiko.Agent().get_keys())
-            IdentityFile = config.get('IdentityFile')
+            IdentityFile = config.get('identityfile')
             if IdentityFile:
-                key = _open_key(IdentityFile)
+                key = _open_key(os.path.expanduser(IdentityFile))
                 if key is None:
                     logger.error('IdentityFile, %s, does not exist',
                                  IdentityFile)
@@ -199,6 +207,7 @@
             else:
                 for path in (
                     '~/.ssh/identity', '~/.ssh/id_rsa', '~/.ssh/id_dsa'):
+                    path = deunixpath(path)
                     key = _open_key(os.path.expanduser(path))
                     if key is not None:
                         keys.insert(0, key)

Modified: zc.buildoutsftp/branches/jim-dev/src/zc/buildoutsftp/main.test
===================================================================
--- zc.buildoutsftp/branches/jim-dev/src/zc/buildoutsftp/main.test	2012-06-29 14:47:56 UTC (rev 127192)
+++ zc.buildoutsftp/branches/jim-dev/src/zc/buildoutsftp/main.test	2012-06-29 15:15:45 UTC (rev 127193)
@@ -16,7 +16,7 @@
 A buildout object is passed to install, but install ignores it.
 
     >>> handler = zc.buildoutsftp.SFTPHandler()
-    >>> request = urllib2.Request('sftp://example.com')
+    >>> request = urllib2.Request('sftp://example.com/data')
 
 Let's start with essentially no ssh support in the user's environment:
 
@@ -33,7 +33,8 @@
     >>> host_keys = paramiko.HostKeys()
     >>> host_keys.add('example.com', 'ssh-rsa', host_key)
     >>> os.mkdir('.ssh', 0700)
-    >>> host_keys.save(os.path.join('.ssh', 'known_hosts'))
+    >>> from os.path import join
+    >>> host_keys.save(join('.ssh', 'known_hosts'))
 
     >>> handler.sftp_open(request)
     Traceback (most recent call last):
@@ -45,5 +46,184 @@
 Let's give them one:
 
     >>> ukey = creds[(('example.com', 22), 'testuser')]['user_key']
-    >>> ukey.write_private_key_file(os.path.join('.ssh', 'id_rsa'))
+    >>> ukey.write_private_key_file(join('.ssh', 'id_rsa'))
+    >>> handler.sftp_open(request)
+    ... # doctest: +NORMALIZE_WHITESPACE
+    Traceback (most recent call last):
+    ...
+    AuthenticationException:
+    ('Remote server authentication failed.', 'example.com')
 
+We got a bit farther, but the host key was wrong.  Let's fix it:
+
+    >>> host_key = creds[(('example.com', 22), 'testuser')]['host_key']
+    >>> host_keys = paramiko.HostKeys()
+    >>> host_keys.add('example.com', 'ssh-rsa', host_key)
+    >>> host_keys.save(join('.ssh', 'known_hosts'))
+    >>> print handler.sftp_open(request).read()
+    <a href="sftp://example.com/data/moredata">moredata</a><br />
+    <a href="sftp://example.com/data/index.html">index.html</a><br />
+
+That worked and we got a directory listing.  If we access a file:
+
+    >>> zc.buildoutsftp.cleanup()
+    >>> request = urllib2.Request('sftp://example.com/data/index.html')
+    >>> print handler.sftp_open(request).read(),
+    Hi world!
+
+We get it's text.
+
+Now, let's try some variations:
+
+User key in ~/.ssh/id_dsa
+-------------------------
+
+    >>> zc.buildoutsftp.cleanup()
+    >>> os.remove(join('.ssh', 'id_rsa'))
+    >>> zc.buildoutsftp.cleanup()
+    >>> request = urllib2.Request('sftp://example.com/data')
+    >>> print handler.sftp_open(request).read(),
+    Traceback (most recent call last):
+    ...
+    AuthenticationException: Authentication failed.
+
+    >>> ukey = paramiko.DSSKey.generate(1024)
+    >>> ukey.write_private_key_file(join('.ssh', 'id_dsa'))
+    >>> creds[(('example.com', 22), 'testuser')]['user_key'] = ukey
+    >>> print handler.sftp_open(request).read(),
+    <a href="sftp://example.com/data/moredata">moredata</a><br />
+    <a href="sftp://example.com/data/index.html">index.html</a><br />
+
+User key in ~/.ssh/identity
+---------------------------
+
+    >>> zc.buildoutsftp.cleanup()
+    >>> os.remove(join('.ssh', 'id_dsa'))
+    >>> zc.buildoutsftp.cleanup()
+    >>> request = urllib2.Request('sftp://example.com/data')
+    >>> print handler.sftp_open(request).read(),
+    Traceback (most recent call last):
+    ...
+    AuthenticationException: Authentication failed.
+
+    >>> ukey.write_private_key_file(join('.ssh', 'identity'))
+    >>> print handler.sftp_open(request).read(),
+    <a href="sftp://example.com/data/moredata">moredata</a><br />
+    <a href="sftp://example.com/data/index.html">index.html</a><br />
+
+Host key in global known hosts file
+-----------------------------------
+
+    >>> zc.buildoutsftp.cleanup()
+    >>> os.remove(join('.ssh', 'known_hosts'))
+    >>> zc.buildoutsftp.cleanup()
+    >>> request = urllib2.Request('sftp://example.com/data')
+    >>> print handler.sftp_open(request).read(),
+    Traceback (most recent call last):
+    ...
+    AuthenticationException: ('No stored host key', 'example.com')
+
+    >>> os.mkdir('etc')
+    >>> host_keys.save(join('etc', 'ssh_known_hosts'))
+    >>> print handler.sftp_open(request).read(),
+    <a href="sftp://example.com/data/moredata">moredata</a><br />
+    <a href="sftp://example.com/data/index.html">index.html</a><br />
+
+    >>> zc.buildoutsftp.cleanup()
+    >>> os.remove(join('etc', 'ssh_known_hosts'))
+    >>> zc.buildoutsftp.cleanup()
+    >>> request = urllib2.Request('sftp://example.com/data')
+    >>> print handler.sftp_open(request).read(),
+    Traceback (most recent call last):
+    ...
+    AuthenticationException: ('No stored host key', 'example.com')
+
+    >>> os.mkdir(join('etc', 'ssh'))
+    >>> host_keys.save(join('etc', 'ssh', 'ssh_known_hosts'))
+    >>> print handler.sftp_open(request).read(),
+    <a href="sftp://example.com/data/moredata">moredata</a><br />
+    <a href="sftp://example.com/data/index.html">index.html</a><br />
+
+Host key in global known hosts file with non-standard name
+----------------------------------------------------------
+
+    >>> os.remove(join('etc', 'ssh', 'ssh_known_hosts'))
+    >>> zc.buildoutsftp.cleanup()
+    >>> request = urllib2.Request('sftp://example.com/data')
+    >>> print handler.sftp_open(request).read(),
+    Traceback (most recent call last):
+    ...
+    AuthenticationException: ('No stored host key', 'example.com')
+
+    >>> with open(join('etc', 'ssh_config'), 'w') as f:
+    ...     f.write('GlobalKnownHostsFile %s\n'
+    ...             % join('etc', 'ssh', 'known_hosts'))
+    >>> zc.buildoutsftp.cleanup()
+    >>> host_keys.save(join('etc', 'ssh', 'known_hosts'))
+    >>> print handler.sftp_open(request).read(),
+    <a href="sftp://example.com/data/moredata">moredata</a><br />
+    <a href="sftp://example.com/data/index.html">index.html</a><br />
+
+Alternate global config location
+--------------------------------
+
+    >>> os.remove(join('etc', 'ssh', 'known_hosts'))
+    >>> os.remove(join('etc', 'ssh_config'))
+    >>> zc.buildoutsftp.cleanup()
+    >>> request = urllib2.Request('sftp://example.com/data')
+    >>> print handler.sftp_open(request).read(),
+    Traceback (most recent call last):
+    ...
+    AuthenticationException: ('No stored host key', 'example.com')
+
+    >>> with open(join('etc', 'ssh', 'ssh_config'), 'w') as f:
+    ...     f.write('GlobalKnownHostsFile %s\n'
+    ...             % join('etc', 'KnownHosts'))
+
+    >>> zc.buildoutsftp.cleanup()
+    >>> host_keys.save(join('etc', 'KnownHosts'))
+    >>> print handler.sftp_open(request).read(),
+    <a href="sftp://example.com/data/moredata">moredata</a><br />
+    <a href="sftp://example.com/data/index.html">index.html</a><br />
+
+Host-specific user
+==================
+
+    >>> import getpass
+    >>> getpass.getuser.return_value = 'bob'
+
+    >>> zc.buildoutsftp.cleanup()
+    >>> print handler.sftp_open(request).read(),
+    Traceback (most recent call last):
+    ...
+    AuthenticationException: Authentication failed.
+
+    >>> with open(join('.ssh', 'config'), 'w') as f:
+    ...     f.write('Host example.com\n'
+    ...             'User testuser\n'
+    ...             )
+    >>> zc.buildoutsftp.cleanup()
+    >>> print handler.sftp_open(request).read(),
+    <a href="sftp://example.com/data/moredata">moredata</a><br />
+    <a href="sftp://example.com/data/index.html">index.html</a><br />
+
+Host-specific key file
+======================
+
+    >>> os.remove(join('.ssh', 'identity'))
+    >>> zc.buildoutsftp.cleanup()
+    >>> print handler.sftp_open(request).read(),
+    Traceback (most recent call last):
+    ...
+    AuthenticationException: Authentication failed.
+
+    >>> with open(join('.ssh', 'config'), 'w') as f:
+    ...     f.write('Host example.com\n'
+    ...             'User testuser\n'
+    ...             'IdentityFile ~/.ssh/id\n'
+    ...             )
+    >>> zc.buildoutsftp.cleanup()
+    >>> ukey.write_private_key_file(join('.ssh', 'id'))
+    >>> print handler.sftp_open(request).read(),
+    <a href="sftp://example.com/data/moredata">moredata</a><br />
+    <a href="sftp://example.com/data/index.html">index.html</a><br />

Modified: zc.buildoutsftp/branches/jim-dev/src/zc/buildoutsftp/tests.py
===================================================================
--- zc.buildoutsftp/branches/jim-dev/src/zc/buildoutsftp/tests.py	2012-06-29 14:47:56 UTC (rev 127192)
+++ zc.buildoutsftp/branches/jim-dev/src/zc/buildoutsftp/tests.py	2012-06-29 15:15:45 UTC (rev 127193)
@@ -13,6 +13,7 @@
 ##############################################################################
 from zope.testing import setupstack
 import doctest
+import mimetypes
 import mock
 import os
 import paramiko
@@ -30,6 +31,9 @@
 
 def setup(test):
     globs = test.globs
+
+    mimetypes.init()
+
     setupstack.setUpDirectory(test)
     setupstack.context_manager(test, mock.patch('urllib2.install_opener'))
     setupstack.context_manager(test, mock.patch('urllib2.build_opener'))
@@ -72,15 +76,20 @@
             self.addr = addr
 
         def connect(self, username, pkey):
-            if pkey != creds.get((self.addr, username))['user_key']:
-                raise paramiko.AuthenticationException()
-            self.username = username
+            try:
+                key = creds[(self.addr, username)]['user_key']
+                if pkey == key:
+                    self.username = username
+                    return
+            except KeyError:
+                pass
+            raise paramiko.AuthenticationException()
 
         def close(self):
             pass
 
         def get_remote_server_key(self):
-            return creds.get((self.addr, self.username))['user_key']
+            return creds.get((self.addr, self.username))['host_key']
 
     @side_effect(
         setupstack.context_manager(
@@ -103,10 +112,10 @@
             else:
                 return base
 
-        def stat(path):
+        def stat(self, path):
             return os.stat(self._path(path))
 
-        def listdir(self):
+        def listdir(self, path):
             return os.listdir(self._path(path))
 
         def open(self, path):
@@ -116,7 +125,7 @@
         setupstack.context_manager(
             test, mock.patch('paramiko.SFTPClient.from_transport')))
     def from_transport(trans):
-        return SFTPClient(addr)
+        return SFTPClient(trans)
 
     os.makedirs(os.path.join('example.com:22', 'data', 'moredata'))
     with open(os.path.join('example.com:22', 'data', 'index.html'), 'w') as f:



More information about the checkins mailing list