summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--NEWS81
-rw-r--r--README1
-rwxr-xr-xdemos/demo.py1
-rw-r--r--demos/forward.py4
-rw-r--r--paramiko/__init__.py2
-rw-r--r--paramiko/_winapi.py269
-rw-r--r--paramiko/agent.py17
-rw-r--r--paramiko/channel.py24
-rw-r--r--paramiko/client.py26
-rw-r--r--paramiko/config.py190
-rw-r--r--paramiko/file.py4
-rw-r--r--paramiko/hostkeys.py18
-rw-r--r--paramiko/message.py3
-rw-r--r--paramiko/packet.py24
-rw-r--r--paramiko/sftp_client.py130
-rw-r--r--paramiko/sftp_file.py25
-rw-r--r--paramiko/transport.py5
-rw-r--r--paramiko/win_pageant.py81
-rw-r--r--requirements.txt2
-rw-r--r--setup.py2
-rwxr-xr-xtests/test_sftp.py45
-rw-r--r--tests/test_util.py128
-rw-r--r--tox.ini6
24 files changed, 862 insertions, 227 deletions
diff --git a/.gitignore b/.gitignore
index 5f9c3d74..4b578950 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
*.pyc
build/
dist/
+.tox/
paramiko.egg-info/
test.log
docs/
diff --git a/NEWS b/NEWS
index 55420463..f3d00dd4 100644
--- a/NEWS
+++ b/NEWS
@@ -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
diff --git a/README b/README
index 68e74347..1899819a 100644
--- a/README
+++ b/README
@@ -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
diff --git a/setup.py b/setup.py
index 9ac9b9ad..0202a70e 100644
--- a/setup.py
+++ b/setup.py
@@ -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