Merge branch 'master' into 112-int

Conflicts:
	paramiko/win_pageant.py
This commit is contained in:
Jeff Forcier 2013-03-19 13:36:52 -07:00
commit a7ee2509e4
16 changed files with 344 additions and 116 deletions

1
.gitignore vendored
View File

@ -1,6 +1,7 @@
*.pyc *.pyc
build/ build/
dist/ dist/
.tox/
paramiko.egg-info/ paramiko.egg-info/
test.log test.log
docs/ docs/

33
NEWS
View File

@ -12,9 +12,40 @@ Issues noted as "Fabric #NN" can be found at https://github.com/fabric/fabric/.
Releases Releases
======== ========
v1.10.0 (DD MM YYYY) v1.11.0 (DD MM YYYY)
--------------------
* #100: Remove use of PyWin32 in `win_pageant` module. Module was already
dependent on ctypes for constructing appropriate structures and had ctypes
implementations of all functionality. Thanks to Jason R. Coombs for the
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 * #71: Add `SFTPClient.putfo` and `.getfo` methods to allow direct
uploading/downloading of file-like objects. Thanks to Eric Buehl for the uploading/downloading of file-like objects. Thanks to Eric Buehl for the
patch. patch.

20
README
View File

@ -5,7 +5,7 @@ paramiko
:Paramiko: Python SSH module :Paramiko: Python SSH module
:Copyright: Copyright (c) 2003-2009 Robey Pointer <robeypointer@gmail.com> :Copyright: Copyright (c) 2003-2009 Robey Pointer <robeypointer@gmail.com>
:Copyright: Copyright (c) 2012 Jeff Forcier <jeff@bitprophet.org> :Copyright: Copyright (c) 2013 Jeff Forcier <jeff@bitprophet.org>
:License: LGPL :License: LGPL
:Homepage: https://github.com/paramiko/paramiko/ :Homepage: https://github.com/paramiko/paramiko/
@ -20,7 +20,7 @@ What
---- ----
"paramiko" is a combination of the esperanto words for "paranoid" and "paramiko" is a combination of the esperanto words for "paranoid" and
"friend". it's a module for python 2.2+ that implements the SSH2 protocol "friend". it's a module for python 2.5+ that implements the SSH2 protocol
for secure (encrypted and authenticated) connections to remote machines. for secure (encrypted and authenticated) connections to remote machines.
unlike SSL (aka TLS), SSH2 protocol does not require hierarchical unlike SSL (aka TLS), SSH2 protocol does not require hierarchical
certificates signed by a powerful central authority. you may know SSH2 as certificates signed by a powerful central authority. you may know SSH2 as
@ -39,8 +39,7 @@ that should have come with this archive.
Requirements Requirements
------------ ------------
- python 2.3 or better <http://www.python.org/> - python 2.5 or better <http://www.python.org/>
(python 2.2 is also supported, but not recommended)
- pycrypto 2.1 or better <https://www.dlitz.net/software/pycrypto/> - pycrypto 2.1 or better <https://www.dlitz.net/software/pycrypto/>
If you have setuptools, you can build and install paramiko and all its If you have setuptools, you can build and install paramiko and all its
@ -58,19 +57,6 @@ should also work on Windows, though i don't test it as frequently there.
if you run into Windows problems, send me a patch: portability is important if you run into Windows problems, send me a patch: portability is important
to me. to me.
python 2.2 may work, thanks to some patches from Roger Binns. things to
watch out for:
* sockets in 2.2 don't support timeouts, so the 'select' module is
imported to do polling.
* logging is mostly stubbed out. it works just enough to let paramiko
create log files for debugging, if you want them. to get real logging,
you can backport python 2.3's logging package. Roger has done that
already:
http://sourceforge.net/project/showfiles.php?group_id=75211&package_id=113804
you really should upgrade to python 2.3. laziness is no excuse! :)
some python distributions don't include the utf-8 string encodings, for some python distributions don't include the utf-8 string encodings, for
reasons of space (misdirected as that is). if your distribution is reasons of space (misdirected as that is). if your distribution is
missing encodings, you'll see an error like this:: missing encodings, you'll see an error like this::

View File

@ -18,7 +18,7 @@
""" """
I{Paramiko} (a combination of the esperanto words for "paranoid" and "friend") I{Paramiko} (a combination of the esperanto words for "paranoid" and "friend")
is a module for python 2.3 or greater that implements the SSH2 protocol for is a module for python 2.5 or greater that implements the SSH2 protocol for
secure (encrypted and authenticated) connections to remote machines. Unlike secure (encrypted and authenticated) connections to remote machines. Unlike
SSL (aka TLS), the SSH2 protocol does not require hierarchical certificates SSL (aka TLS), the SSH2 protocol does not require hierarchical certificates
signed by a powerful central authority. You may know SSH2 as the protocol that signed by a powerful central authority. You may know SSH2 as the protocol that
@ -50,8 +50,8 @@ Website: U{https://github.com/paramiko/paramiko/}
import sys import sys
if sys.version_info < (2, 2): if sys.version_info < (2, 5):
raise RuntimeError('You need python 2.2 for this module.') raise RuntimeError('You need python 2.5+ for this module.')
__author__ = "Jeff Forcier <jeff@bitprophet.org>" __author__ = "Jeff Forcier <jeff@bitprophet.org>"

View File

@ -122,7 +122,8 @@ class Channel (object):
out += '>' out += '>'
return 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 Request a pseudo-terminal from the server. This is usually used right
after creating a client channel, to ask the server to provide some after creating a client channel, to ask the server to provide some
@ -136,6 +137,10 @@ class Channel (object):
@type width: int @type width: int
@param height: height (in characters) of the terminal screen @param height: height (in characters) of the terminal screen
@type height: int @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 @raise SSHException: if the request was rejected or the channel was
closed closed
@ -150,8 +155,8 @@ class Channel (object):
m.add_string(term) m.add_string(term)
m.add_int(width) m.add_int(width)
m.add_int(height) m.add_int(height)
# pixel height, width (usually useless) m.add_int(width_pixels)
m.add_int(0).add_int(0) m.add_int(height_pixels)
m.add_string('') m.add_string('')
self._event_pending() self._event_pending()
self.transport._send_user_message(m) self.transport._send_user_message(m)
@ -239,7 +244,7 @@ class Channel (object):
self.transport._send_user_message(m) self.transport._send_user_message(m)
self._wait_for_event() 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 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. height of the terminal emulation created in a previous L{get_pty} call.
@ -248,6 +253,10 @@ class Channel (object):
@type width: int @type width: int
@param height: new height (in characters) of the terminal screen @param height: new height (in characters) of the terminal screen
@type height: int @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 @raise SSHException: if the request was rejected or the channel was
closed closed
@ -258,13 +267,12 @@ class Channel (object):
m.add_byte(chr(MSG_CHANNEL_REQUEST)) m.add_byte(chr(MSG_CHANNEL_REQUEST))
m.add_int(self.remote_chanid) m.add_int(self.remote_chanid)
m.add_string('window-change') m.add_string('window-change')
m.add_boolean(True) m.add_boolean(False)
m.add_int(width) m.add_int(width)
m.add_int(height) m.add_int(height)
m.add_int(0).add_int(0) m.add_int(width_pixels)
self._event_pending() m.add_int(height_pixels)
self.transport._send_user_message(m) self.transport._send_user_message(m)
self._wait_for_event()
def exit_status_ready(self): def exit_status_ready(self):
""" """

View File

@ -349,7 +349,7 @@ class SSHClient (object):
self._agent.close() self._agent.close()
self._agent = None self._agent = None
def exec_command(self, command, bufsize=-1, timeout=None): 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 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 the requested command is executed. The command's input and output
@ -368,6 +368,8 @@ class SSHClient (object):
@raise SSHException: if the server fails to execute the command @raise SSHException: if the server fails to execute the command
""" """
chan = self._transport.open_session() chan = self._transport.open_session()
if(get_pty):
chan.get_pty()
chan.settimeout(timeout) chan.settimeout(timeout)
chan.exec_command(command) chan.exec_command(command)
stdin = chan.makefile('wb', bufsize) stdin = chan.makefile('wb', bufsize)
@ -375,7 +377,8 @@ class SSHClient (object):
stderr = chan.makefile_stderr('rb', bufsize) stderr = chan.makefile_stderr('rb', bufsize)
return stdin, stdout, stderr 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} Start an interactive shell session on the SSH server. A new L{Channel}
is opened and connected to a pseudo-terminal using the requested is opened and connected to a pseudo-terminal using the requested
@ -387,13 +390,17 @@ class SSHClient (object):
@type width: int @type width: int
@param height: the height (in characters) of the terminal window @param height: the height (in characters) of the terminal window
@type height: int @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 @return: a new channel connected to the remote shell
@rtype: L{Channel} @rtype: L{Channel}
@raise SSHException: if the server fails to invoke a shell @raise SSHException: if the server fails to invoke a shell
""" """
chan = self._transport.open_session() chan = self._transport.open_session()
chan.get_pty(term, width, height) chan.get_pty(term, width, height, width_pixels, height_pixels)
chan.invoke_shell() chan.invoke_shell()
return chan return chan

View File

@ -1,4 +1,5 @@
# Copyright (C) 2006-2007 Robey Pointer <robeypointer@gmail.com> # Copyright (C) 2006-2007 Robey Pointer <robeypointer@gmail.com>
# Copyright (C) 2012 Olle Lundberg <geek@nerd.sh>
# #
# This file is part of paramiko. # This file is part of paramiko.
# #
@ -29,6 +30,51 @@ SSH_PORT = 22
proxy_re = re.compile(r"^(proxycommand)\s*=*\s*(.*)", re.I) 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):
self.fqdn = None
self.config = config
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':
family = socket.AF_INET if address_family == 'inet' \
else socket.AF_INET6
results = socket.getaddrinfo(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
# Handle 'any' / unspecified
if fqdn is None:
fqdn = socket.getfqdn()
# Cache
self.fqdn = fqdn
return self.fqdn
class SSHConfig (object): class SSHConfig (object):
""" """
Representation of config information as stored in the format used by Representation of config information as stored in the format used by
@ -44,7 +90,7 @@ class SSHConfig (object):
""" """
Create a new OpenSSH config object. Create a new OpenSSH config object.
""" """
self._config = [ { 'host': '*' } ] self._config = []
def parse(self, file_obj): def parse(self, file_obj):
""" """
@ -53,7 +99,7 @@ class SSHConfig (object):
@param file_obj: a file-like object to read the config file from @param file_obj: a file-like object to read the config file from
@type file_obj: file @type file_obj: file
""" """
configs = [self._config[0]] host = {"host": ['*'], "config": {}}
for line in file_obj: for line in file_obj:
line = line.rstrip('\n').lstrip() line = line.rstrip('\n').lstrip()
if (line == '') or (line[0] == '#'): if (line == '') or (line[0] == '#'):
@ -77,20 +123,20 @@ class SSHConfig (object):
value = line[i:].lstrip() value = line[i:].lstrip()
if key == 'host': if key == 'host':
del configs[:] self._config.append(host)
# the value may be multiple hosts, space-delimited value = value.split()
for host in value.split(): host = {key: value, 'config': {}}
# do we have a pre-existing host config to append to? #identityfile is a special case, since it is allowed to be
matches = [c for c in self._config if c['host'] == host] # specified multiple times and they should be tried in order
if len(matches) > 0: # of specification.
configs.append(matches[0]) elif key == 'identityfile':
else: if key in host['config']:
config = { 'host': host } host['config']['identityfile'].append(value)
self._config.append(config) else:
configs.append(config) host['config']['identityfile'] = [value]
else: elif key not in host['config']:
for config in configs: host['config'].update({key: value})
config[key] = value self._config.append(host)
def lookup(self, hostname): def lookup(self, hostname):
""" """
@ -105,31 +151,45 @@ class SSHConfig (object):
will win out. will win out.
The keys in the returned dict are all normalized to lowercase (look for 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 C{"port"}, not C{"Port"}. The values are processed according to the
values. rules for substitution variable expansion in C{ssh_config}.
@param hostname: the hostname to lookup @param hostname: the hostname to lookup
@type hostname: str @type hostname: str
""" """
matches = [x for x in self._config if fnmatch.fnmatch(hostname, x['host'])]
# Move * to the end matches = [config for config in self._config if
_star = matches.pop(0) self._allowed(hostname, config['host'])]
matches.append(_star)
ret = {} ret = {}
for m in matches: for match in matches:
for k,v in m.iteritems(): for key, value in match['config'].iteritems():
if not k in ret: if key not in ret:
ret[k] = v # 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) ret = self._expand_variables(ret, hostname)
del ret['host']
return ret 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 Return a dict of config options with expanded substitutions
for a given hostname. 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. are replaced.
@param config: the config for the hostname @param config: the config for the hostname
@ -139,7 +199,7 @@ class SSHConfig (object):
""" """
if 'hostname' in config: if 'hostname' in config:
config['hostname'] = config['hostname'].replace('%h',hostname) config['hostname'] = config['hostname'].replace('%h', hostname)
else: else:
config['hostname'] = hostname config['hostname'] = hostname
@ -155,34 +215,42 @@ class SSHConfig (object):
remoteuser = user remoteuser = user
host = socket.gethostname().split('.')[0] host = socket.gethostname().split('.')[0]
fqdn = socket.getfqdn() fqdn = LazyFqdn(config)
homedir = os.path.expanduser('~') homedir = os.path.expanduser('~')
replacements = { replacements = {'controlpath':
'controlpath': [ [
('%h', config['hostname']), ('%h', config['hostname']),
('%l', fqdn), ('%l', fqdn),
('%L', host), ('%L', host),
('%n', hostname), ('%n', hostname),
('%p', port), ('%p', port),
('%r', remoteuser), ('%r', remoteuser),
('%u', user) ('%u', user)
], ],
'identityfile': [ 'identityfile':
('~', homedir), [
('%d', homedir), ('~', homedir),
('%h', config['hostname']), ('%d', homedir),
('%l', fqdn), ('%h', config['hostname']),
('%u', user), ('%l', fqdn),
('%r', remoteuser) ('%u', user),
], ('%r', remoteuser)
'proxycommand': [ ],
('%h', config['hostname']), 'proxycommand':
('%p', port), [
('%r', remoteuser), ('%h', config['hostname']),
], ('%p', port),
} ('%r', remoteuser)
]
}
for k in config: for k in config:
if k in replacements: if k in replacements:
for find, replace in replacements[k]: for find, replace in replacements[k]:
config[k] = config[k].replace(find, str(replace)) 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 return config

View File

@ -110,7 +110,8 @@ class Message (object):
@rtype: string @rtype: string
""" """
b = self.packet.read(n) 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 + '\x00' * (n - len(b))
return b return b

View File

@ -87,6 +87,7 @@ class Packetizer (object):
self.__mac_size_in = 0 self.__mac_size_in = 0
self.__block_engine_out = None self.__block_engine_out = None
self.__block_engine_in = None self.__block_engine_in = None
self.__sdctr_out = False
self.__mac_engine_out = None self.__mac_engine_out = None
self.__mac_engine_in = None self.__mac_engine_in = None
self.__mac_key_out = '' self.__mac_key_out = ''
@ -110,11 +111,12 @@ class Packetizer (object):
""" """
self.__logger = log 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. Switch outbound data cipher.
""" """
self.__block_engine_out = block_engine self.__block_engine_out = block_engine
self.__sdctr_out = sdctr
self.__block_size_out = block_size self.__block_size_out = block_size
self.__mac_engine_out = mac_engine self.__mac_engine_out = mac_engine
self.__mac_size_out = mac_size self.__mac_size_out = mac_size
@ -490,12 +492,12 @@ class Packetizer (object):
padding = 3 + bsize - ((len(payload) + 8) % bsize) padding = 3 + bsize - ((len(payload) + 8) % bsize)
packet = struct.pack('>IB', len(payload) + padding + 1, padding) packet = struct.pack('>IB', len(payload) + padding + 1, padding)
packet += payload packet += payload
if self.__block_engine_out is not None: if self.__sdctr_out or self.__block_engine_out is None:
packet += rng.read(padding) # cute trick i caught openssh doing: if we're not encrypting or SDCTR mode (RFC4344),
else:
# cute trick i caught openssh doing: if we're not encrypting,
# don't waste random bytes for the padding # don't waste random bytes for the padding
packet += (chr(0) * padding) packet += (chr(0) * padding)
else:
packet += rng.read(padding)
return packet return packet
def _trigger_rekey(self): def _trigger_rekey(self):

View File

@ -198,7 +198,7 @@ class SFTPClient (BaseSFTP):
Open a file on the remote server. The arguments are the same as for 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 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 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, 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, C{'w'} for writing (truncating an existing file), C{'a'} for appending,

View File

@ -21,6 +21,7 @@ L{SFTPFile}
""" """
from binascii import hexlify from binascii import hexlify
from collections import deque
import socket import socket
import threading import threading
import time import time
@ -34,6 +35,9 @@ from paramiko.sftp_attr import SFTPAttributes
class SFTPFile (BufferedFile): class SFTPFile (BufferedFile):
""" """
Proxy object for a file on the remote server, in client mode SFTP. 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 # 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_data = {}
self._prefetch_reads = [] self._prefetch_reads = []
self._saved_exception = None self._saved_exception = None
self._reqs = deque()
def __del__(self): def __del__(self):
self._close(async=True) self._close(async=True)
@ -160,12 +165,14 @@ class SFTPFile (BufferedFile):
def _write(self, data): def _write(self, data):
# may write less than requested if it would exceed max packet size # may write less than requested if it would exceed max packet size
chunk = min(len(data), self.MAX_REQUEST_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])) self._reqs.append(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(): if not self.pipelined or (len(self._reqs) > 100 and self.sftp.sock.recv_ready()):
t, msg = self.sftp._read_response(req) while len(self._reqs):
if t != CMD_STATUS: req = self._reqs.popleft()
raise SFTPError('Expected status') t, msg = self.sftp._read_response(req)
# convert_status already called if t != CMD_STATUS:
raise SFTPError('Expected status')
# convert_status already called
return chunk return chunk
def settimeout(self, timeout): def settimeout(self, timeout):
@ -474,3 +481,9 @@ class SFTPFile (BufferedFile):
x = self._saved_exception x = self._saved_exception
self._saved_exception = None self._saved_exception = None
raise x raise x
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
self.close()

View File

@ -1885,7 +1885,8 @@ class Transport (threading.Thread):
mac_key = self._compute_key('F', mac_engine.digest_size) mac_key = self._compute_key('F', mac_engine.digest_size)
else: else:
mac_key = self._compute_key('E', mac_engine.digest_size) 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] 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): if (compress_out is not None) and ((self.local_compression != 'zlib@openssh.com') or self.authenticated):
self._log(DEBUG, 'Switching on outbound compression ...') self._log(DEBUG, 'Switching on outbound compression ...')

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
pycrypto
tox

View File

@ -23,6 +23,8 @@ 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). do test file operations in (so no existing files will be harmed).
""" """
from __future__ import with_statement
from binascii import hexlify from binascii import hexlify
import logging import logging
import os import os
@ -188,6 +190,17 @@ class SFTPTest (unittest.TestCase):
finally: finally:
sftp.remove(FOLDER + '/duck.txt') 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): def test_4_append(self):
""" """
verify that a file can be opened for append, and tell() still works. verify that a file can be opened for append, and tell() still works.

View File

@ -104,23 +104,32 @@ class UtilTest(ParamikoTest):
f = cStringIO.StringIO(test_config_file) f = cStringIO.StringIO(test_config_file)
config = paramiko.util.parse_ssh_config(f) config = paramiko.util.parse_ssh_config(f)
self.assertEquals(config._config, self.assertEquals(config._config,
[ {'identityfile': '~/.ssh/id_rsa', 'host': '*', 'user': 'robey', [{'host': ['*'], 'config': {}}, {'host': ['*'], 'config': {'identityfile': ['~/.ssh/id_rsa'], 'user': 'robey'}},
'crazy': 'something dumb '}, {'host': ['*.example.com'], 'config': {'user': 'bjork', 'port': '3333'}},
{'host': '*.example.com', 'user': 'bjork', 'port': '3333'}, {'host': ['*'], 'config': {'crazy': 'something dumb '}},
{'host': 'spoo.example.com', 'crazy': 'something else'}]) {'host': ['spoo.example.com'], 'config': {'crazy': 'something else'}}])
def test_3_host_config(self): def test_3_host_config(self):
global test_config_file global test_config_file
f = cStringIO.StringIO(test_config_file) f = cStringIO.StringIO(test_config_file)
config = paramiko.util.parse_ssh_config(f) config = paramiko.util.parse_ssh_config(f)
for host, values in { for host, values in {
'irc.danger.com': {'user': 'robey', 'crazy': 'something dumb '}, 'irc.danger.com': {'crazy': 'something dumb ',
'irc.example.com': {'user': 'bjork', 'crazy': 'something dumb ', 'port': '3333'}, 'hostname': 'irc.danger.com',
'spoo.example.com': {'user': 'bjork', 'crazy': 'something else', 'port': '3333'} '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(): }.items():
values = dict(values, values = dict(values,
hostname=host, hostname=host,
identityfile=os.path.expanduser("~/.ssh/id_rsa") identityfile=[os.path.expanduser("~/.ssh/id_rsa")]
) )
self.assertEquals( self.assertEquals(
paramiko.util.lookup_ssh_host_config(host, config), 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. # just verify that we can pull out 32 bytes and not get an exception.
x = rng.read(32) x = rng.read(32)
self.assertEquals(len(x), 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 = """ test_config_file = """
Host www13.* Host www13.*
Port 22 Port 22
@ -220,16 +229,16 @@ Host equals-delimited
ProxyCommand should perform interpolation on the value ProxyCommand should perform interpolation on the value
""" """
config = paramiko.util.parse_ssh_config(cStringIO.StringIO(""" config = paramiko.util.parse_ssh_config(cStringIO.StringIO("""
Host *
Port 25
ProxyCommand host %h port %p
Host specific Host specific
Port 37 Port 37
ProxyCommand host %h port %p lol ProxyCommand host %h port %p lol
Host portonly Host portonly
Port 155 Port 155
Host *
Port 25
ProxyCommand host %h port %p
""")) """))
for host, val in ( for host, val in (
('foo.com', "host foo.com port 25"), ('foo.com', "host foo.com port 25"),
@ -240,3 +249,83 @@ Host portonly
host_config(host, config)['proxycommand'], host_config(host, config)['proxycommand'],
val 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
)

6
tox.ini Normal file
View File

@ -0,0 +1,6 @@
[tox]
envlist = py25,py26,py27
[testenv]
commands = pip install --use-mirrors -q -r requirements.txt
python test.py