Merge branch 'master' into 112-int
Conflicts: paramiko/win_pageant.py
This commit is contained in:
commit
a7ee2509e4
|
@ -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
33
NEWS
|
@ -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
20
README
|
@ -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::
|
||||||
|
|
|
@ -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>"
|
||||||
|
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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 ...')
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
pycrypto
|
||||||
|
tox
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in New Issue