diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | NEWS | 81 | ||||
-rw-r--r-- | README | 1 | ||||
-rwxr-xr-x | demos/demo.py | 1 | ||||
-rw-r--r-- | demos/forward.py | 4 | ||||
-rw-r--r-- | paramiko/__init__.py | 2 | ||||
-rw-r--r-- | paramiko/_winapi.py | 269 | ||||
-rw-r--r-- | paramiko/agent.py | 17 | ||||
-rw-r--r-- | paramiko/channel.py | 24 | ||||
-rw-r--r-- | paramiko/client.py | 26 | ||||
-rw-r--r-- | paramiko/config.py | 190 | ||||
-rw-r--r-- | paramiko/file.py | 4 | ||||
-rw-r--r-- | paramiko/hostkeys.py | 18 | ||||
-rw-r--r-- | paramiko/message.py | 3 | ||||
-rw-r--r-- | paramiko/packet.py | 24 | ||||
-rw-r--r-- | paramiko/sftp_client.py | 130 | ||||
-rw-r--r-- | paramiko/sftp_file.py | 25 | ||||
-rw-r--r-- | paramiko/transport.py | 5 | ||||
-rw-r--r-- | paramiko/win_pageant.py | 81 | ||||
-rw-r--r-- | requirements.txt | 2 | ||||
-rw-r--r-- | setup.py | 2 | ||||
-rwxr-xr-x | tests/test_sftp.py | 45 | ||||
-rw-r--r-- | tests/test_util.py | 128 | ||||
-rw-r--r-- | tox.ini | 6 |
24 files changed, 862 insertions, 227 deletions
@@ -1,6 +1,7 @@ *.pyc build/ dist/ +.tox/ paramiko.egg-info/ test.log docs/ @@ -12,6 +12,85 @@ Issues noted as "Fabric #NN" can be found at https://github.com/fabric/fabric/. Releases ======== +v1.10.4 (27th Sep 2013) +----------------------- + +* #179: Fix a missing variable causing errors when an ssh_config file has a + non-default AddressFamily set. Thanks to Ed Marshall & Tomaz Muraus for catch + & patch. +* #200: Fix an exception-causing typo in `demo_simple.py`. Thanks to Alex + Buchanan for catch & Dave Foster for patch. +* #199: Typo fix in the license header cross-project. Thanks to Armin Ronacher + for catch & patch. + +v1.10.3 (20th Sep 2013) +----------------------- + +* #162: Clean up HMAC module import to avoid deadlocks in certain uses of + SSHClient. Thanks to Gernot Hillier for the catch & suggested + fix. +* #36: Fix the port-forwarding demo to avoid file descriptor errors. Thanks to + Jonathan Halcrow for catch & patch. +* #168: Update config handling to properly handle multiple 'localforward' and + 'remoteforward' keys. Thanks to Emre Yılmaz for the patch. + +v1.10.2 (26th Jul 2013) +----------------------- + +* #153, #67: Warn on parse failure when reading known_hosts file. Thanks to + `@glasserc` for patch. +* #146: Indentation fixes for readability. Thanks to Abhinav Upadhyay for catch + & patch. + +v1.10.1 (5th Apr 2013) +---------------------- + +* #142: (Fabric #811) SFTP put of empty file will still return the attributes + of the put file. Thanks to Jason R. Coombs for the patch. +* #154: (Fabric #876) Forwarded SSH agent connections left stale local pipes + lying around, which could cause local (and sometimes remote or network) + resource starvation when running many agent-using remote commands. Thanks to + Kevin Tegtmeier for catch & patch. + +v1.10.0 (1st Mar 2013) +-------------------- + +* #66: Batch SFTP writes to help speed up file transfers. Thanks to Olle + Lundberg for the patch. +* #133: Fix handling of window-change events to be on-spec and not + attempt to wait for a response from the remote sshd; this fixes problems with + less common targets such as some Cisco devices. Thanks to Phillip Heller for + catch & patch. +* #93: Overhaul SSH config parsing to be in line with `man ssh_config` (& the + behavior of `ssh` itself), including addition of parameter expansion within + config values. Thanks to Olle Lundberg for the patch. +* #110: Honor SSH config `AddressFamily` setting when looking up local + host's FQDN. Thanks to John Hensley for the patch. +* #128: Defer FQDN resolution until needed, when parsing SSH config files. + Thanks to Parantapa Bhattacharya for catch & patch. +* #102: Forego random padding for packets when running under `*-ctr` ciphers. + This corrects some slowdowns on platforms where random byte generation is + inefficient (e.g. Windows). Thanks to `@warthog618` for catch & patch, and + Michael van der Kolff for code/technique review. +* #127: Turn `SFTPFile` into a context manager. Thanks to Michael Williamson + for the patch. +* #116: Limit `Message.get_bytes` to an upper bound of 1MB to protect against + potential DoS vectors. Thanks to `@mvschaik` for catch & patch. +* #115: Add convenience `get_pty` kwarg to `Client.exec_command` so users not + manually controlling a channel object can still toggle PTY creation. Thanks + to Michael van der Kolff for the patch. +* #71: Add `SFTPClient.putfo` and `.getfo` methods to allow direct + uploading/downloading of file-like objects. Thanks to Eric Buehl for the + patch. +* #113: Add `timeout` parameter to `SSHClient.exec_command` for easier setting + of the command's internal channel object's timeout. Thanks to Cernov Vladimir + for the patch. +* #94: Remove duplication of SSH port constant. Thanks to Olle Lundberg for the + catch. +* #80: Expose the internal "is closed" property of the file transfer class + `BufferedFile` as `.closed`, better conforming to Python's file interface. + Thanks to `@smunaut` and James Hiscock for catch & patch. + v1.9.0 (6th Nov 2012) --------------------- @@ -286,7 +365,7 @@ v1.5 (paras) 02oct05 separation * demo scripts fixed to have a better chance of loading the host keys correctly on windows/cygwin - + v1.4 (oddish) 17jul05 --------------------- * added SSH-agent support (for posix) from john rochester @@ -8,6 +8,7 @@ paramiko :Copyright: Copyright (c) 2013 Jeff Forcier <jeff@bitprophet.org> :License: LGPL :Homepage: https://github.com/paramiko/paramiko/ +:API docs: http://docs.paramiko.org What diff --git a/demos/demo.py b/demos/demo.py index d5e8a067..aa4bdaa5 100755 --- a/demos/demo.py +++ b/demos/demo.py @@ -26,7 +26,6 @@ import os import select import socket import sys -import threading import time import traceback diff --git a/demos/forward.py b/demos/forward.py index ef01c7b2..5048c775 100644 --- a/demos/forward.py +++ b/demos/forward.py @@ -78,9 +78,11 @@ class Handler (SocketServer.BaseRequestHandler): if len(data) == 0: break self.request.send(data) + + peername = self.request.getpeername() chan.close() self.request.close() - verbose('Tunnel closed from %r' % (self.request.getpeername(),)) + verbose('Tunnel closed from %r' % (peername,)) def forward_tunnel(local_port, remote_host, remote_port, transport): diff --git a/paramiko/__init__.py b/paramiko/__init__.py index 3fd28ae4..b2e1c03e 100644 --- a/paramiko/__init__.py +++ b/paramiko/__init__.py @@ -55,7 +55,7 @@ if sys.version_info < (2, 5): __author__ = "Jeff Forcier <jeff@bitprophet.org>" -__version__ = "1.9.0" +__version__ = "1.10.4" __version_info__ = tuple([ int(d) for d in __version__.split(".") ]) __license__ = "GNU Lesser General Public License (LGPL)" diff --git a/paramiko/_winapi.py b/paramiko/_winapi.py new file mode 100644 index 00000000..f141b005 --- /dev/null +++ b/paramiko/_winapi.py @@ -0,0 +1,269 @@ +""" +Windows API functions implemented as ctypes functions and classes as found +in jaraco.windows (2.10). + +If you encounter issues with this module, please consider reporting the issues +in jaraco.windows and asking the author to port the fixes back here. +""" + +import ctypes +import ctypes.wintypes +import __builtin__ + +###################### +# jaraco.windows.error + +def format_system_message(errno): + """ + Call FormatMessage with a system error number to retrieve + the descriptive error message. + """ + # first some flags used by FormatMessageW + ALLOCATE_BUFFER = 0x100 + ARGUMENT_ARRAY = 0x2000 + FROM_HMODULE = 0x800 + FROM_STRING = 0x400 + FROM_SYSTEM = 0x1000 + IGNORE_INSERTS = 0x200 + + # Let FormatMessageW allocate the buffer (we'll free it below) + # Also, let it know we want a system error message. + flags = ALLOCATE_BUFFER | FROM_SYSTEM + source = None + message_id = errno + language_id = 0 + result_buffer = ctypes.wintypes.LPWSTR() + buffer_size = 0 + arguments = None + bytes = ctypes.windll.kernel32.FormatMessageW( + flags, + source, + message_id, + language_id, + ctypes.byref(result_buffer), + buffer_size, + arguments, + ) + # note the following will cause an infinite loop if GetLastError + # repeatedly returns an error that cannot be formatted, although + # this should not happen. + handle_nonzero_success(bytes) + message = result_buffer.value + ctypes.windll.kernel32.LocalFree(result_buffer) + return message + + +class WindowsError(__builtin__.WindowsError): + "more info about errors at http://msdn.microsoft.com/en-us/library/ms681381(VS.85).aspx" + + def __init__(self, value=None): + if value is None: + value = ctypes.windll.kernel32.GetLastError() + strerror = format_system_message(value) + super(WindowsError, self).__init__(value, strerror) + + @property + def message(self): + return self.strerror + + @property + def code(self): + return self.winerror + + def __str__(self): + return self.message + + def __repr__(self): + return '{self.__class__.__name__}({self.winerror})'.format(**vars()) + +def handle_nonzero_success(result): + if result == 0: + raise WindowsError() + + +##################### +# jaraco.windows.mmap + +CreateFileMapping = ctypes.windll.kernel32.CreateFileMappingW +CreateFileMapping.argtypes = [ + ctypes.wintypes.HANDLE, + ctypes.c_void_p, + ctypes.wintypes.DWORD, + ctypes.wintypes.DWORD, + ctypes.wintypes.DWORD, + ctypes.wintypes.LPWSTR, +] +CreateFileMapping.restype = ctypes.wintypes.HANDLE + +MapViewOfFile = ctypes.windll.kernel32.MapViewOfFile +MapViewOfFile.restype = ctypes.wintypes.HANDLE + +class MemoryMap(object): + """ + A memory map object which can have security attributes overrideden. + """ + def __init__(self, name, length, security_attributes=None): + self.name = name + self.length = length + self.security_attributes = security_attributes + self.pos = 0 + + def __enter__(self): + p_SA = ( + ctypes.byref(self.security_attributes) + if self.security_attributes else None + ) + INVALID_HANDLE_VALUE = -1 + PAGE_READWRITE = 0x4 + FILE_MAP_WRITE = 0x2 + filemap = ctypes.windll.kernel32.CreateFileMappingW( + INVALID_HANDLE_VALUE, p_SA, PAGE_READWRITE, 0, self.length, + unicode(self.name)) + handle_nonzero_success(filemap) + if filemap == INVALID_HANDLE_VALUE: + raise Exception("Failed to create file mapping") + self.filemap = filemap + self.view = MapViewOfFile(filemap, FILE_MAP_WRITE, 0, 0, 0) + return self + + def seek(self, pos): + self.pos = pos + + def write(self, msg): + ctypes.windll.msvcrt.memcpy(self.view + self.pos, msg, len(msg)) + self.pos += len(msg) + + def read(self, n): + """ + Read n bytes from mapped view. + """ + out = ctypes.create_string_buffer(n) + ctypes.windll.msvcrt.memcpy(out, self.view + self.pos, n) + self.pos += n + return out.raw + + def __exit__(self, exc_type, exc_val, tb): + ctypes.windll.kernel32.UnmapViewOfFile(self.view) + ctypes.windll.kernel32.CloseHandle(self.filemap) + +######################### +# jaraco.windows.security + +class TokenInformationClass: + TokenUser = 1 + +class TOKEN_USER(ctypes.Structure): + num = 1 + _fields_ = [ + ('SID', ctypes.c_void_p), + ('ATTRIBUTES', ctypes.wintypes.DWORD), + ] + + +class SECURITY_DESCRIPTOR(ctypes.Structure): + """ + typedef struct _SECURITY_DESCRIPTOR + { + UCHAR Revision; + UCHAR Sbz1; + SECURITY_DESCRIPTOR_CONTROL Control; + PSID Owner; + PSID Group; + PACL Sacl; + PACL Dacl; + } SECURITY_DESCRIPTOR; + """ + SECURITY_DESCRIPTOR_CONTROL = ctypes.wintypes.USHORT + REVISION = 1 + + _fields_ = [ + ('Revision', ctypes.c_ubyte), + ('Sbz1', ctypes.c_ubyte), + ('Control', SECURITY_DESCRIPTOR_CONTROL), + ('Owner', ctypes.c_void_p), + ('Group', ctypes.c_void_p), + ('Sacl', ctypes.c_void_p), + ('Dacl', ctypes.c_void_p), + ] + +class SECURITY_ATTRIBUTES(ctypes.Structure): + """ + typedef struct _SECURITY_ATTRIBUTES { + DWORD nLength; + LPVOID lpSecurityDescriptor; + BOOL bInheritHandle; + } SECURITY_ATTRIBUTES; + """ + _fields_ = [ + ('nLength', ctypes.wintypes.DWORD), + ('lpSecurityDescriptor', ctypes.c_void_p), + ('bInheritHandle', ctypes.wintypes.BOOL), + ] + + def __init__(self, *args, **kwargs): + super(SECURITY_ATTRIBUTES, self).__init__(*args, **kwargs) + self.nLength = ctypes.sizeof(SECURITY_ATTRIBUTES) + + def _get_descriptor(self): + return self._descriptor + def _set_descriptor(self, descriptor): + self._descriptor = descriptor + self.lpSecurityDescriptor = ctypes.addressof(descriptor) + descriptor = property(_get_descriptor, _set_descriptor) + +def GetTokenInformation(token, information_class): + """ + Given a token, get the token information for it. + """ + data_size = ctypes.wintypes.DWORD() + ctypes.windll.advapi32.GetTokenInformation(token, information_class.num, + 0, 0, ctypes.byref(data_size)) + data = ctypes.create_string_buffer(data_size.value) + handle_nonzero_success(ctypes.windll.advapi32.GetTokenInformation(token, + information_class.num, + ctypes.byref(data), ctypes.sizeof(data), + ctypes.byref(data_size))) + return ctypes.cast(data, ctypes.POINTER(TOKEN_USER)).contents + +class TokenAccess: + TOKEN_QUERY = 0x8 + +def OpenProcessToken(proc_handle, access): + result = ctypes.wintypes.HANDLE() + proc_handle = ctypes.wintypes.HANDLE(proc_handle) + handle_nonzero_success(ctypes.windll.advapi32.OpenProcessToken( + proc_handle, access, ctypes.byref(result))) + return result + +def get_current_user(): + """ + Return a TOKEN_USER for the owner of this process. + """ + process = OpenProcessToken( + ctypes.windll.kernel32.GetCurrentProcess(), + TokenAccess.TOKEN_QUERY, + ) + return GetTokenInformation(process, TOKEN_USER) + +def get_security_attributes_for_user(user=None): + """ + Return a SECURITY_ATTRIBUTES structure with the SID set to the + specified user (uses current user if none is specified). + """ + if user is None: + user = get_current_user() + + assert isinstance(user, TOKEN_USER), "user must be TOKEN_USER instance" + + SD = SECURITY_DESCRIPTOR() + SA = SECURITY_ATTRIBUTES() + # by attaching the actual security descriptor, it will be garbage- + # collected with the security attributes + SA.descriptor = SD + SA.bInheritHandle = 1 + + ctypes.windll.advapi32.InitializeSecurityDescriptor(ctypes.byref(SD), + SECURITY_DESCRIPTOR.REVISION) + ctypes.windll.advapi32.SetSecurityDescriptorOwner(ctypes.byref(SD), + user.SID, 0) + return SA diff --git a/paramiko/agent.py b/paramiko/agent.py index e45a9b07..23a5a2e4 100644 --- a/paramiko/agent.py +++ b/paramiko/agent.py @@ -130,15 +130,22 @@ class AgentProxyThread(threading.Thread): if len(data) != 0: self.__inr.send(data) else: + self._close() break elif self.__inr == fd: data = self.__inr.recv(512) if len(data) != 0: self._agent._conn.send(data) else: + self._close() break time.sleep(io_sleep) + def _close(self): + self._exit = True + self.__inr.close() + self._agent._conn.close() + class AgentLocalProxy(AgentProxyThread): """ Class to be used when wanting to ask a local SSH Agent being @@ -248,11 +255,11 @@ class AgentServerProxy(AgentSSH): self.close() def connect(self): - conn_sock = self.__t.open_forward_agent_channel() - if conn_sock is None: - raise SSHException('lost ssh-agent') - conn_sock.set_name('auth-agent') - self._connect(conn_sock) + conn_sock = self.__t.open_forward_agent_channel() + if conn_sock is None: + raise SSHException('lost ssh-agent') + conn_sock.set_name('auth-agent') + self._connect(conn_sock) def close(self): """ diff --git a/paramiko/channel.py b/paramiko/channel.py index 536b5d1c..c680e44b 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -122,7 +122,8 @@ class Channel (object): out += '>' return out - def get_pty(self, term='vt100', width=80, height=24): + def get_pty(self, term='vt100', width=80, height=24, width_pixels=0, + height_pixels=0): """ Request a pseudo-terminal from the server. This is usually used right after creating a client channel, to ask the server to provide some @@ -136,6 +137,10 @@ class Channel (object): @type width: int @param height: height (in characters) of the terminal screen @type height: int + @param width_pixels: width (in pixels) of the terminal screen + @type width_pixels: int + @param height_pixels: height (in pixels) of the terminal screen + @type height_pixels: int @raise SSHException: if the request was rejected or the channel was closed @@ -150,8 +155,8 @@ class Channel (object): m.add_string(term) m.add_int(width) m.add_int(height) - # pixel height, width (usually useless) - m.add_int(0).add_int(0) + m.add_int(width_pixels) + m.add_int(height_pixels) m.add_string('') self._event_pending() self.transport._send_user_message(m) @@ -239,7 +244,7 @@ class Channel (object): self.transport._send_user_message(m) self._wait_for_event() - def resize_pty(self, width=80, height=24): + def resize_pty(self, width=80, height=24, width_pixels=0, height_pixels=0): """ Resize the pseudo-terminal. This can be used to change the width and height of the terminal emulation created in a previous L{get_pty} call. @@ -248,6 +253,10 @@ class Channel (object): @type width: int @param height: new height (in characters) of the terminal screen @type height: int + @param width_pixels: new width (in pixels) of the terminal screen + @type width_pixels: int + @param height_pixels: new height (in pixels) of the terminal screen + @type height_pixels: int @raise SSHException: if the request was rejected or the channel was closed @@ -258,13 +267,12 @@ class Channel (object): m.add_byte(chr(MSG_CHANNEL_REQUEST)) m.add_int(self.remote_chanid) m.add_string('window-change') - m.add_boolean(True) + m.add_boolean(False) m.add_int(width) m.add_int(height) - m.add_int(0).add_int(0) - self._event_pending() + m.add_int(width_pixels) + m.add_int(height_pixels) self.transport._send_user_message(m) - self._wait_for_event() def exit_status_ready(self): """ diff --git a/paramiko/client.py b/paramiko/client.py index 90814590..c5a2d1ac 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -28,6 +28,7 @@ import warnings from paramiko.agent import Agent from paramiko.common import * +from paramiko.config import SSH_PORT from paramiko.dsskey import DSSKey from paramiko.hostkeys import HostKeys from paramiko.resource import ResourceManager @@ -37,8 +38,6 @@ from paramiko.transport import Transport from paramiko.util import retry_on_signal -SSH_PORT = 22 - class MissingHostKeyPolicy (object): """ Interface for defining the policy that L{SSHClient} should use when the @@ -187,8 +186,13 @@ class SSHClient (object): @raise IOError: if the file could not be written """ + + # update local host keys from file (in case other SSH clients + # have written to the known_hosts file meanwhile. + if self.known_hosts is not None: + self.load_host_keys(self.known_hosts) + f = open(filename, 'w') - f.write('# SSH host keys collected by paramiko\n') for hostname, keys in self._host_keys.iteritems(): for keytype, key in keys.iteritems(): f.write('%s %s %s\n' % (hostname, keytype, key.get_base64())) @@ -350,7 +354,7 @@ class SSHClient (object): self._agent.close() self._agent = None - def exec_command(self, command, bufsize=-1): + def exec_command(self, command, bufsize=-1, timeout=None, get_pty=False): """ Execute a command on the SSH server. A new L{Channel} is opened and the requested command is executed. The command's input and output @@ -361,19 +365,25 @@ class SSHClient (object): @type command: str @param bufsize: interpreted the same way as by the built-in C{file()} function in python @type bufsize: int + @param timeout: set command's channel timeout. See L{Channel.settimeout}.settimeout + @type timeout: int @return: the stdin, stdout, and stderr of the executing command @rtype: tuple(L{ChannelFile}, L{ChannelFile}, L{ChannelFile}) @raise SSHException: if the server fails to execute the command """ chan = self._transport.open_session() + if(get_pty): + chan.get_pty() + chan.settimeout(timeout) chan.exec_command(command) stdin = chan.makefile('wb', bufsize) stdout = chan.makefile('rb', bufsize) stderr = chan.makefile_stderr('rb', bufsize) return stdin, stdout, stderr - def invoke_shell(self, term='vt100', width=80, height=24): + def invoke_shell(self, term='vt100', width=80, height=24, width_pixels=0, + height_pixels=0): """ Start an interactive shell session on the SSH server. A new L{Channel} is opened and connected to a pseudo-terminal using the requested @@ -385,13 +395,17 @@ class SSHClient (object): @type width: int @param height: the height (in characters) of the terminal window @type height: int + @param width_pixels: the width (in pixels) of the terminal window + @type width_pixels: int + @param height_pixels: the height (in pixels) of the terminal window + @type height_pixels: int @return: a new channel connected to the remote shell @rtype: L{Channel} @raise SSHException: if the server fails to invoke a shell """ chan = self._transport.open_session() - chan.get_pty(term, width, height) + chan.get_pty(term, width, height, width_pixels, height_pixels) chan.invoke_shell() return chan diff --git a/paramiko/config.py b/paramiko/config.py index 1d629c61..1705de76 100644 --- a/paramiko/config.py +++ b/paramiko/config.py @@ -1,4 +1,5 @@ # Copyright (C) 2006-2007 Robey Pointer <robeypointer@gmail.com> +# Copyright (C) 2012 Olle Lundberg <geek@nerd.sh> # # This file is part of paramiko. # @@ -25,10 +26,64 @@ import os import re import socket -SSH_PORT=22 +SSH_PORT = 22 proxy_re = re.compile(r"^(proxycommand)\s*=*\s*(.*)", re.I) +class LazyFqdn(object): + """ + Returns the host's fqdn on request as string. + """ + + def __init__(self, config, host=None): + self.fqdn = None + self.config = config + self.host = host + + def __str__(self): + if self.fqdn is None: + # + # If the SSH config contains AddressFamily, use that when + # determining the local host's FQDN. Using socket.getfqdn() from + # the standard library is the most general solution, but can + # result in noticeable delays on some platforms when IPv6 is + # misconfigured or not available, as it calls getaddrinfo with no + # address family specified, so both IPv4 and IPv6 are checked. + # + + # Handle specific option + fqdn = None + address_family = self.config.get('addressfamily', 'any').lower() + if address_family != 'any': + try: + family = socket.AF_INET if address_family == 'inet' \ + else socket.AF_INET6 + results = socket.getaddrinfo( + self.host, + None, + family, + socket.SOCK_DGRAM, + socket.IPPROTO_IP, + socket.AI_CANONNAME + ) + for res in results: + af, socktype, proto, canonname, sa = res + if canonname and '.' in canonname: + fqdn = canonname + break + # giaerror -> socket.getaddrinfo() can't resolve self.host + # (which is from socket.gethostname()). Fall back to the + # getfqdn() call below. + except socket.gaierror: + pass + # Handle 'any' / unspecified + if fqdn is None: + fqdn = socket.getfqdn() + # Cache + self.fqdn = fqdn + return self.fqdn + + class SSHConfig (object): """ Representation of config information as stored in the format used by @@ -44,7 +99,7 @@ class SSHConfig (object): """ Create a new OpenSSH config object. """ - self._config = [ { 'host': '*' } ] + self._config = [] def parse(self, file_obj): """ @@ -53,7 +108,7 @@ class SSHConfig (object): @param file_obj: a file-like object to read the config file from @type file_obj: file """ - configs = [self._config[0]] + host = {"host": ['*'], "config": {}} for line in file_obj: line = line.rstrip('\n').lstrip() if (line == '') or (line[0] == '#'): @@ -77,20 +132,21 @@ class SSHConfig (object): value = line[i:].lstrip() if key == 'host': - del configs[:] - # the value may be multiple hosts, space-delimited - for host in value.split(): - # do we have a pre-existing host config to append to? - matches = [c for c in self._config if c['host'] == host] - if len(matches) > 0: - configs.append(matches[0]) - else: - config = { 'host': host } - self._config.append(config) - configs.append(config) - else: - for config in configs: - config[key] = value + self._config.append(host) + value = value.split() + host = {key: value, 'config': {}} + #identityfile, localforward, remoteforward keys are special cases, since they are allowed to be + # specified multiple times and they should be tried in order + # of specification. + + elif key in ['identityfile', 'localforward', 'remoteforward']: + if key in host['config']: + host['config'][key].append(value) + else: + host['config'][key] = [value] + elif key not in host['config']: + host['config'].update({key: value}) + self._config.append(host) def lookup(self, hostname): """ @@ -105,31 +161,45 @@ class SSHConfig (object): will win out. The keys in the returned dict are all normalized to lowercase (look for - C{"port"}, not C{"Port"}. No other processing is done to the keys or - values. + C{"port"}, not C{"Port"}. The values are processed according to the + rules for substitution variable expansion in C{ssh_config}. @param hostname: the hostname to lookup @type hostname: str """ - matches = [x for x in self._config if fnmatch.fnmatch(hostname, x['host'])] - # Move * to the end - _star = matches.pop(0) - matches.append(_star) + + matches = [config for config in self._config if + self._allowed(hostname, config['host'])] + ret = {} - for m in matches: - for k,v in m.iteritems(): - if not k in ret: - ret[k] = v + for match in matches: + for key, value in match['config'].iteritems(): + if key not in ret: + # Create a copy of the original value, + # else it will reference the original list + # in self._config and update that value too + # when the extend() is being called. + ret[key] = value[:] + elif key == 'identityfile': + ret[key].extend(value) ret = self._expand_variables(ret, hostname) - del ret['host'] return ret - def _expand_variables(self, config, hostname ): + def _allowed(self, hostname, hosts): + match = False + for host in hosts: + if host.startswith('!') and fnmatch.fnmatch(hostname, host[1:]): + return False + elif fnmatch.fnmatch(hostname, host): + match = True + return match + + def _expand_variables(self, config, hostname): """ Return a dict of config options with expanded substitutions for a given hostname. - Please refer to man ssh_config(5) for the parameters that + Please refer to man C{ssh_config} for the parameters that are replaced. @param config: the config for the hostname @@ -139,7 +209,7 @@ class SSHConfig (object): """ if 'hostname' in config: - config['hostname'] = config['hostname'].replace('%h',hostname) + config['hostname'] = config['hostname'].replace('%h', hostname) else: config['hostname'] = hostname @@ -155,34 +225,42 @@ class SSHConfig (object): remoteuser = user host = socket.gethostname().split('.')[0] - fqdn = socket.getfqdn() + fqdn = LazyFqdn(config, host) homedir = os.path.expanduser('~') - replacements = { - 'controlpath': [ - ('%h', config['hostname']), - ('%l', fqdn), - ('%L', host), - ('%n', hostname), - ('%p', port), - ('%r', remoteuser), - ('%u', user) - ], - 'identityfile': [ - ('~', homedir), - ('%d', homedir), - ('%h', config['hostname']), - ('%l', fqdn), - ('%u', user), - ('%r', remoteuser) - ], - 'proxycommand': [ - ('%h', config['hostname']), - ('%p', port), - ('%r', remoteuser), - ], - } + replacements = {'controlpath': + [ + ('%h', config['hostname']), + ('%l', fqdn), + ('%L', host), + ('%n', hostname), + ('%p', port), + ('%r', remoteuser), + ('%u', user) + ], + 'identityfile': + [ + ('~', homedir), + ('%d', homedir), + ('%h', config['hostname']), + ('%l', fqdn), + ('%u', user), + ('%r', remoteuser) + ], + 'proxycommand': + [ + ('%h', config['hostname']), + ('%p', port), + ('%r', remoteuser) + ] + } + for k in config: if k in replacements: for find, replace in replacements[k]: + if isinstance(config[k], list): + for item in range(len(config[k])): + config[k][item] = config[k][item].\ + replace(find, str(replace)) + else: config[k] = config[k].replace(find, str(replace)) return config diff --git a/paramiko/file.py b/paramiko/file.py index 17298eee..5fd81cfe 100644 --- a/paramiko/file.py +++ b/paramiko/file.py @@ -354,6 +354,10 @@ class BufferedFile (object): """ return self + @property + def closed(self): + return self._closed + ### overrides... diff --git a/paramiko/hostkeys.py b/paramiko/hostkeys.py index a3ad2ed6..f967a3da 100644 --- a/paramiko/hostkeys.py +++ b/paramiko/hostkeys.py @@ -28,6 +28,7 @@ import UserDict from paramiko.common import * from paramiko.dsskey import DSSKey from paramiko.rsakey import RSAKey +from paramiko.util import get_logger class InvalidHostKey(Exception): @@ -48,7 +49,7 @@ class HostKeyEntry: self.hostnames = hostnames self.key = key - def from_line(cls, line): + def from_line(cls, line, lineno=None): """ Parses the given line of text to find the names for the host, the type of key, and the key data. The line is expected to be in the @@ -61,9 +62,12 @@ class HostKeyEntry: @param line: a line from an OpenSSH known_hosts file @type line: str """ + log = get_logger('paramiko.hostkeys') fields = line.split(' ') if len(fields) < 3: # Bad number of fields + log.info("Not enough fields found in known_hosts in line %s (%r)" % + (lineno, line)) return None fields = fields[:3] @@ -78,6 +82,7 @@ class HostKeyEntry: elif keytype == 'ssh-dss': key = DSSKey(data=base64.decodestring(key)) else: + log.info("Unable to handle key of type %s" % (keytype,)) return None except binascii.Error, e: raise InvalidHostKey(line, e) @@ -160,13 +165,18 @@ class HostKeys (UserDict.DictMixin): @raise IOError: if there was an error reading the file """ f = open(filename, 'r') - for line in f: + for lineno, line in enumerate(f): line = line.strip() if (len(line) == 0) or (line[0] == '#'): continue - e = HostKeyEntry.from_line(line) + e = HostKeyEntry.from_line(line, lineno) if e is not None: - self._entries.append(e) + _hostnames = e.hostnames + for h in _hostnames: + if self.check(h, e.key): + e.hostnames.remove(h) + if len(e.hostnames): + self._entries.append(e) f.close() def save(self, filename): diff --git a/paramiko/message.py b/paramiko/message.py index ff4a4a71..c0e8692b 100644 --- a/paramiko/message.py +++ b/paramiko/message.py @@ -110,7 +110,8 @@ class Message (object): @rtype: string """ b = self.packet.read(n) - if len(b) < n: + max_pad_size = 1<<20 # Limit padding to 1 MB + if len(b) < n and n < max_pad_size: return b + '\x00' * (n - len(b)) return b diff --git a/paramiko/packet.py b/paramiko/packet.py index 66be9962..3f85d668 100644 --- a/paramiko/packet.py +++ b/paramiko/packet.py @@ -33,17 +33,13 @@ from paramiko.ssh_exception import SSHException, ProxyCommandFailure from paramiko.message import Message -got_r_hmac = False try: - import r_hmac - got_r_hmac = True + from r_hmac import HMAC except ImportError: - pass + from Crypto.Hash.HMAC import HMAC + def compute_hmac(key, message, digest_class): - if got_r_hmac: - return r_hmac.HMAC(key, message, digest_class).digest() - from Crypto.Hash import HMAC - return HMAC.HMAC(key, message, digest_class).digest() + return HMAC(key, message, digest_class).digest() class NeedRekeyException (Exception): @@ -87,6 +83,7 @@ class Packetizer (object): self.__mac_size_in = 0 self.__block_engine_out = None self.__block_engine_in = None + self.__sdctr_out = False self.__mac_engine_out = None self.__mac_engine_in = None self.__mac_key_out = '' @@ -110,11 +107,12 @@ class Packetizer (object): """ self.__logger = log - def set_outbound_cipher(self, block_engine, block_size, mac_engine, mac_size, mac_key): + def set_outbound_cipher(self, block_engine, block_size, mac_engine, mac_size, mac_key, sdctr=False): """ Switch outbound data cipher. """ self.__block_engine_out = block_engine + self.__sdctr_out = sdctr self.__block_size_out = block_size self.__mac_engine_out = mac_engine self.__mac_size_out = mac_size @@ -490,12 +488,12 @@ class Packetizer (object): padding = 3 + bsize - ((len(payload) + 8) % bsize) packet = struct.pack('>IB', len(payload) + padding + 1, padding) packet += payload - if self.__block_engine_out is not None: - packet += rng.read(padding) - else: - # cute trick i caught openssh doing: if we're not encrypting, + if self.__sdctr_out or self.__block_engine_out is None: + # cute trick i caught openssh doing: if we're not encrypting or SDCTR mode (RFC4344), # don't waste random bytes for the padding packet += (chr(0) * padding) + else: + packet += rng.read(padding) return packet def _trigger_rekey(self): diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py index 89327e0e..d9215743 100644 --- a/paramiko/sftp_client.py +++ b/paramiko/sftp_client.py @@ -198,7 +198,7 @@ class SFTPClient (BaseSFTP): Open a file on the remote server. The arguments are the same as for python's built-in C{file} (aka C{open}). A file-like object is returned, which closely mimics the behavior of a normal python file - object. + object, including the ability to be used as a context manager. The mode indicates how the file is to be opened: C{'r'} for reading, C{'w'} for writing (truncating an existing file), C{'a'} for appending, @@ -533,6 +533,56 @@ class SFTPClient (BaseSFTP): """ return self._cwd + def putfo(self, fl, remotepath, file_size=0, callback=None, confirm=True): + """ + Copy the contents of an open file object (C{fl}) to the SFTP server as + C{remotepath}. Any exception raised by operations will be passed through. + + The SFTP operations use pipelining for speed. + + @param fl: opened file or file-like object to copy + @type localpath: object + @param remotepath: the destination path on the SFTP server + @type remotepath: str + @param file_size: optional size parameter passed to callback. If none is + specified, size defaults to 0 + @type file_size: int + @param callback: optional callback function that accepts the bytes + transferred so far and the total bytes to be transferred + (since 1.7.4) + @type callback: function(int, int) + @param confirm: whether to do a stat() on the file afterwards to + confirm the file size (since 1.7.7) + @type confirm: bool + + @return: an object containing attributes about the given file + (since 1.7.4) + @rtype: SFTPAttributes + + @since: 1.4 + """ + fr = self.file(remotepath, 'wb') + fr.set_pipelined(True) + size = 0 + try: + while True: + data = fl.read(32768) + fr.write(data) + size += len(data) + if callback is not None: + callback(size, file_size) + if len(data) == 0: + break + finally: + fr.close() + if confirm: + s = self.stat(remotepath) + if s.st_size != size: + raise IOError('size mismatch in put! %d != %d' % (s.st_size, size)) + else: + s = SFTPAttributes() + return s + def put(self, localpath, remotepath, callback=None, confirm=True): """ Copy a local file (C{localpath}) to the SFTP server as C{remotepath}. @@ -562,29 +612,46 @@ class SFTPClient (BaseSFTP): file_size = os.stat(localpath).st_size fl = file(localpath, 'rb') try: - fr = self.file(remotepath, 'wb') - fr.set_pipelined(True) - size = 0 - try: - while True: - data = fl.read(32768) - if len(data) == 0: - break - fr.write(data) - size += len(data) - if callback is not None: - callback(size, file_size) - finally: - fr.close() + return self.putfo(fl, remotepath, os.stat(localpath).st_size, callback, confirm) finally: fl.close() - if confirm: - s = self.stat(remotepath) - if s.st_size != size: - raise IOError('size mismatch in put! %d != %d' % (s.st_size, size)) - else: - s = SFTPAttributes() - return s + + def getfo(self, remotepath, fl, callback=None): + """ + Copy a remote file (C{remotepath}) from the SFTP server and write to + an open file or file-like object, C{fl}. Any exception raised by + operations will be passed through. This method is primarily provided + as a convenience. + + @param remotepath: opened file or file-like object to copy to + @type remotepath: object + @param fl: the destination path on the local host or open file + object + @type localpath: str + @param callback: optional callback function that accepts the bytes + transferred so far and the total bytes to be transferred + (since 1.7.4) + @type callback: function(int, int) + @return: the number of bytes written to the opened file object + + @since: 1.4 + """ + fr = self.file(remotepath, 'rb') + file_size = self.stat(remotepath).st_size + fr.prefetch() + try: + size = 0 + while True: + data = fr.read(32768) + fl.write(data) + size += len(data) + if callback is not None: + callback(size, file_size) + if len(data) == 0: + break + finally: + fr.close() + return size def get(self, remotepath, localpath, callback=None): """ @@ -603,25 +670,12 @@ class SFTPClient (BaseSFTP): @since: 1.4 """ - fr = self.file(remotepath, 'rb') file_size = self.stat(remotepath).st_size - fr.prefetch() + fl = file(localpath, 'wb') try: - fl = file(localpath, 'wb') - try: - size = 0 - while True: - data = fr.read(32768) - fl.write(data) - size += len(data) - if callback is not None: - callback(size, file_size) - if len(data) == 0: - break - finally: - fl.close() + size = self.getfo(remotepath, fl, callback) finally: - fr.close() + fl.close() s = os.stat(localpath) if s.st_size != size: raise IOError('size mismatch in get! %d != %d' % (s.st_size, size)) diff --git a/paramiko/sftp_file.py b/paramiko/sftp_file.py index a6f02d7d..4ec936d0 100644 --- a/paramiko/sftp_file.py +++ b/paramiko/sftp_file.py @@ -21,6 +21,7 @@ L{SFTPFile} """ from binascii import hexlify +from collections import deque import socket import threading import time @@ -34,6 +35,9 @@ from paramiko.sftp_attr import SFTPAttributes class SFTPFile (BufferedFile): """ Proxy object for a file on the remote server, in client mode SFTP. + + Instances of this class may be used as context managers in the same way + that built-in Python file objects are. """ # Some sftp servers will choke if you send read/write requests larger than @@ -51,6 +55,7 @@ class SFTPFile (BufferedFile): self._prefetch_data = {} self._prefetch_reads = [] self._saved_exception = None + self._reqs = deque() def __del__(self): self._close(async=True) @@ -160,12 +165,14 @@ class SFTPFile (BufferedFile): def _write(self, data): # may write less than requested if it would exceed max packet size chunk = min(len(data), self.MAX_REQUEST_SIZE) - req = self.sftp._async_request(type(None), CMD_WRITE, self.handle, long(self._realpos), str(data[:chunk])) - if not self.pipelined or self.sftp.sock.recv_ready(): - t, msg = self.sftp._read_response(req) - if t != CMD_STATUS: - raise SFTPError('Expected status') - # convert_status already called + self._reqs.append(self.sftp._async_request(type(None), CMD_WRITE, self.handle, long(self._realpos), str(data[:chunk]))) + if not self.pipelined or (len(self._reqs) > 100 and self.sftp.sock.recv_ready()): + while len(self._reqs): + req = self._reqs.popleft() + t, msg = self.sftp._read_response(req) + if t != CMD_STATUS: + raise SFTPError('Expected status') + # convert_status already called return chunk def settimeout(self, timeout): @@ -474,3 +481,9 @@ class SFTPFile (BufferedFile): x = self._saved_exception self._saved_exception = None raise x + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self.close() diff --git a/paramiko/transport.py b/paramiko/transport.py index 0a88fd12..6c42cc27 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -1439,7 +1439,7 @@ class Transport (threading.Thread): break self.clear_to_send_lock.release() if time.time() > start + self.clear_to_send_timeout: - raise SSHException('Key-exchange timed out waiting for key negotiation') + raise SSHException('Key-exchange timed out waiting for key negotiation') try: self._send_message(data) finally: @@ -1885,7 +1885,8 @@ class Transport (threading.Thread): mac_key = self._compute_key('F', mac_engine.digest_size) else: mac_key = self._compute_key('E', mac_engine.digest_size) - self.packetizer.set_outbound_cipher(engine, block_size, mac_engine, mac_size, mac_key) + sdctr = self.local_cipher.endswith('-ctr') + self.packetizer.set_outbound_cipher(engine, block_size, mac_engine, mac_size, mac_key, sdctr) compress_out = self._compression_info[self.local_compression][0] if (compress_out is not None) and ((self.local_compression != 'zlib@openssh.com') or self.authenticated): self._log(DEBUG, 'Switching on outbound compression ...') diff --git a/paramiko/win_pageant.py b/paramiko/win_pageant.py index e86f826d..de1cd64b 100644 --- a/paramiko/win_pageant.py +++ b/paramiko/win_pageant.py @@ -21,28 +21,15 @@ Functions for communicating with Pageant, the basic windows ssh agent program. """ -import os +from __future__ import with_statement + import struct -import tempfile -import mmap +import threading import array import platform import ctypes.wintypes -# if you're on windows, you should have one of these, i guess? -# ctypes is part of standard library since Python 2.5 -_has_win32all = False -_has_ctypes = False -try: - # win32gui is preferred over win32ui to avoid MFC dependencies - import win32gui - _has_win32all = True -except ImportError: - try: - import ctypes - _has_ctypes = True - except ImportError: - pass +from . import _winapi _AGENT_COPYDATA_ID = 0x804e50ba _AGENT_MAX_MSGLEN = 8192 @@ -52,16 +39,7 @@ win32con_WM_COPYDATA = 74 def _get_pageant_window_object(): - if _has_win32all: - try: - hwnd = win32gui.FindWindow('Pageant', 'Pageant') - return hwnd - except win32gui.error: - pass - elif _has_ctypes: - # Return 0 if there is no Pageant window. - return ctypes.windll.user32.FindWindowA('Pageant', 'Pageant') - return None + return ctypes.windll.user32.FindWindowA('Pageant', 'Pageant') def can_talk_to_agent(): @@ -71,9 +49,7 @@ def can_talk_to_agent(): This checks both if we have the required libraries (win32all or ctypes) and if there is a Pageant currently running. """ - if (_has_win32all or _has_ctypes) and _get_pageant_window_object(): - return True - return False + return bool(_get_pageant_window_object()) ULONG_PTR = ctypes.c_uint64 if platform.architecture()[0] == '64bit' else ctypes.c_uint32 class COPYDATASTRUCT(ctypes.Structure): @@ -88,48 +64,39 @@ class COPYDATASTRUCT(ctypes.Structure): ] def _query_pageant(msg): + """ + Communication with the Pageant process is done through a shared + memory-mapped file. + """ hwnd = _get_pageant_window_object() if not hwnd: # Raise a failure to connect exception, pageant isn't running anymore! return None - # Write our pageant request string into the file (pageant will read this to determine what to do) - filename = tempfile.mktemp('.pag') - map_filename = os.path.basename(filename) - - f = open(filename, 'w+b') - f.write(msg ) - # Ensure the rest of the file is empty, otherwise pageant will read this - f.write('\0' * (_AGENT_MAX_MSGLEN - len(msg))) - # Create the shared file map that pageant will use to read from - pymap = mmap.mmap(f.fileno(), _AGENT_MAX_MSGLEN, tagname=map_filename, access=mmap.ACCESS_WRITE) - try: + # create a name for the mmap + map_name = 'PageantRequest%08x' % threading.current_thread().ident + + pymap = _winapi.MemoryMap(map_name, _AGENT_MAX_MSGLEN, + _winapi.get_security_attributes_for_user(), + ) + with pymap: + pymap.write(msg) # Create an array buffer containing the mapped filename - char_buffer = array.array("c", map_filename + '\0') + char_buffer = array.array("c", map_name + '\0') char_buffer_address, char_buffer_size = char_buffer.buffer_info() # Create a string to use for the SendMessage function call - cds = COPYDATASTRUCT(_AGENT_COPYDATA_ID, char_buffer_size, char_buffer_address) + cds = COPYDATASTRUCT(_AGENT_COPYDATA_ID, char_buffer_size, + char_buffer_address) - if _has_win32all: - # win32gui.SendMessage should also allow the same pattern as - # ctypes, but let's keep it like this for now... - response = win32gui.SendMessage(hwnd, win32con_WM_COPYDATA, ctypes.sizeof(cds), ctypes.addressof(cds)) - elif _has_ctypes: - response = ctypes.windll.user32.SendMessageA(hwnd, win32con_WM_COPYDATA, ctypes.sizeof(cds), ctypes.byref(cds)) - else: - response = 0 + response = ctypes.windll.user32.SendMessageA(hwnd, + win32con_WM_COPYDATA, ctypes.sizeof(cds), ctypes.byref(cds)) if response > 0: + pymap.seek(0) datalen = pymap.read(4) retlen = struct.unpack('>I', datalen)[0] return datalen + pymap.read(retlen) return None - finally: - pymap.close() - f.close() - # Remove the file, it was temporary only - os.unlink(filename) - class PageantConnection (object): """ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..75112a23 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +pycrypto +tox @@ -52,7 +52,7 @@ if sys.platform == 'darwin': setup(name = "paramiko", - version = "1.9.0", + version = "1.10.4", description = "SSH2 protocol library", author = "Jeff Forcier", author_email = "jeff@bitprophet.org", diff --git a/tests/test_sftp.py b/tests/test_sftp.py index abf33b30..cc512c18 100755 --- a/tests/test_sftp.py +++ b/tests/test_sftp.py @@ -23,15 +23,15 @@ a real actual sftp server is contacted, and a new folder is created there to do test file operations in (so no existing files will be harmed). """ +from __future__ import with_statement + from binascii import hexlify -import logging import os -import random -import struct +import warnings import sys import threading -import time import unittest +import StringIO import paramiko from stub_sftp import StubServer, StubSFTPServer @@ -188,6 +188,17 @@ class SFTPTest (unittest.TestCase): finally: sftp.remove(FOLDER + '/duck.txt') + def test_3_sftp_file_can_be_used_as_context_manager(self): + """ + verify that an opened file can be used as a context manager + """ + try: + with sftp.open(FOLDER + '/duck.txt', 'w') as f: + f.write(ARTICLE) + self.assertEqual(sftp.stat(FOLDER + '/duck.txt').st_size, 1483) + finally: + sftp.remove(FOLDER + '/duck.txt') + def test_4_append(self): """ verify that a file can be opened for append, and tell() still works. @@ -214,7 +225,7 @@ class SFTPTest (unittest.TestCase): """ f = sftp.open(FOLDER + '/first.txt', 'w') try: - f.write('content!\n'); + f.write('content!\n') f.close() sftp.rename(FOLDER + '/first.txt', FOLDER + '/second.txt') try: @@ -425,7 +436,7 @@ class SFTPTest (unittest.TestCase): self.assertEqual(sftp.readlink(FOLDER + '/link.txt'), 'original.txt') f = sftp.open(FOLDER + '/link.txt', 'r') - self.assertEqual(f.readlines(), [ 'original\n' ]) + self.assertEqual(f.readlines(), ['original\n']) f.close() cwd = sftp.normalize('.') @@ -553,7 +564,6 @@ class SFTPTest (unittest.TestCase): """ verify that get/put work. """ - import os, warnings warnings.filterwarnings('ignore', 'tempnam.*') localname = os.tempnam() @@ -618,7 +628,7 @@ class SFTPTest (unittest.TestCase): try: f = sftp.open(FOLDER + '/unusual.txt', 'wx') self.fail('expected exception') - except IOError, x: + except IOError: pass finally: sftp.unlink(FOLDER + '/unusual.txt') @@ -658,12 +668,12 @@ class SFTPTest (unittest.TestCase): f.close() try: f = sftp.open(FOLDER + '/zero', 'r') - data = f.readv([(0, 12)]) + f.readv([(0, 12)]) f.close() f = sftp.open(FOLDER + '/zero', 'r') f.prefetch() - data = f.read(100) + f.read(100) f.close() finally: sftp.unlink(FOLDER + '/zero') @@ -672,7 +682,6 @@ class SFTPTest (unittest.TestCase): """ verify that get/put work without confirmation. """ - import os, warnings warnings.filterwarnings('ignore', 'tempnam.*') localname = os.tempnam() @@ -684,7 +693,7 @@ class SFTPTest (unittest.TestCase): def progress_callback(x, y): saved_progress.append((x, y)) res = sftp.put(localname, FOLDER + '/bunny.txt', progress_callback, False) - + self.assertEquals(SFTPAttributes().attr, res.attr) f = sftp.open(FOLDER + '/bunny.txt', 'r') @@ -717,3 +726,15 @@ class SFTPTest (unittest.TestCase): finally: sftp.remove(FOLDER + '/append.txt') + def test_putfo_empty_file(self): + """ + Send an empty file and confirm it is sent. + """ + target = FOLDER + '/empty file.txt' + stream = StringIO.StringIO() + try: + attrs = sftp.putfo(stream, target) + # the returned attributes should not be null + self.assertNotEqual(attrs, None) + finally: + sftp.remove(target) diff --git a/tests/test_util.py b/tests/test_util.py index 30b8160c..12677a9b 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -104,23 +104,32 @@ class UtilTest(ParamikoTest): f = cStringIO.StringIO(test_config_file) config = paramiko.util.parse_ssh_config(f) self.assertEquals(config._config, - [ {'identityfile': '~/.ssh/id_rsa', 'host': '*', 'user': 'robey', - 'crazy': 'something dumb '}, - {'host': '*.example.com', 'user': 'bjork', 'port': '3333'}, - {'host': 'spoo.example.com', 'crazy': 'something else'}]) + [{'host': ['*'], 'config': {}}, {'host': ['*'], 'config': {'identityfile': ['~/.ssh/id_rsa'], 'user': 'robey'}}, + {'host': ['*.example.com'], 'config': {'user': 'bjork', 'port': '3333'}}, + {'host': ['*'], 'config': {'crazy': 'something dumb '}}, + {'host': ['spoo.example.com'], 'config': {'crazy': 'something else'}}]) def test_3_host_config(self): global test_config_file f = cStringIO.StringIO(test_config_file) config = paramiko.util.parse_ssh_config(f) + for host, values in { - 'irc.danger.com': {'user': 'robey', 'crazy': 'something dumb '}, - 'irc.example.com': {'user': 'bjork', 'crazy': 'something dumb ', 'port': '3333'}, - 'spoo.example.com': {'user': 'bjork', 'crazy': 'something else', 'port': '3333'} + 'irc.danger.com': {'crazy': 'something dumb ', + 'hostname': 'irc.danger.com', + 'user': 'robey'}, + 'irc.example.com': {'crazy': 'something dumb ', + 'hostname': 'irc.example.com', + 'user': 'robey', + 'port': '3333'}, + 'spoo.example.com': {'crazy': 'something dumb ', + 'hostname': 'spoo.example.com', + 'user': 'robey', + 'port': '3333'} }.items(): values = dict(values, hostname=host, - identityfile=os.path.expanduser("~/.ssh/id_rsa") + identityfile=[os.path.expanduser("~/.ssh/id_rsa")] ) self.assertEquals( paramiko.util.lookup_ssh_host_config(host, config), @@ -151,8 +160,8 @@ class UtilTest(ParamikoTest): # just verify that we can pull out 32 bytes and not get an exception. x = rng.read(32) self.assertEquals(len(x), 32) - - def test_7_host_config_expose_ssh_issue_33(self): + + def test_7_host_config_expose_issue_33(self): test_config_file = """ Host www13.* Port 22 @@ -220,16 +229,16 @@ Host equals-delimited ProxyCommand should perform interpolation on the value """ config = paramiko.util.parse_ssh_config(cStringIO.StringIO(""" -Host * - Port 25 - ProxyCommand host %h port %p - Host specific Port 37 ProxyCommand host %h port %p lol Host portonly Port 155 + +Host * + Port 25 + ProxyCommand host %h port %p """)) for host, val in ( ('foo.com', "host foo.com port 25"), @@ -240,3 +249,94 @@ Host portonly host_config(host, config)['proxycommand'], val ) + + def test_11_host_config_test_negation(self): + test_config_file = """ +Host www13.* !*.example.com + Port 22 + +Host *.example.com !www13.* + Port 2222 + +Host www13.* + Port 8080 + +Host * + Port 3333 + """ + f = cStringIO.StringIO(test_config_file) + config = paramiko.util.parse_ssh_config(f) + host = 'www13.example.com' + self.assertEquals( + paramiko.util.lookup_ssh_host_config(host, config), + {'hostname': host, 'port': '8080'} + ) + + def test_12_host_config_test_proxycommand(self): + test_config_file = """ +Host proxy-with-equal-divisor-and-space +ProxyCommand = foo=bar + +Host proxy-with-equal-divisor-and-no-space +ProxyCommand=foo=bar + +Host proxy-without-equal-divisor +ProxyCommand foo=bar:%h-%p + """ + for host, values in { + 'proxy-with-equal-divisor-and-space' :{'hostname': 'proxy-with-equal-divisor-and-space', + 'proxycommand': 'foo=bar'}, + 'proxy-with-equal-divisor-and-no-space':{'hostname': 'proxy-with-equal-divisor-and-no-space', + 'proxycommand': 'foo=bar'}, + 'proxy-without-equal-divisor' :{'hostname': 'proxy-without-equal-divisor', + 'proxycommand': + 'foo=bar:proxy-without-equal-divisor-22'} + }.items(): + + f = cStringIO.StringIO(test_config_file) + config = paramiko.util.parse_ssh_config(f) + self.assertEquals( + paramiko.util.lookup_ssh_host_config(host, config), + values + ) + + def test_11_host_config_test_identityfile(self): + test_config_file = """ + +IdentityFile id_dsa0 + +Host * +IdentityFile id_dsa1 + +Host dsa2 +IdentityFile id_dsa2 + +Host dsa2* +IdentityFile id_dsa22 + """ + for host, values in { + 'foo' :{'hostname': 'foo', + 'identityfile': ['id_dsa0', 'id_dsa1']}, + 'dsa2' :{'hostname': 'dsa2', + 'identityfile': ['id_dsa0', 'id_dsa1', 'id_dsa2', 'id_dsa22']}, + 'dsa22' :{'hostname': 'dsa22', + 'identityfile': ['id_dsa0', 'id_dsa1', 'id_dsa22']} + }.items(): + + f = cStringIO.StringIO(test_config_file) + config = paramiko.util.parse_ssh_config(f) + self.assertEquals( + paramiko.util.lookup_ssh_host_config(host, config), + values + ) + + def test_12_config_addressfamily_and_lazy_fqdn(self): + """ + Ensure the code path honoring non-'all' AddressFamily doesn't asplode + """ + test_config = """ +AddressFamily inet +IdentityFile something_%l_using_fqdn +""" + config = paramiko.util.parse_ssh_config(cStringIO.StringIO(test_config)) + assert config.lookup('meh') # will die during lookup() if bug regresses diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..6cb80012 --- /dev/null +++ b/tox.ini @@ -0,0 +1,6 @@ +[tox] +envlist = py25,py26,py27 + +[testenv] +commands = pip install --use-mirrors -q -r requirements.txt + python test.py |