diff options
53 files changed, 798 insertions, 545 deletions
diff --git a/.travis.yml b/.travis.yml index 7042570f..3f6f7331 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ python: - "2.7" - "3.2" - "3.3" + - "3.4" install: # Self-install for setup.py-driven deps - pip install -e . @@ -17,8 +18,9 @@ script: # Run 'docs' first since its objects.inv is referred to by 'www'. # Also force warnings to be errors since most of them tend to be actual # problems. - - invoke docs -o -W - - invoke www -o -W + # Finally, skip them under Python 3.2 due to sphinx shenanigans + - "[[ $TRAVIS_PYTHON_VERSION != 3.2 ]] && invoke docs -o -W || true" + - "[[ $TRAVIS_PYTHON_VERSION != 3.2 ]] && invoke www -o -W || true" notifications: irc: channels: "irc.freenode.org#paramiko" @@ -26,7 +28,6 @@ notifications: - "%{repository}@%{branch}: %{message} (%{build_url})" on_success: change on_failure: change - use_notice: true email: false after_success: - coveralls @@ -35,7 +35,7 @@ Requirements ------------ - Python 2.6 or better <http://www.python.org/> - this includes Python - 3.3 and higher as well. + 3.2 and higher as well. - pycrypto 2.1 or better <https://www.dlitz.net/software/pycrypto/> - ecdsa 0.9 or better <https://pypi.python.org/pypi/ecdsa> diff --git a/dev-requirements.txt b/dev-requirements.txt index 5744f331..7a0ccbc5 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,9 +1,9 @@ # Older junk tox>=1.4,<1.5 # For newer tasks like building Sphinx docs. -# NOTE: Requires Python >=2.6 -invoke>=0.7.0 +invoke>=0.7.0,<0.8 invocations>=0.5.0 sphinx>=1.1.3 -alabaster>=0.3.1 +alabaster>=0.6.1 releases>=0.5.2 +wheel==0.23.0 diff --git a/paramiko/__init__.py b/paramiko/__init__.py index b1d9aaa9..65f6f8a2 100644 --- a/paramiko/__init__.py +++ b/paramiko/__init__.py @@ -17,14 +17,13 @@ # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. import sys +from paramiko._version import __version__, __version_info__ if sys.version_info < (2, 6): raise RuntimeError('You need Python 2.6+ for this module.') __author__ = "Jeff Forcier <jeff@bitprophet.org>" -__version__ = "1.13.0" -__version_info__ = tuple([ int(d) for d in __version__.split(".") ]) __license__ = "GNU Lesser General Public License (LGPL)" diff --git a/paramiko/_version.py b/paramiko/_version.py new file mode 100644 index 00000000..a7857b09 --- /dev/null +++ b/paramiko/_version.py @@ -0,0 +1,2 @@ +__version_info__ = (1, 15, 0) +__version__ = '.'.join(map(str, __version_info__)) diff --git a/paramiko/agent.py b/paramiko/agent.py index 2b11337f..5a08d452 100644 --- a/paramiko/agent.py +++ b/paramiko/agent.py @@ -364,7 +364,7 @@ class AgentKey(PKey): def get_name(self): return self.name - def sign_ssh_data(self, rng, data): + def sign_ssh_data(self, data): msg = Message() msg.add_byte(cSSH2_AGENTC_SIGN_REQUEST) msg.add_string(self.blob) diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py index c00ad41c..57babef0 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -206,7 +206,7 @@ class AuthHandler (object): m.add_string(self.private_key.get_name()) m.add_string(self.private_key) blob = self._get_session_blob(self.private_key, 'ssh-connection', self.username) - sig = self.private_key.sign_ssh_data(self.transport.rng, blob) + sig = self.private_key.sign_ssh_data(blob) m.add_string(sig) elif self.auth_method == 'keyboard-interactive': m.add_string('') diff --git a/paramiko/channel.py b/paramiko/channel.py index e10ddbac..49d8dd6e 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -21,9 +21,10 @@ Abstraction for an SSH2 channel. """ import binascii +import os +import socket import time import threading -import socket from paramiko import util from paramiko.common import cMSG_CHANNEL_REQUEST, cMSG_CHANNEL_WINDOW_ADJUST, \ @@ -279,7 +280,7 @@ class Channel (object): def recv_exit_status(self): """ Return the exit status from the process on the server. This is - mostly useful for retrieving the reults of an `exec_command`. + mostly useful for retrieving the results of an `exec_command`. If the command hasn't finished yet, this method will wait until it does, or until the channel is closed. If no exit status is provided by the server, -1 is returned. @@ -329,7 +330,7 @@ class Channel (object): If you omit the auth_cookie, a new secure random 128-bit value will be generated, used, and returned. You will need to use this value to verify incoming x11 requests and replace them with the actual local - x11 cookie (which requires some knoweldge of the x11 protocol). + x11 cookie (which requires some knowledge of the x11 protocol). If a handler is passed in, the handler is called from another thread whenever a new x11 connection arrives. The default handler queues up @@ -338,7 +339,7 @@ class Channel (object): handler(channel: Channel, (address: str, port: int)) - :param int screen_number: the x11 screen number (0, 10, etc) + :param int screen_number: the x11 screen number (0, 10, etc.) :param str auth_protocol: the name of the X11 authentication method used; if none is given, ``"MIT-MAGIC-COOKIE-1"`` is used @@ -358,7 +359,7 @@ class Channel (object): if auth_protocol is None: auth_protocol = 'MIT-MAGIC-COOKIE-1' if auth_cookie is None: - auth_cookie = binascii.hexlify(self.transport.rng.read(16)) + auth_cookie = binascii.hexlify(os.urandom(16)) m = Message() m.add_byte(cMSG_CHANNEL_REQUEST) @@ -743,10 +744,10 @@ class Channel (object): :raises socket.timeout: if sending stalled for longer than the timeout set by `settimeout`. :raises socket.error: - if an error occured before the entire string was sent. + if an error occurred before the entire string was sent. .. note:: - If the channel is closed while only part of the data hase been + If the channel is closed while only part of the data has been sent, there is no way to determine how much data (if any) was sent. This is irritating, but identically follows Python's API. """ @@ -770,7 +771,7 @@ class Channel (object): :raises socket.timeout: if sending stalled for longer than the timeout set by `settimeout`. :raises socket.error: - if an error occured before the entire string was sent. + if an error occurred before the entire string was sent. .. versionadded:: 1.1 """ @@ -811,7 +812,7 @@ class Channel (object): def fileno(self): """ Returns an OS-level file descriptor which can be used for polling, but - but not for reading or writing. This is primaily to allow Python's + but not for reading or writing. This is primarily to allow Python's ``select`` module to work. The first time ``fileno`` is called on a channel, a pipe is created to diff --git a/paramiko/client.py b/paramiko/client.py index c1bf4735..4326abbd 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -30,6 +30,7 @@ from paramiko.agent import Agent from paramiko.common import DEBUG from paramiko.config import SSH_PORT from paramiko.dsskey import DSSKey +from paramiko.ecdsakey import ECDSAKey from paramiko.hostkeys import HostKeys from paramiko.py3compat import string_types from paramiko.resource import ResourceManager @@ -184,7 +185,8 @@ class SSHClient (object): - The ``pkey`` or ``key_filename`` passed in (if any) - Any key we can find through an SSH agent - - Any "id_rsa" or "id_dsa" key discoverable in ``~/.ssh/`` + - Any "id_rsa", "id_dsa" or "id_ecdsa" key discoverable in + ``~/.ssh/`` - Plain username/password auth, if a password was given If a private key requires a password to unlock it, and a password is @@ -360,7 +362,8 @@ class SSHClient (object): - The key passed in, if one was passed in. - Any key we can find through an SSH agent (if allowed). - - Any "id_rsa" or "id_dsa" key discoverable in ~/.ssh/ (if allowed). + - Any "id_rsa", "id_dsa" or "id_ecdsa" key discoverable in ~/.ssh/ + (if allowed). - Plain username/password auth, if a password was given. (The password might be needed to unlock a private key, or for @@ -382,7 +385,7 @@ class SSHClient (object): if not two_factor: for key_filename in key_filenames: - for pkey_class in (RSAKey, DSSKey): + for pkey_class in (RSAKey, DSSKey, ECDSAKey): try: key = pkey_class.from_private_key_file(key_filename, password) self._log(DEBUG, 'Trying key %s from %s' % (hexlify(key.get_fingerprint()), key_filename)) @@ -414,17 +417,23 @@ class SSHClient (object): keyfiles = [] rsa_key = os.path.expanduser('~/.ssh/id_rsa') dsa_key = os.path.expanduser('~/.ssh/id_dsa') + ecdsa_key = os.path.expanduser('~/.ssh/id_ecdsa') if os.path.isfile(rsa_key): keyfiles.append((RSAKey, rsa_key)) if os.path.isfile(dsa_key): keyfiles.append((DSSKey, dsa_key)) + if os.path.isfile(ecdsa_key): + keyfiles.append((ECDSAKey, ecdsa_key)) # look in ~/ssh/ for windows users: rsa_key = os.path.expanduser('~/ssh/id_rsa') dsa_key = os.path.expanduser('~/ssh/id_dsa') + ecdsa_key = os.path.expanduser('~/ssh/id_ecdsa') if os.path.isfile(rsa_key): keyfiles.append((RSAKey, rsa_key)) if os.path.isfile(dsa_key): keyfiles.append((DSSKey, dsa_key)) + if os.path.isfile(ecdsa_key): + keyfiles.append((ECDSAKey, ecdsa_key)) if not look_for_keys: keyfiles = [] diff --git a/paramiko/common.py b/paramiko/common.py index 9a5e2ee1..18298922 100644 --- a/paramiko/common.py +++ b/paramiko/common.py @@ -126,11 +126,6 @@ CONNECTION_FAILED_CODE = { DISCONNECT_SERVICE_NOT_AVAILABLE, DISCONNECT_AUTH_CANCELLED_BY_USER, \ DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE = 7, 13, 14 -from Crypto import Random - -# keep a crypto-strong PRNG nearby -rng = Random.new() - zero_byte = byte_chr(0) one_byte = byte_chr(1) four_byte = byte_chr(4) diff --git a/paramiko/config.py b/paramiko/config.py index 77fa13d7..20ca4aa7 100644 --- a/paramiko/config.py +++ b/paramiko/config.py @@ -27,7 +27,6 @@ import re import socket SSH_PORT = 22 -proxy_re = re.compile(r"^(proxycommand)\s*=*\s*(.*)", re.I) class SSHConfig (object): @@ -41,6 +40,8 @@ class SSHConfig (object): .. versionadded:: 1.6 """ + SETTINGS_REGEX = re.compile(r'(\w+)(?:\s*=\s*|\s+)(.+)') + def __init__(self): """ Create a new OpenSSH config object. @@ -53,44 +54,39 @@ class SSHConfig (object): :param file file_obj: a file-like object to read the config file from """ + host = {"host": ['*'], "config": {}} for line in file_obj: - line = line.rstrip('\n').lstrip() - if (line == '') or (line[0] == '#'): + line = line.rstrip('\r\n').lstrip() + if not line or line.startswith('#'): continue - if '=' in line: - # Ensure ProxyCommand gets properly split - if line.lower().strip().startswith('proxycommand'): - match = proxy_re.match(line) - key, value = match.group(1).lower(), match.group(2) - else: - key, value = line.split('=', 1) - key = key.strip().lower() - else: - # find first whitespace, and split there - i = 0 - while (i < len(line)) and not line[i].isspace(): - i += 1 - if i == len(line): - raise Exception('Unparsable line: %r' % line) - key = line[:i].lower() - value = line[i:].lstrip() + match = re.match(self.SETTINGS_REGEX, line) + if not match: + raise Exception("Unparsable line %s" % line) + key = match.group(1).lower() + value = match.group(2) + if key == 'host': 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}) + host = { + 'host': self._get_hosts(value), + 'config': {} + } + else: + if value.startswith('"') and value.endswith('"'): + value = value[1:-1] + + #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. + if 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'][key] = value self._config.append(host) def lookup(self, hostname): @@ -111,8 +107,10 @@ class SSHConfig (object): :param str hostname: the hostname to lookup """ - matches = [config for config in self._config if - self._allowed(hostname, config['host'])] + matches = [ + config for config in self._config + if self._allowed(config['host'], hostname) + ] ret = {} for match in matches: @@ -128,7 +126,7 @@ class SSHConfig (object): ret = self._expand_variables(ret, hostname) return ret - def _allowed(self, hostname, hosts): + def _allowed(self, hosts, hostname): match = False for host in hosts: if host.startswith('!') and fnmatch.fnmatch(hostname, host[1:]): @@ -200,12 +198,38 @@ class SSHConfig (object): 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)) + if find in config[k][item]: + config[k][item] = config[k][item].\ + replace(find, str(replace)) else: - config[k] = config[k].replace(find, str(replace)) + if find in config[k]: + config[k] = config[k].replace(find, str(replace)) return config + def _get_hosts(self, host): + """ + Return a list of host_names from host value. + """ + i, length = 0, len(host) + hosts = [] + while i < length: + if host[i] == '"': + end = host.find('"', i + 1) + if end < 0: + raise Exception("Unparsable host %s" % host) + hosts.append(host[i + 1:end]) + i = end + 1 + elif not host[i].isspace(): + end = i + 1 + while end < length and not host[end].isspace() and host[end] != '"': + end += 1 + hosts.append(host[i:end]) + i = end + else: + i += 1 + + return hosts + class LazyFqdn(object): """ diff --git a/paramiko/dsskey.py b/paramiko/dsskey.py index c26966e8..1901596d 100644 --- a/paramiko/dsskey.py +++ b/paramiko/dsskey.py @@ -20,11 +20,13 @@ DSS keys. """ +import os +from hashlib import sha1 + from Crypto.PublicKey import DSA -from Crypto.Hash import SHA from paramiko import util -from paramiko.common import zero_byte, rng +from paramiko.common import zero_byte from paramiko.py3compat import long from paramiko.ssh_exception import SSHException from paramiko.message import Message @@ -91,17 +93,17 @@ class DSSKey (PKey): def get_bits(self): return self.size - + def can_sign(self): return self.x is not None - def sign_ssh_data(self, rng, data): - digest = SHA.new(data).digest() + def sign_ssh_data(self, data): + digest = sha1(data).digest() dss = DSA.construct((long(self.y), long(self.g), long(self.p), long(self.q), long(self.x))) # generate a suitable k qsize = len(util.deflate_long(self.q, 0)) while True: - k = util.inflate_long(rng.read(qsize), 1) + k = util.inflate_long(os.urandom(qsize), 1) if (k > 2) and (k < self.q): break r, s = dss.sign(util.inflate_long(digest, 1), k) @@ -111,9 +113,9 @@ class DSSKey (PKey): rstr = util.deflate_long(r, 0) sstr = util.deflate_long(s, 0) if len(rstr) < 20: - rstr += zero_byte * (20 - len(rstr)) + rstr = zero_byte * (20 - len(rstr)) + rstr if len(sstr) < 20: - sstr += zero_byte * (20 - len(sstr)) + sstr = zero_byte * (20 - len(sstr)) + sstr m.add_string(rstr + sstr) return m @@ -130,7 +132,7 @@ class DSSKey (PKey): # pull out (r, s) which are NOT encoded as mpints sigR = util.inflate_long(sig[:20], 1) sigS = util.inflate_long(sig[20:], 1) - sigM = util.inflate_long(SHA.new(data).digest(), 1) + sigM = util.inflate_long(sha1(data).digest(), 1) dss = DSA.construct((long(self.y), long(self.g), long(self.p), long(self.q))) return dss.verify(sigM, (sigR, sigS)) @@ -163,7 +165,7 @@ class DSSKey (PKey): by ``pyCrypto.PublicKey``). :return: new `.DSSKey` private key """ - dsa = DSA.generate(bits, rng.read, progress_func) + dsa = DSA.generate(bits, os.urandom, progress_func) key = DSSKey(vals=(dsa.p, dsa.q, dsa.g, dsa.y)) key.x = dsa.x return key @@ -174,11 +176,11 @@ class DSSKey (PKey): def _from_private_key_file(self, filename, password): data = self._read_private_key_file('DSA', filename, password) self._decode_key(data) - + def _from_private_key(self, file_obj, password): data = self._read_private_key('DSA', file_obj, password) self._decode_key(data) - + def _decode_key(self, data): # private key file contains: # DSAPrivateKey = { version = 0, p, q, g, y, x } diff --git a/paramiko/ecdsakey.py b/paramiko/ecdsakey.py index 490373b7..e869ee61 100644 --- a/paramiko/ecdsakey.py +++ b/paramiko/ecdsakey.py @@ -21,11 +21,11 @@ L{ECDSAKey} """ import binascii +from hashlib import sha256 + from ecdsa import SigningKey, VerifyingKey, der, curves -from Crypto.Hash import SHA256 -from ecdsa.test_pyecdsa import ECDSA -from paramiko.common import four_byte, one_byte +from paramiko.common import four_byte, one_byte from paramiko.message import Message from paramiko.pkey import PKey from paramiko.py3compat import byte_chr, u @@ -51,7 +51,7 @@ class ECDSAKey (PKey): if (msg is None) and (data is not None): msg = Message(data) if vals is not None: - self.verifying_key, self.signing_key = vals + self.signing_key, self.verifying_key = vals else: if msg is None: raise SSHException('Key object may not be empty') @@ -99,10 +99,9 @@ class ECDSAKey (PKey): def can_sign(self): return self.signing_key is not None - def sign_ssh_data(self, rpool, data): - digest = SHA256.new(data).digest() - sig = self.signing_key.sign_digest(digest, entropy=rpool.read, - sigencode=self._sigencode) + def sign_ssh_data(self, data): + sig = self.signing_key.sign_deterministic( + data, sigencode=self._sigencode, hashfunc=sha256) m = Message() m.add_string('ecdsa-sha2-nistp256') m.add_string(sig) @@ -115,7 +114,7 @@ class ECDSAKey (PKey): # verify the signature by SHA'ing the data and encrypting it # using the public key. - hash_obj = SHA256.new(data).digest() + hash_obj = sha256(data).digest() return self.verifying_key.verify_digest(sig, hash_obj, sigdecode=self._sigdecode) @@ -127,7 +126,7 @@ class ECDSAKey (PKey): key = self.signing_key or self.verifying_key self._write_private_key('EC', file_obj, key.to_der(), password) - def generate(bits, progress_func=None): + def generate(curve=curves.NIST256p, progress_func=None): """ Generate a new private RSA key. This factory function can be used to generate a new host key or authentication key. @@ -140,7 +139,7 @@ class ECDSAKey (PKey): @return: new private key @rtype: L{RSAKey} """ - signing_key = ECDSA.generate() + signing_key = SigningKey.generate(curve) key = ECDSAKey(vals=(signing_key, signing_key.get_verifying_key())) return key generate = staticmethod(generate) diff --git a/paramiko/file.py b/paramiko/file.py index f57aa79f..2238f0bf 100644 --- a/paramiko/file.py +++ b/paramiko/file.py @@ -124,9 +124,15 @@ class BufferedFile (object): file first). If the ``size`` argument is negative or omitted, read all the remaining data in the file. + .. note:: + ``'b'`` mode flag is ignored (``self.FLAG_BINARY`` in + ``self._flags``), because SSH treats all files as binary, since we + have no idea what encoding the file is in, or even if the file is + text data. + :param int size: maximum number of bytes to read :return: - data read from the file (as a `str`), or an empty string if EOF was + data read from the file (as bytes), or an empty string if EOF was encountered immediately """ if self._closed: @@ -148,12 +154,12 @@ class BufferedFile (object): result += new_data self._realpos += len(new_data) self._pos += len(new_data) - return result if self._flags & self.FLAG_BINARY else u(result) + return result if size <= len(self._rbuffer): result = self._rbuffer[:size] self._rbuffer = self._rbuffer[size:] self._pos += len(result) - return result if self._flags & self.FLAG_BINARY else u(result) + return result while len(self._rbuffer) < size: read_size = size - len(self._rbuffer) if self._flags & self.FLAG_BUFFERED: @@ -169,7 +175,7 @@ class BufferedFile (object): result = self._rbuffer[:size] self._rbuffer = self._rbuffer[size:] self._pos += len(result) - return result if self._flags & self.FLAG_BINARY else u(result) + return result def readline(self, size=None): """ @@ -186,8 +192,12 @@ class BufferedFile (object): :param int size: maximum length of returned string. :return: - next line of the file (`str`), or an empty string if the end of the + next line of the file, or an empty string if the end of the file has been reached. + + If the file was opened in binary (``'b'``) mode: bytes are returned + Else: the encoding of the file is assumed to be UTF-8 and character + strings (`str`) are returned """ # it's almost silly how complex this function is. if self._closed: @@ -277,7 +287,8 @@ class BufferedFile (object): Set the file's current position, like stdio's ``fseek``. Not all file objects support seeking. - .. note:: If a file is opened in append mode (``'a'`` or ``'a+'``), any seek + .. note:: + If a file is opened in append mode (``'a'`` or ``'a+'``), any seek operations will be undone at the next write (as the file position will move back to the end of the file). diff --git a/paramiko/hostkeys.py b/paramiko/hostkeys.py index 30031fad..b94ff0db 100644 --- a/paramiko/hostkeys.py +++ b/paramiko/hostkeys.py @@ -18,8 +18,11 @@ import binascii -from Crypto.Hash import SHA, HMAC -from paramiko.common import rng +import os + +from hashlib import sha1 +from hmac import HMAC + from paramiko.py3compat import b, u, encodebytes, decodebytes try: @@ -176,7 +179,7 @@ class HostKeys (MutableMapping): entries = [] for e in self._entries: for h in e.hostnames: - if h.startswith('|1|') and constant_time_bytes_eq(self.hash_host(hostname, h), h) or h == hostname: + if h.startswith('|1|') and not hostname.startswith('|1|') and constant_time_bytes_eq(self.hash_host(hostname, h), h) or h == hostname: entries.append(e) if len(entries) == 0: return None @@ -262,13 +265,13 @@ class HostKeys (MutableMapping): :return: the hashed hostname as a `str` """ if salt is None: - salt = rng.read(SHA.digest_size) + salt = os.urandom(sha1().digest_size) else: if salt.startswith('|1|'): salt = salt.split('|')[2] salt = decodebytes(b(salt)) - assert len(salt) == SHA.digest_size - hmac = HMAC.HMAC(salt, b(hostname), SHA).digest() + assert len(salt) == sha1().digest_size + hmac = HMAC(salt, b(hostname), sha1).digest() hostkey = '|1|%s|%s' % (u(encodebytes(salt)), u(encodebytes(hmac))) return hostkey.replace('\n', '') hash_host = staticmethod(hash_host) diff --git a/paramiko/kex_gex.py b/paramiko/kex_gex.py index 02e507b7..5ff8a287 100644 --- a/paramiko/kex_gex.py +++ b/paramiko/kex_gex.py @@ -22,7 +22,8 @@ generator "g" are provided by the server. A bit more work is required on the client side, and a B{lot} more on the server side. """ -from Crypto.Hash import SHA +import os +from hashlib import sha1 from paramiko import util from paramiko.common import DEBUG @@ -101,7 +102,7 @@ class KexGex (object): qhbyte <<= 1 qmask >>= 1 while True: - x_bytes = self.transport.rng.read(byte_count) + x_bytes = os.urandom(byte_count) x_bytes = byte_mask(x_bytes[0], qmask) + x_bytes[1:] x = util.inflate_long(x_bytes, 1) if (x > 1) and (x < q): @@ -203,10 +204,10 @@ class KexGex (object): hm.add_mpint(self.e) hm.add_mpint(self.f) hm.add_mpint(K) - H = SHA.new(hm.asbytes()).digest() + H = sha1(hm.asbytes()).digest() self.transport._set_K_H(K, H) # sign it - sig = self.transport.get_server_key().sign_ssh_data(self.transport.rng, H) + sig = self.transport.get_server_key().sign_ssh_data(H) # send reply m = Message() m.add_byte(c_MSG_KEXDH_GEX_REPLY) @@ -215,7 +216,7 @@ class KexGex (object): m.add_string(sig) self.transport._send_message(m) self.transport._activate_outbound() - + def _parse_kexdh_gex_reply(self, m): host_key = m.get_string() self.f = m.get_mpint() @@ -238,6 +239,6 @@ class KexGex (object): hm.add_mpint(self.e) hm.add_mpint(self.f) hm.add_mpint(K) - self.transport._set_K_H(K, SHA.new(hm.asbytes()).digest()) + self.transport._set_K_H(K, sha1(hm.asbytes()).digest()) self.transport._verify_key(host_key, sig) self.transport._activate_outbound() diff --git a/paramiko/kex_group1.py b/paramiko/kex_group1.py index 3dfb7f18..7ccceea6 100644 --- a/paramiko/kex_group1.py +++ b/paramiko/kex_group1.py @@ -21,7 +21,8 @@ Standard SSH key exchange ("kex" if you wanna sound cool). Diffie-Hellman of 1024 bit key halves, using a known "p" prime and "g" generator. """ -from Crypto.Hash import SHA +import os +from hashlib import sha1 from paramiko import util from paramiko.common import max_byte, zero_byte @@ -82,7 +83,7 @@ class KexGroup1(object): # potential x where the first 63 bits are 1, because some of those will be # larger than q (but this is a tiny tiny subset of potential x). while 1: - x_bytes = self.transport.rng.read(128) + x_bytes = os.urandom(128) x_bytes = byte_mask(x_bytes[0], 0x7f) + x_bytes[1:] if (x_bytes[:8] != b7fffffffffffffff and x_bytes[:8] != b0000000000000000): @@ -105,7 +106,7 @@ class KexGroup1(object): hm.add_mpint(self.e) hm.add_mpint(self.f) hm.add_mpint(K) - self.transport._set_K_H(K, SHA.new(hm.asbytes()).digest()) + self.transport._set_K_H(K, sha1(hm.asbytes()).digest()) self.transport._verify_key(host_key, sig) self.transport._activate_outbound() @@ -124,10 +125,10 @@ class KexGroup1(object): hm.add_mpint(self.e) hm.add_mpint(self.f) hm.add_mpint(K) - H = SHA.new(hm.asbytes()).digest() + H = sha1(hm.asbytes()).digest() self.transport._set_K_H(K, H) # sign it - sig = self.transport.get_server_key().sign_ssh_data(self.transport.rng, H) + sig = self.transport.get_server_key().sign_ssh_data(H) # send reply m = Message() m.add_byte(c_MSG_KEXDH_REPLY) diff --git a/paramiko/packet.py b/paramiko/packet.py index 0f51df5e..e97d92f0 100644 --- a/paramiko/packet.py +++ b/paramiko/packet.py @@ -21,25 +21,21 @@ Packet handling """ import errno +import os import socket import struct import threading import time +from hmac import HMAC from paramiko import util from paramiko.common import linefeed_byte, cr_byte_value, asbytes, MSG_NAMES, \ - DEBUG, xffffffff, zero_byte, rng + DEBUG, xffffffff, zero_byte from paramiko.py3compat import u, byte_ord from paramiko.ssh_exception import SSHException, ProxyCommandFailure from paramiko.message import Message -try: - from r_hmac import HMAC -except ImportError: - from Crypto.Hash.HMAC import HMAC - - def compute_hmac(key, message, digest_class): return HMAC(key, message, digest_class).digest() @@ -359,7 +355,7 @@ class Packetizer (object): raise SSHException('Mismatched MAC') padding = byte_ord(packet[0]) payload = packet[1:packet_size - padding] - + if self.__dump_packets: self._log(DEBUG, 'Got payload (%d bytes, %d padding)' % (packet_size, padding)) @@ -455,7 +451,7 @@ class Packetizer (object): # don't waste random bytes for the padding packet += (zero_byte * padding) else: - packet += rng.read(padding) + packet += os.urandom(padding) return packet def _trigger_rekey(self): diff --git a/paramiko/pkey.py b/paramiko/pkey.py index c8f84e0a..373563f6 100644 --- a/paramiko/pkey.py +++ b/paramiko/pkey.py @@ -23,12 +23,12 @@ Common API for all public keys. import base64 from binascii import hexlify, unhexlify import os +from hashlib import md5 -from Crypto.Hash import MD5 from Crypto.Cipher import DES3, AES from paramiko import util -from paramiko.common import o600, rng, zero_byte +from paramiko.common import o600, zero_byte from paramiko.py3compat import u, encodebytes, decodebytes, b from paramiko.ssh_exception import SSHException, PasswordRequiredException @@ -126,7 +126,7 @@ class PKey (object): a 16-byte `string <str>` (binary) of the MD5 fingerprint, in SSH format. """ - return MD5.new(self.asbytes()).digest() + return md5(self.asbytes()).digest() def get_base64(self): """ @@ -138,12 +138,11 @@ class PKey (object): """ return u(encodebytes(self.asbytes())).replace('\n', '') - def sign_ssh_data(self, rng, data): + def sign_ssh_data(self, data): """ Sign a blob of data with this private key, and return a `.Message` representing an SSH signature message. - :param .Crypto.Util.rng.RandomPool rng: a secure random number generator. :param str data: the data to sign. :return: an SSH signature `message <.Message>`. """ @@ -300,7 +299,7 @@ class PKey (object): keysize = self._CIPHER_TABLE[encryption_type]['keysize'] mode = self._CIPHER_TABLE[encryption_type]['mode'] salt = unhexlify(b(saltstr)) - key = util.generate_key_bytes(MD5, salt, password, keysize) + key = util.generate_key_bytes(md5, salt, password, keysize) return cipher.new(key, mode, salt).decrypt(data) def _write_private_key_file(self, tag, filename, data, password=None): @@ -331,11 +330,11 @@ class PKey (object): keysize = self._CIPHER_TABLE[cipher_name]['keysize'] blocksize = self._CIPHER_TABLE[cipher_name]['blocksize'] mode = self._CIPHER_TABLE[cipher_name]['mode'] - salt = rng.read(16) - key = util.generate_key_bytes(MD5, salt, password, keysize) + salt = os.urandom(16) + key = util.generate_key_bytes(md5, salt, password, keysize) if len(data) % blocksize != 0: n = blocksize - len(data) % blocksize - #data += rng.read(n) + #data += os.urandom(n) # that would make more sense ^, but it confuses openssh. data += zero_byte * n data = cipher.new(key, mode, salt).encrypt(data) diff --git a/paramiko/primes.py b/paramiko/primes.py index 58d158c8..8e02e80c 100644 --- a/paramiko/primes.py +++ b/paramiko/primes.py @@ -20,32 +20,14 @@ Utility functions for dealing with primes. """ -from Crypto.Util import number +import os from paramiko import util from paramiko.py3compat import byte_mask, long from paramiko.ssh_exception import SSHException -def _generate_prime(bits, rng): - """primtive attempt at prime generation""" - hbyte_mask = pow(2, bits % 8) - 1 - while True: - # loop catches the case where we increment n into a higher bit-range - x = rng.read((bits + 7) // 8) - if hbyte_mask > 0: - x = byte_mask(x[0], hbyte_mask) + x[1:] - n = util.inflate_long(x, 1) - n |= 1 - n |= (1 << (bits - 1)) - while not number.isPrime(n): - n += 2 - if util.bit_length(n) == bits: - break - return n - - -def _roll_random(rng, n): +def _roll_random(n): """returns a random # from 0 to N-1""" bits = util.bit_length(n - 1) byte_count = (bits + 7) // 8 @@ -58,7 +40,7 @@ def _roll_random(rng, n): # fits, so i can't guarantee that this loop will ever finish, but the odds # of it looping forever should be infinitesimal. while True: - x = rng.read(byte_count) + x = os.urandom(byte_count) if hbyte_mask > 0: x = byte_mask(x[0], hbyte_mask) + x[1:] num = util.inflate_long(x, 1) @@ -73,11 +55,10 @@ class ModulusPack (object): on systems that have such a file. """ - def __init__(self, rpool): + def __init__(self): # pack is a hash of: bits -> [ (generator, modulus) ... ] self.pack = {} self.discarded = [] - self.rng = rpool def _parse_modulus(self, line): timestamp, mod_type, tests, tries, size, generator, modulus = line.split() @@ -147,5 +128,5 @@ class ModulusPack (object): if min > good: good = bitsizes[-1] # now pick a random modulus of this bitsize - n = _roll_random(self.rng, len(self.pack[good])) + n = _roll_random(len(self.pack[good])) return self.pack[good][n] diff --git a/paramiko/py3compat.py b/paramiko/py3compat.py index 8842b988..57c096b2 100644 --- a/paramiko/py3compat.py +++ b/paramiko/py3compat.py @@ -39,6 +39,8 @@ if PY2: return s elif isinstance(s, unicode): return s.encode(encoding) + elif isinstance(s, buffer): + return s else: raise TypeError("Expected unicode or bytes, got %r" % s) @@ -49,6 +51,8 @@ if PY2: return s.decode(encoding) elif isinstance(s, unicode): return s + elif isinstance(s, buffer): + return s.decode(encoding) else: raise TypeError("Expected unicode or bytes, got %r" % s) diff --git a/paramiko/rsakey.py b/paramiko/rsakey.py index c93f3218..d1f3ecfe 100644 --- a/paramiko/rsakey.py +++ b/paramiko/rsakey.py @@ -20,11 +20,13 @@ RSA keys. """ +import os +from hashlib import sha1 + from Crypto.PublicKey import RSA -from Crypto.Hash import SHA from paramiko import util -from paramiko.common import rng, max_byte, zero_byte, one_byte +from paramiko.common import max_byte, zero_byte, one_byte from paramiko.message import Message from paramiko.ber import BER, BERException from paramiko.pkey import PKey @@ -90,8 +92,8 @@ class RSAKey (PKey): def can_sign(self): return self.d is not None - def sign_ssh_data(self, rpool, data): - digest = SHA.new(data).digest() + def sign_ssh_data(self, data): + digest = sha1(data).digest() rsa = RSA.construct((long(self.n), long(self.e), long(self.d))) sig = util.deflate_long(rsa.sign(self._pkcs1imify(digest), bytes())[0], 0) m = Message() @@ -106,7 +108,7 @@ class RSAKey (PKey): # verify the signature by SHA'ing the data and encrypting it using the # public key. some wackiness ensues where we "pkcs1imify" the 20-byte # hash into a string as long as the RSA key. - hash_obj = util.inflate_long(self._pkcs1imify(SHA.new(data).digest()), True) + hash_obj = util.inflate_long(self._pkcs1imify(sha1(data).digest()), True) rsa = RSA.construct((long(self.n), long(self.e))) return rsa.verify(hash_obj, (sig,)) @@ -125,7 +127,7 @@ class RSAKey (PKey): def write_private_key_file(self, filename, password=None): self._write_private_key_file('RSA', filename, self._encode_key(), password) - + def write_private_key(self, file_obj, password=None): self._write_private_key('RSA', file_obj, self._encode_key(), password) @@ -140,7 +142,7 @@ class RSAKey (PKey): by ``pyCrypto.PublicKey``). :return: new `.RSAKey` private key """ - rsa = RSA.generate(bits, rng.read, progress_func) + rsa = RSA.generate(bits, os.urandom, progress_func) key = RSAKey(vals=(rsa.e, rsa.n)) key.d = rsa.d key.p = rsa.p @@ -162,11 +164,11 @@ class RSAKey (PKey): def _from_private_key_file(self, filename, password): data = self._read_private_key_file('RSA', filename, password) self._decode_key(data) - + def _from_private_key(self, file_obj, password): data = self._read_private_key('RSA', file_obj, password) self._decode_key(data) - + def _decode_key(self, data): # private key file contains: # RSAPrivateKey = { version = 0, n, e, d, p, q, d mod p-1, d mod q-1, q**-1 mod p } diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py index ce6fbec6..3e85a8c9 100644 --- a/paramiko/sftp_client.py +++ b/paramiko/sftp_client.py @@ -117,8 +117,10 @@ class SFTPClient(BaseSFTP): def _log(self, level, msg, *args): if isinstance(msg, list): for m in msg: - super(SFTPClient, self)._log(level, "[chan %s] " + m, *([self.sock.get_name()] + list(args))) + self._log(level, m, *args) else: + # escape '%' in msg (they could come from file or directory names) before logging + msg = msg.replace('%','%%') super(SFTPClient, self)._log(level, "[chan %s] " + msg, *([self.sock.get_name()] + list(args))) def close(self): @@ -194,6 +196,71 @@ class SFTPClient(BaseSFTP): self._request(CMD_CLOSE, handle) return filelist + def listdir_iter(self, path='.', read_aheads=50): + """ + Generator version of `.listdir_attr`. + + See the API docs for `.listdir_attr` for overall details. + + This function adds one more kwarg on top of `.listdir_attr`: + ``read_aheads``, an integer controlling how many + ``SSH_FXP_READDIR`` requests are made to the server. The default of 50 + should suffice for most file listings as each request/response cycle + may contain multiple files (dependant on server implementation.) + + .. versionadded:: 1.15 + """ + path = self._adjust_cwd(path) + self._log(DEBUG, 'listdir(%r)' % path) + t, msg = self._request(CMD_OPENDIR, path) + + if t != CMD_HANDLE: + raise SFTPError('Expected handle') + + handle = msg.get_string() + + nums = list() + while True: + try: + # Send out a bunch of readdir requests so that we can read the + # responses later on Section 6.7 of the SSH file transfer RFC + # explains this + # http://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt + for i in range(read_aheads): + num = self._async_request(type(None), CMD_READDIR, handle) + nums.append(num) + + + # For each of our sent requests + # Read and parse the corresponding packets + # If we're at the end of our queued requests, then fire off + # some more requests + # Exit the loop when we've reached the end of the directory + # handle + for num in nums: + t, pkt_data = self._read_packet() + msg = Message(pkt_data) + new_num = msg.get_int() + if num == new_num: + if t == CMD_STATUS: + self._convert_status(msg) + count = msg.get_int() + for i in range(count): + filename = msg.get_text() + longname = msg.get_text() + attr = SFTPAttributes._from_msg( + msg, filename, longname) + if (filename != '.') and (filename != '..'): + yield attr + + # If we've hit the end of our queued requests, reset nums. + nums = list() + + except EOFError: + self._request(CMD_CLOSE, handle) + return + + def open(self, filename, mode='r', bufsize=-1): """ Open a file on the remote server. The arguments are the same as for @@ -532,9 +599,7 @@ class SFTPClient(BaseSFTP): an `.SFTPAttributes` object containing attributes about the given file. - .. versionadded:: 1.4 - .. versionchanged:: 1.7.4 - Began returning rich attribute objects. + .. versionadded:: 1.10 """ with self.file(remotepath, 'wb') as fr: fr.set_pipelined(True) @@ -564,7 +629,9 @@ class SFTPClient(BaseSFTP): The SFTP operations use pipelining for speed. :param str localpath: the local file to copy - :param str remotepath: the destination path on the SFTP server + :param str remotepath: the destination path on the SFTP server. Note + that the filename should be included. Only specifying a directory + may result in an error. :param callable callback: optional callback function (form: ``func(int, int)``) that accepts the bytes transferred so far and the total bytes to be transferred @@ -582,7 +649,7 @@ class SFTPClient(BaseSFTP): """ file_size = os.stat(localpath).st_size with open(localpath, 'rb') as fl: - return self.putfo(fl, remotepath, os.stat(localpath).st_size, callback, confirm) + return self.putfo(fl, remotepath, file_size, callback, confirm) def getfo(self, remotepath, fl, callback=None): """ @@ -599,9 +666,7 @@ class SFTPClient(BaseSFTP): the bytes transferred so far and the total bytes to be transferred :return: the `number <int>` of bytes written to the opened file object - .. versionadded:: 1.4 - .. versionchanged:: 1.7.4 - Added the ``callable`` param. + .. versionadded:: 1.10 """ with self.open(remotepath, 'rb') as fr: file_size = self.stat(remotepath).st_size diff --git a/paramiko/sftp_server.py b/paramiko/sftp_server.py index dadfd026..2d8d1909 100644 --- a/paramiko/sftp_server.py +++ b/paramiko/sftp_server.py @@ -22,9 +22,9 @@ Server-mode SFTP support. import os import errno - -from Crypto.Hash import MD5, SHA import sys +from hashlib import md5, sha1 + from paramiko import util from paramiko.sftp import BaseSFTP, Message, SFTP_FAILURE, \ SFTP_PERMISSION_DENIED, SFTP_NO_SUCH_FILE @@ -45,8 +45,8 @@ from paramiko.sftp import CMD_HANDLE, SFTP_DESC, CMD_STATUS, SFTP_EOF, CMD_NAME, CMD_READLINK, CMD_SYMLINK, CMD_REALPATH, CMD_EXTENDED, SFTP_OP_UNSUPPORTED _hash_class = { - 'sha1': SHA, - 'md5': MD5, + 'sha1': sha1, + 'md5': md5, } @@ -82,14 +82,14 @@ class SFTPServer (BaseSFTP, SubsystemHandler): self.file_table = {} self.folder_table = {} self.server = sftp_si(server, *largs, **kwargs) - + def _log(self, level, msg): if issubclass(type(msg), list): for m in msg: super(SFTPServer, self)._log(level, "[chan " + self.sock.get_name() + "] " + m) else: super(SFTPServer, self)._log(level, "[chan " + self.sock.get_name() + "] " + msg) - + def start_subsystem(self, name, transport, channel): self.sock = channel self._log(DEBUG, 'Started sftp server on channel %s' % repr(channel)) @@ -157,7 +157,7 @@ class SFTPServer (BaseSFTP, SubsystemHandler): This is meant to be a handy helper function for translating SFTP file requests into local file operations. - + :param str filename: name of the file to alter (should usually be an absolute path). :param .SFTPAttributes attr: attributes to change. @@ -281,7 +281,7 @@ class SFTPServer (BaseSFTP, SubsystemHandler): # don't try to read more than about 64KB at a time chunklen = min(blocklen, 65536) count = 0 - hash_obj = alg.new() + hash_obj = alg() while count < blocklen: data = f.read(offset, chunklen) if not isinstance(data, bytes_types): @@ -298,7 +298,7 @@ class SFTPServer (BaseSFTP, SubsystemHandler): msg.add_string(algname) msg.add_bytes(sum_out) self._send_packet(CMD_EXTENDED_REPLY, msg) - + def _convert_pflags(self, pflags): """convert SFTP-style open() flags to Python's os.open() flags""" if (pflags & SFTP_FLAG_READ) and (pflags & SFTP_FLAG_WRITE): diff --git a/paramiko/ssh_exception.py b/paramiko/ssh_exception.py index 63ca6409..b99e42b3 100644 --- a/paramiko/ssh_exception.py +++ b/paramiko/ssh_exception.py @@ -59,7 +59,9 @@ class BadAuthenticationType (AuthenticationException): def __init__(self, explanation, types): AuthenticationException.__init__(self, explanation) self.allowed_types = types - + # for unpickling + self.args = (explanation, types, ) + def __str__(self): return SSHException.__str__(self) + ' (allowed_types=%r)' % self.allowed_types @@ -73,6 +75,8 @@ class PartialAuthentication (AuthenticationException): def __init__(self, types): AuthenticationException.__init__(self, 'partial authentication') self.allowed_types = types + # for unpickling + self.args = (types, ) class ChannelException (SSHException): @@ -86,6 +90,8 @@ class ChannelException (SSHException): def __init__(self, code, text): SSHException.__init__(self, text) self.code = code + # for unpickling + self.args = (code, text, ) class BadHostKeyException (SSHException): @@ -103,6 +109,8 @@ class BadHostKeyException (SSHException): self.hostname = hostname self.key = got_key self.expected_key = expected_key + # for unpickling + self.args = (hostname, got_key, expected_key, ) class ProxyCommandFailure (SSHException): @@ -119,3 +127,5 @@ class ProxyCommandFailure (SSHException): ) ) self.error = error + # for unpickling + self.args = (command, error, ) diff --git a/paramiko/transport.py b/paramiko/transport.py index 1471b543..d3990e54 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -20,17 +20,19 @@ Core protocol implementation """ +import os import socket import sys import threading import time import weakref +from hashlib import md5, sha1 import paramiko from paramiko import util from paramiko.auth_handler import AuthHandler from paramiko.channel import Channel -from paramiko.common import rng, xffffffff, cMSG_CHANNEL_OPEN, cMSG_IGNORE, \ +from paramiko.common import xffffffff, cMSG_CHANNEL_OPEN, cMSG_IGNORE, \ cMSG_GLOBAL_REQUEST, DEBUG, MSG_KEXINIT, MSG_IGNORE, MSG_DISCONNECT, \ MSG_DEBUG, ERROR, WARNING, cMSG_UNIMPLEMENTED, INFO, cMSG_KEXINIT, \ cMSG_NEWKEYS, MSG_NEWKEYS, cMSG_REQUEST_SUCCESS, cMSG_REQUEST_FAILURE, \ @@ -57,9 +59,7 @@ from paramiko.ssh_exception import (SSHException, BadAuthenticationType, ChannelException, ProxyCommandFailure) from paramiko.util import retry_on_signal -from Crypto import Random from Crypto.Cipher import Blowfish, AES, DES3, ARC4 -from Crypto.Hash import SHA, MD5 try: from Crypto.Util import Counter except ImportError: @@ -107,10 +107,10 @@ class Transport (threading.Thread): } _mac_info = { - 'hmac-sha1': {'class': SHA, 'size': 20}, - 'hmac-sha1-96': {'class': SHA, 'size': 12}, - 'hmac-md5': {'class': MD5, 'size': 16}, - 'hmac-md5-96': {'class': MD5, 'size': 12}, + 'hmac-sha1': {'class': sha1, 'size': 20}, + 'hmac-sha1-96': {'class': sha1, 'size': 12}, + 'hmac-md5': {'class': md5, 'size': 16}, + 'hmac-md5-96': {'class': md5, 'size': 12}, } _key_info = { @@ -164,6 +164,8 @@ class Transport (threading.Thread): :param socket sock: a socket or socket-like object to create the session over. """ + self.active = False + if isinstance(sock, string_types): # convert "host:port" into (host, port) hl = sock.split(':', 1) @@ -192,7 +194,6 @@ class Transport (threading.Thread): # okay, normal socket-ish flow here... threading.Thread.__init__(self) self.setDaemon(True) - self.rng = rng self.sock = sock # Python < 2.3 doesn't have the settimeout method - RogerB try: @@ -220,7 +221,6 @@ class Transport (threading.Thread): self.H = None self.K = None - self.active = False self.initial_kex_done = False self.in_kex = False self.authenticated = False @@ -339,7 +339,6 @@ class Transport (threading.Thread): # synchronous, wait for a result self.completion_event = event = threading.Event() self.start() - Random.atfork() while True: event.wait(0.1) if not self.active: @@ -475,7 +474,7 @@ class Transport (threading.Thread): .. note:: This has no effect when used in client mode. """ - Transport._modulus_pack = ModulusPack(rng) + Transport._modulus_pack = ModulusPack() # places to look for the openssh "moduli" file file_list = ['/etc/ssh/moduli', '/usr/local/etc/moduli'] if filename is not None: @@ -732,8 +731,8 @@ class Transport (threading.Thread): m = Message() m.add_byte(cMSG_IGNORE) if byte_count is None: - byte_count = (byte_ord(rng.read(1)) % 32) + 10 - m.add_bytes(rng.read(byte_count)) + byte_count = (byte_ord(os.urandom(1)) % 32) + 10 + m.add_bytes(os.urandom(byte_count)) self._send_user_message(m) def renegotiate_keys(self): @@ -1338,13 +1337,13 @@ class Transport (threading.Thread): m.add_bytes(self.H) m.add_byte(b(id)) m.add_bytes(self.session_id) - out = sofar = SHA.new(m.asbytes()).digest() + out = sofar = sha1(m.asbytes()).digest() while len(out) < nbytes: m = Message() m.add_mpint(self.K) m.add_bytes(self.H) m.add_bytes(sofar) - digest = SHA.new(m.asbytes()).digest() + digest = sha1(m.asbytes()).digest() out += digest sofar += digest return out[:nbytes] @@ -1402,10 +1401,6 @@ class Transport (threading.Thread): # interpreter shutdown. self.sys = sys - # Required to prevent RNG errors when running inside many subprocess - # containers. - Random.atfork() - # active=True occurs before the thread is launched, to avoid a race _active_threads.append(self) if self.server_mode: @@ -1590,7 +1585,7 @@ class Transport (threading.Thread): m = Message() m.add_byte(cMSG_KEXINIT) - m.add_bytes(rng.read(16)) + m.add_bytes(os.urandom(16)) m.add_list(self._preferred_kex) m.add_list(available_server_keys) m.add_list(self._preferred_ciphers) @@ -1719,9 +1714,9 @@ class Transport (threading.Thread): # initial mac keys are done in the hash's natural size (not the potentially truncated # transmission size) if self.server_mode: - mac_key = self._compute_key('E', mac_engine.digest_size) + mac_key = self._compute_key('E', mac_engine().digest_size) else: - mac_key = self._compute_key('F', mac_engine.digest_size) + mac_key = self._compute_key('F', mac_engine().digest_size) self.packetizer.set_inbound_cipher(engine, block_size, mac_engine, mac_size, mac_key) compress_in = self._compression_info[self.remote_compression][1] if (compress_in is not None) and ((self.remote_compression != 'zlib@openssh.com') or self.authenticated): @@ -1746,9 +1741,9 @@ class Transport (threading.Thread): # initial mac keys are done in the hash's natural size (not the potentially truncated # transmission size) if self.server_mode: - mac_key = self._compute_key('F', mac_engine.digest_size) + mac_key = self._compute_key('F', mac_engine().digest_size) else: - mac_key = self._compute_key('E', mac_engine.digest_size) + mac_key = self._compute_key('E', mac_engine().digest_size) 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] @@ -1860,7 +1855,7 @@ class Transport (threading.Thread): self.lock.acquire() try: chan._set_remote_channel(server_chanid, server_window_size, server_max_packet_size) - self._log(INFO, 'Secsh channel %d opened.' % chanid) + self._log(DEBUG, 'Secsh channel %d opened.' % chanid) if chanid in self.channel_events: self.channel_events[chanid].set() del self.channel_events[chanid] @@ -1874,7 +1869,7 @@ class Transport (threading.Thread): reason_str = m.get_text() lang = m.get_text() reason_text = CONNECTION_FAILED_CODE.get(reason, '(unknown code)') - self._log(INFO, 'Secsh channel %d open FAILED: %s: %s' % (chanid, reason_str, reason_text)) + self._log(ERROR, 'Secsh channel %d open FAILED: %s: %s' % (chanid, reason_str, reason_text)) self.lock.acquire() try: self.saved_exception = ChannelException(reason, reason_text) @@ -1970,7 +1965,7 @@ class Transport (threading.Thread): m.add_int(self.window_size) m.add_int(self.max_packet_size) self._send_message(m) - self._log(INFO, 'Secsh channel %d (%s) opened.', my_chanid, kind) + self._log(DEBUG, 'Secsh channel %d (%s) opened.', my_chanid, kind) if kind == 'auth-agent@openssh.com': self._forward_agent_handler(chan) elif kind == 'x11': diff --git a/paramiko/util.py b/paramiko/util.py index dbcbbae4..f4ee3adc 100644 --- a/paramiko/util.py +++ b/paramiko/util.py @@ -143,15 +143,14 @@ def tb_strings(): return ''.join(traceback.format_exception(*sys.exc_info())).split('\n') -def generate_key_bytes(hashclass, salt, key, nbytes): +def generate_key_bytes(hash_alg, salt, key, nbytes): """ Given a password, passphrase, or other human-source key, scramble it through a secure hash into some keyworthy bytes. This specific algorithm is used for encrypting/decrypting private key files. - :param class hashclass: - class from `Crypto.Hash` that can be used as a secure hashing function - (like ``MD5`` or ``SHA``). + :param function hash_alg: A function which creates a new hash object, such + as ``hashlib.sha256``. :param salt: data to salt the hash with. :type salt: byte string :param str key: human-entered password or passphrase. @@ -163,7 +162,7 @@ def generate_key_bytes(hashclass, salt, key, nbytes): if len(salt) > 8: salt = salt[:8] while nbytes > 0: - hash_obj = hashclass.new() + hash_obj = hash_alg() if len(digest) > 0: hash_obj.update(digest) hash_obj.update(b(key)) @@ -40,9 +40,10 @@ import sys try: from setuptools import setup kw = { - 'install_requires': ['pycrypto >= 2.1, != 2.4', - 'ecdsa >= 0.11', - ], + 'install_requires': [ + 'pycrypto >= 2.1, != 2.4', + 'ecdsa >= 0.11', + ], } except ImportError: from distutils.core import setup @@ -53,9 +54,16 @@ if sys.platform == 'darwin': setup_helper.install_custom_make_tarball() +# Version info -- read without importing +_locals = {} +with open('paramiko/_version.py') as fp: + exec(fp.read(), None, _locals) +version = _locals['__version__'] + + setup( name = "paramiko", - version = "1.13.0", + version = version, description = "SSH2 protocol library", long_description = longdesc, author = "Jeff Forcier", @@ -78,6 +86,7 @@ setup( 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', ], **kw ) diff --git a/sites/_shared_static/logo.png b/sites/_shared_static/logo.png Binary files differdeleted file mode 100644 index bc76697e..00000000 --- a/sites/_shared_static/logo.png +++ /dev/null diff --git a/sites/docs/conf.py b/sites/docs/conf.py index 619ff816..5674fed1 100644 --- a/sites/docs/conf.py +++ b/sites/docs/conf.py @@ -5,12 +5,12 @@ sys.path.append(os.path.abspath('../..')) from shared_conf import * # Enable autodoc, intersphinx -extensions.extend(['sphinx.ext.autodoc', 'sphinx.ext.intersphinx']) +extensions.extend(['sphinx.ext.autodoc']) # Autodoc settings autodoc_default_flags = ['members', 'special-members'] -# Intersphinx connection to stdlib -intersphinx_mapping = { - 'python': ('http://docs.python.org/2.6', None), +# Sister-site links to WWW +html_theme_options['extra_nav_links'] = { + "Main website": 'http://www.paramiko.org', } diff --git a/sites/shared_conf.py b/sites/shared_conf.py index 52cec938..4a6a5c4e 100644 --- a/sites/shared_conf.py +++ b/sites/shared_conf.py @@ -1,25 +1,20 @@ from datetime import datetime -import os -import sys import alabaster # Alabaster theme + mini-extension html_theme_path = [alabaster.get_path()] -extensions = ['alabaster'] +extensions = ['alabaster', 'sphinx.ext.intersphinx'] # Paths relative to invoking conf.py - not this shared file -html_static_path = ['../_shared_static'] html_theme = 'alabaster' html_theme_options = { 'description': "A Python implementation of SSHv2.", 'github_user': 'paramiko', 'github_repo': 'paramiko', - 'gittip_user': 'bitprophet', + 'gratipay_user': 'bitprophet', 'analytics_id': 'UA-18486793-2', - - 'link': '#3782BE', - 'link_hover': '#3782BE', + 'travis_button': True, } html_sidebars = { '**': [ @@ -30,6 +25,11 @@ html_sidebars = { ] } +# Everything intersphinx's to Python +intersphinx_mapping = { + 'python': ('http://docs.python.org/2.6', None), +} + # Regular settings project = 'Paramiko' year = datetime.now().year diff --git a/sites/www/blog.py b/sites/www/blog.py deleted file mode 100644 index 3b129ebf..00000000 --- a/sites/www/blog.py +++ /dev/null @@ -1,140 +0,0 @@ -from collections import namedtuple -from datetime import datetime -import time -import email.utils - -from sphinx.util.compat import Directive -from docutils import nodes - - -class BlogDateDirective(Directive): - """ - Used to parse/attach date info to blog post documents. - - No nodes generated, since none are needed. - """ - has_content = True - - def run(self): - # Tag parent document with parsed date value. - self.state.document.blog_date = datetime.strptime( - self.content[0], "%Y-%m-%d" - ) - # Don't actually insert any nodes, we're already done. - return [] - -class blog_post_list(nodes.General, nodes.Element): - pass - -class BlogPostListDirective(Directive): - """ - Simply spits out a 'blog_post_list' temporary node for replacement. - - Gets replaced at doctree-resolved time - only then will all blog post - documents be written out (& their date directives executed). - """ - def run(self): - return [blog_post_list('')] - - -Post = namedtuple('Post', 'name doc title date opener') - -def get_posts(app): - # Obtain blog posts - post_names = filter(lambda x: x.startswith('blog/'), app.env.found_docs) - posts = map(lambda x: (x, app.env.get_doctree(x)), post_names) - # Obtain common data used for list page & RSS - data = [] - for post, doc in sorted(posts, key=lambda x: x[1].blog_date, reverse=True): - # Welp. No "nice" way to get post title. Thanks Sphinx. - title = doc[0][0][0] - # Date. This may or may not end up reflecting the required - # *input* format, but doing it here gives us flexibility. - date = doc.blog_date - # 1st paragraph as opener. TODO: allow a role or something marking - # where to actually pull from? - opener = doc.traverse(nodes.paragraph)[0] - data.append(Post(post, doc, title, date, opener)) - return data - -def replace_blog_post_lists(app, doctree, fromdocname): - """ - Replace blog_post_list nodes with ordered list-o-links to posts. - """ - # Obtain blog posts - post_names = filter(lambda x: x.startswith('blog/'), app.env.found_docs) - posts = map(lambda x: (x, app.env.get_doctree(x)), post_names) - # Build "list" of links/etc - post_links = [] - for post, doc, title, date, opener in get_posts(app): - # Link itself - uri = app.builder.get_relative_uri(fromdocname, post) - link = nodes.reference('', '', refdocname=post, refuri=uri) - # Title, bolded. TODO: use 'topic' or something maybe? - link.append(nodes.strong('', title)) - date = date.strftime("%Y-%m-%d") - # Meh @ not having great docutils nodes which map to this. - html = '<div class="timestamp"><span>%s</span></div>' % date - timestamp = nodes.raw(text=html, format='html') - # NOTE: may group these within another element later if styling - # necessitates it - group = [timestamp, nodes.paragraph('', '', link), opener] - post_links.extend(group) - - # Replace temp node(s) w/ expanded list-o-links - for node in doctree.traverse(blog_post_list): - node.replace_self(post_links) - -def rss_timestamp(timestamp): - # Use horribly inappropriate module for its magical daylight-savings-aware - # timezone madness. Props to Tinkerer for the idea. - return email.utils.formatdate( - time.mktime(timestamp.timetuple()), - localtime=True - ) - -def generate_rss(app): - # Meh at having to run this subroutine like 3x per build. Not worth trying - # to be clever for now tho. - posts_ = get_posts(app) - # LOL URLs - root = app.config.rss_link - if not root.endswith('/'): - root += '/' - # Oh boy - posts = [ - ( - root + app.builder.get_target_uri(x.name), - x.title, - str(x.opener[0]), # Grab inner text element from paragraph - rss_timestamp(x.date), - ) - for x in posts_ - ] - location = 'blog/rss.xml' - context = { - 'title': app.config.project, - 'link': root, - 'atom': root + location, - 'description': app.config.rss_description, - # 'posts' is sorted by date already - 'date': rss_timestamp(posts_[0].date), - 'posts': posts, - } - yield (location, context, 'rss.xml') - -def setup(app): - # Link in RSS feed back to main website, e.g. 'http://paramiko.org' - app.add_config_value('rss_link', None, '') - # Ditto for RSS description field - app.add_config_value('rss_description', None, '') - # Interprets date metadata in blog post documents - app.add_directive('date', BlogDateDirective) - # Inserts blog post list node (in e.g. a listing page) for replacement - # below - app.add_node(blog_post_list) - app.add_directive('blog-posts', BlogPostListDirective) - # Performs abovementioned replacement - app.connect('doctree-resolved', replace_blog_post_lists) - # Generates RSS page from whole cloth at page generation step - app.connect('html-collect-pages', generate_rss) diff --git a/sites/www/blog.rst b/sites/www/blog.rst deleted file mode 100644 index af9651e4..00000000 --- a/sites/www/blog.rst +++ /dev/null @@ -1,16 +0,0 @@ -==== -Blog -==== - -.. blog-posts directive gets replaced with an ordered list of blog posts. - -.. blog-posts:: - - -.. The following toctree ensures blog posts get processed. - -.. toctree:: - :hidden: - :glob: - - blog/* diff --git a/sites/www/blog/first-post.rst b/sites/www/blog/first-post.rst deleted file mode 100644 index 7b075073..00000000 --- a/sites/www/blog/first-post.rst +++ /dev/null @@ -1,7 +0,0 @@ -=========== -First post! -=========== - -A blog post. - -.. date:: 2013-12-04 diff --git a/sites/www/blog/second-post.rst b/sites/www/blog/second-post.rst deleted file mode 100644 index c4463f33..00000000 --- a/sites/www/blog/second-post.rst +++ /dev/null @@ -1,7 +0,0 @@ -=========== -Another one -=========== - -.. date:: 2013-12-05 - -Indeed! diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 4563877d..44bd61e9 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,82 @@ Changelog ========= +* :bug:`234 major` Lower logging levels for a few overly-noisy log messages + about secure channels. Thanks to David Pursehouse for noticing & contributing + the fix. +* :feature:`218` Add support for ECDSA private keys on the client side. Thanks + to ``@aszlig`` for the patch. +* :bug:`335 major` Fix ECDSA key generation (generation of brand new ECDSA keys + was broken previously). Thanks to ``@solarw`` for catch & patch. +* :feature:`184` Support quoted values in SSH config file parsing. Credit to + Yan Kalchevskiy. +* :feature:`131` Add a `~paramiko.sftp_client.SFTPClient.listdir_iter` method + to `~paramiko.sftp_client.SFTPClient` allowing for more efficient, + async/generator based file listings. Thanks to John Begeman. +* :support:`378 backported` Minor code cleanup in the SSH config module + courtesy of Olle Lundberg. +* :support:`249` Consolidate version information into one spot. Thanks to Gabi + Davar for the reminder. +* :release:`1.14.1 <2014-08-25>` +* :release:`1.13.2 <2014-08-25>` +* :bug:`376` Be less aggressive about expanding variables in ``ssh_config`` + files, which results in a speedup of SSH config parsing. Credit to Olle + Lundberg. +* :support:`324 backported` A bevvy of documentation typo fixes, courtesy of Roy + Wellington. +* :bug:`312` `paramiko.transport.Transport` had a bug in its ``__repr__`` which + surfaces during errors encountered within its ``__init__``, causing + problematic tracebacks in such situations. Thanks to Simon Percivall for + catch & patch. +* :bug:`272` Fix a bug where ``known_hosts`` parsing hashed the input hostname + as well as the hostnames from the ``known_hosts`` file, on every comparison. + Thanks to ``@sigmunau`` for final patch and ``@ostacey`` for the original + report. +* :bug:`239` Add Windows-style CRLF support to SSH config file parsing. Props + to Christopher Swenson. +* :support:`229 backported` Fix a couple of incorrectly-copied docstrings' ``.. + versionadded::`` RST directives. Thanks to Aarni Koskela for the catch. +* :support:`169 backported` Minor refactor of + `paramiko.sftp_client.SFTPClient.put` thanks to Abhinav Upadhyay. +* :bug:`285` (also :issue:`352`) Update our Python 3 ``b()`` compatibility shim + to handle ``buffer`` objects correctly; this fixes a frequently reported + issue affecting many users, including users of the ``bzr`` software suite. + Thanks to ``@basictheprogram`` for the initial report, Jelmer Vernooij for + the fix and Andrew Starr-Bochicchio & Jeremy T. Bouse (among others) for + discussion & feedback. +* :support:`371` Add Travis support & docs update for Python 3.4. Thanks to + Olle Lundberg. +* :release:`1.14.0 <2014-05-07>` +* :release:`1.13.1 <2014-05-07>` +* :release:`1.12.4 <2014-05-07>` +* :release:`1.11.6 <2014-05-07>` +* :bug:`-` `paramiko.file.BufferedFile.read` incorrectly returned text strings + after the Python 3 migration, despite bytes being more appropriate for file + contents (which may be binary or of an unknown encoding.) This has been + addressed. + + .. note:: + `paramiko.file.BufferedFile.readline` continues to return strings, not + bytes, as "lines" only make sense for textual data. It assumes UTF-8 by + default. + + This should fix `this issue raised on the Obnam mailing list + <http://comments.gmane.org/gmane.comp.sysutils.backup.obnam/252>`_. Thanks + to Antoine Brenner for the patch. +* :bug:`-` Added self.args for exception classes. Used for unpickling. Related + to (`Fabric #986 <https://github.com/fabric/fabric/issues/986>`_, `Fabric + #714 <https://github.com/fabric/fabric/issues/714>`_). Thanks to Alex + Plugaru. +* :bug:`-` Fix logging error in sftp_client for filenames containing the '%' + character. Thanks to Antoine Brenner. +* :bug:`308` Fix regression in dsskey.py that caused sporadic signature + verification failures. Thanks to Chris Rose. +* :support:`299` Use deterministic signatures for ECDSA keys for improved + security. Thanks to Alex Gaynor. +* :support:`297` Replace PyCrypto's ``Random`` with `os.urandom` for improved + speed and security. Thanks again to Alex. +* :support:`295` Swap out a bunch of PyCrypto hash functions with use of + `hashlib`. Thanks to Alex Gaynor. * :support:`290` (also :issue:`292`) Add support for building universal (Python 2+3 compatible) wheel files during the release process. Courtesy of Alex Gaynor. diff --git a/sites/www/conf.py b/sites/www/conf.py index 1c6c9254..0b0fb85c 100644 --- a/sites/www/conf.py +++ b/sites/www/conf.py @@ -6,28 +6,18 @@ from os.path import abspath, join, dirname sys.path.append(abspath(join(dirname(__file__), '..'))) from shared_conf import * -# Local blog extension -sys.path.append(abspath('.')) -extensions.append('blog') -rss_link = 'http://paramiko.org' -rss_description = 'Paramiko project news' - # Releases changelog extension extensions.append('releases') -releases_release_uri = "https://github.com/paramiko/paramiko/tree/%s" +# Paramiko 1.x tags start with 'v'. Meh. +releases_release_uri = "https://github.com/paramiko/paramiko/tree/v%s" releases_issue_uri = "https://github.com/paramiko/paramiko/issues/%s" -# Intersphinx for referencing API/usage docs -extensions.append('sphinx.ext.intersphinx') # Default is 'local' building, but reference the public docs site when building # under RTD. target = join(dirname(__file__), '..', 'docs', '_build') if os.environ.get('READTHEDOCS') == 'True': - # TODO: switch to docs.paramiko.org post go-live of sphinx API docs target = 'http://docs.paramiko.org/en/latest/' -intersphinx_mapping = { - 'docs': (target, None), -} +intersphinx_mapping['docs'] = (target, None) # Sister-site links to API docs html_theme_options['extra_nav_links'] = { diff --git a/sites/www/contact.rst b/sites/www/contact.rst index 2b6583f5..7e6c947e 100644 --- a/sites/www/contact.rst +++ b/sites/www/contact.rst @@ -9,3 +9,4 @@ following ways: * Mailing list: ``paramiko@librelist.com`` (see `the LibreList homepage <http://librelist.com>`_ for usage details). * This website - a blog section is forthcoming. +* Submit contributions on Github - see the :doc:`contributing` page. diff --git a/sites/www/contributing.rst b/sites/www/contributing.rst index 2b752cc5..a44414e8 100644 --- a/sites/www/contributing.rst +++ b/sites/www/contributing.rst @@ -5,15 +5,22 @@ Contributing How to get the code =================== -Our primary Git repository is on Github at `paramiko/paramiko -<https://github.com/paramiko/paramiko>`_; please follow their instructions for -cloning to your local system. (If you intend to submit patches/pull requests, -we recommend forking first, then cloning your fork. Github has excellent -documentation for all this.) +Our primary Git repository is on Github at `paramiko/paramiko`_; +please follow their instructions for cloning to your local system. (If you +intend to submit patches/pull requests, we recommend forking first, then +cloning your fork. Github has excellent documentation for all this.) How to submit bug reports or new code ===================================== Please see `this project-agnostic contribution guide -<http://contribution-guide.org>`_ - we follow it explicitly. +<http://contribution-guide.org>`_ - we follow it explicitly. Again, our code +repository and bug tracker is `on Github`_. + +Our current changelog is located in ``sites/www/changelog.rst`` - the top +level files like ``ChangeLog.*`` and ``NEWS`` are historical only. + + +.. _paramiko/paramiko: +.. _on Github: https://github.com/paramiko/paramiko diff --git a/sites/www/faq.rst b/sites/www/faq.rst index a7e80014..a5d9b383 100644 --- a/sites/www/faq.rst +++ b/sites/www/faq.rst @@ -7,3 +7,20 @@ Which version should I use? I see multiple active releases. Please see :ref:`the installation docs <release-lines>` which have an explicit section about this topic. + +Paramiko doesn't work with my Cisco, Windows or other non-Unix system! +====================================================================== + +In an ideal world, the developers would love to support every possible target +system. Unfortunately, volunteer development time and access to non-mainstream +platforms are limited, meaning that we can only fully support standard OpenSSH +implementations such as those found on the average Linux distribution (as well +as on Mac OS X and \*BSD.) + +Because of this, **we typically close bug reports for nonstandard SSH +implementations or host systems**. + +However, **closed does not imply locked** - affected users can still post +comments on such tickets - and **we will always consider actual patch +submissions for these issues**, provided they can get +1s from similarly +affected users and are proven to not break existing functionality. diff --git a/sites/www/index.rst b/sites/www/index.rst index cb3961ce..1b609709 100644 --- a/sites/www/index.rst +++ b/sites/www/index.rst @@ -11,20 +11,17 @@ contribution guidelines, development roadmap, news/blog, and so forth. Detailed usage and API documentation can be found at our code documentation site, `docs.paramiko.org <http://docs.paramiko.org>`_. +Please see the sidebar to the left to begin. + .. toctree:: + :hidden: + changelog FAQs <faq> installing contributing contact -.. Hide blog in hidden toctree for now (to avoid warnings.) - -.. toctree:: - :hidden: - - blog - .. rubric:: Footnotes diff --git a/sites/www/installing.rst b/sites/www/installing.rst index 74c5c6e8..052825c4 100644 --- a/sites/www/installing.rst +++ b/sites/www/installing.rst @@ -16,12 +16,12 @@ via `pip <http://pip-installer.org>`_:: Users who want the bleeding edge can install the development version via ``pip install paramiko==dev``. -We currently support **Python 2.6, 2.7 and 3.3** (Python **3.2** should also +We currently support **Python 2.6, 2.7 and 3.3+** (Python **3.2** should also work but has a less-strong compatibility guarantee from us.) Users on Python 2.5 or older are urged to upgrade. -Paramiko has two dependencies: the pure-Python ECDSA module `ecdsa`, and the -PyCrypto C extension. `ecdsa` is easily installable from wherever you +Paramiko has two dependencies: the pure-Python ECDSA module ``ecdsa``, and the +PyCrypto C extension. ``ecdsa`` is easily installable from wherever you obtained Paramiko's package; PyCrypto may require more work. Read on for details. @@ -32,7 +32,7 @@ Release lines Users desiring stability may wish to pin themselves to a specific release line once they first start using Paramiko; to assist in this, we guarantee bugfixes -for at least the last 2-3 releases including the latest stable one. This currently means Paramiko **1.11** through **1.13**. +for the last 2-3 releases including the latest stable one. If you're unsure which version to install, we have suggestions: @@ -36,8 +36,10 @@ def coverage(ctx): # Until we stop bundling docs w/ releases. Need to discover use cases first. -@task('docs') # Will invoke the API doc site build +@task def release(ctx): + # Build docs first. Use terribad workaround pending invoke #146 + ctx.run("inv docs") # Move the built docs into where Epydocs used to live target = 'docs' rmtree(target, ignore_errors=True) @@ -101,12 +101,12 @@ def main(): parser.add_option('-P', '--sftp-passwd', dest='password', type='string', default=default_passwd, metavar='<password>', help='[with -R] (optional) password to unlock the private key for remote sftp tests') - + options, args = parser.parse_args() - + # setup logging paramiko.util.log_to_file('test.log') - + if options.use_sftp: from tests.test_sftp import SFTPTest if options.use_loopback_sftp: @@ -149,10 +149,7 @@ def main(): # TODO: make that not a problem, jeez for thread in threading.enumerate(): if thread is not threading.currentThread(): - if PY2: - thread._Thread__stop() - else: - thread._stop() + thread.join(timeout=1) # Exit correctly if not result.wasSuccessful(): sys.exit(1) diff --git a/tests/test_client.py b/tests/test_client.py index 7e5c80b4..7c094628 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -29,10 +29,22 @@ import warnings import os from tests.util import test_path import paramiko -from paramiko.common import PY2 +from paramiko.common import PY2, b +from paramiko.ssh_exception import SSHException + + +FINGERPRINTS = { + 'ssh-dss': b'\x44\x78\xf0\xb9\xa2\x3c\xc5\x18\x20\x09\xff\x75\x5b\xc1\xd2\x6c', + 'ssh-rsa': b'\x60\x73\x38\x44\xcb\x51\x86\x65\x7f\xde\xda\xa2\x2b\x5a\x57\xd5', + 'ecdsa-sha2-nistp256': b'\x25\x19\xeb\x55\xe6\xa1\x47\xff\x4f\x38\xd2\x75\x6f\xa5\xd5\x60', +} class NullServer (paramiko.ServerInterface): + def __init__(self, *args, **kwargs): + # Allow tests to enable/disable specific key types + self.__allowed_keys = kwargs.pop('allowed_keys', []) + super(NullServer, self).__init__(*args, **kwargs) def get_allowed_auths(self, username): if username == 'slowdive': @@ -45,7 +57,14 @@ class NullServer (paramiko.ServerInterface): return paramiko.AUTH_FAILED def check_auth_publickey(self, username, key): - if (key.get_name() == 'ssh-dss') and key.get_fingerprint() == b'\x44\x78\xf0\xb9\xa2\x3c\xc5\x18\x20\x09\xff\x75\x5b\xc1\xd2\x6c': + try: + expected = FINGERPRINTS[key.get_name()] + except KeyError: + return paramiko.AUTH_FAILED + if ( + key.get_name() in self.__allowed_keys and + key.get_fingerprint() == expected + ): return paramiko.AUTH_SUCCESSFUL return paramiko.AUTH_FAILED @@ -72,32 +91,44 @@ class SSHClientTest (unittest.TestCase): if hasattr(self, attr): getattr(self, attr).close() - def _run(self): + def _run(self, allowed_keys=None): + if allowed_keys is None: + allowed_keys = FINGERPRINTS.keys() self.socks, addr = self.sockl.accept() self.ts = paramiko.Transport(self.socks) host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key')) self.ts.add_server_key(host_key) - server = NullServer() + server = NullServer(allowed_keys=allowed_keys) self.ts.start_server(self.event, server) - def test_1_client(self): + def _test_connection(self, **kwargs): """ - verify that the SSHClient stuff works too. + (Most) kwargs get passed directly into SSHClient.connect(). + + The exception is ``allowed_keys`` which is stripped and handed to the + ``NullServer`` used for testing. """ - threading.Thread(target=self._run).start() + run_kwargs = {'allowed_keys': kwargs.pop('allowed_keys', None)} + # Server setup + threading.Thread(target=self._run, kwargs=run_kwargs).start() host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key')) public_host_key = paramiko.RSAKey(data=host_key.asbytes()) + # Client setup self.tc = paramiko.SSHClient() self.tc.get_host_keys().add('[%s]:%d' % (self.addr, self.port), 'ssh-rsa', public_host_key) - self.tc.connect(self.addr, self.port, username='slowdive', password='pygmalion') + # Actual connection + self.tc.connect(self.addr, self.port, username='slowdive', **kwargs) + + # Authentication successful? self.event.wait(1.0) self.assertTrue(self.event.isSet()) self.assertTrue(self.ts.is_active()) self.assertEqual('slowdive', self.ts.get_username()) self.assertEqual(True, self.ts.is_authenticated()) + # Command execution functions? stdin, stdout, stderr = self.tc.exec_command('yes') schan = self.ts.accept(1.0) @@ -110,61 +141,71 @@ class SSHClientTest (unittest.TestCase): self.assertEqual('This is on stderr.\n', stderr.readline()) self.assertEqual('', stderr.readline()) + # Cleanup stdin.close() stdout.close() stderr.close() + def test_1_client(self): + """ + verify that the SSHClient stuff works too. + """ + self._test_connection(password='pygmalion') + def test_2_client_dsa(self): """ verify that SSHClient works with a DSA key. """ - threading.Thread(target=self._run).start() - host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key')) - public_host_key = paramiko.RSAKey(data=host_key.asbytes()) - - self.tc = paramiko.SSHClient() - self.tc.get_host_keys().add('[%s]:%d' % (self.addr, self.port), 'ssh-rsa', public_host_key) - self.tc.connect(self.addr, self.port, username='slowdive', key_filename=test_path('test_dss.key')) - - self.event.wait(1.0) - self.assertTrue(self.event.isSet()) - self.assertTrue(self.ts.is_active()) - self.assertEqual('slowdive', self.ts.get_username()) - self.assertEqual(True, self.ts.is_authenticated()) + self._test_connection(key_filename=test_path('test_dss.key')) - stdin, stdout, stderr = self.tc.exec_command('yes') - schan = self.ts.accept(1.0) - - schan.send('Hello there.\n') - schan.send_stderr('This is on stderr.\n') - schan.close() - - self.assertEqual('Hello there.\n', stdout.readline()) - self.assertEqual('', stdout.readline()) - self.assertEqual('This is on stderr.\n', stderr.readline()) - self.assertEqual('', stderr.readline()) + def test_client_rsa(self): + """ + verify that SSHClient works with an RSA key. + """ + self._test_connection(key_filename=test_path('test_rsa.key')) - stdin.close() - stdout.close() - stderr.close() + def test_2_5_client_ecdsa(self): + """ + verify that SSHClient works with an ECDSA key. + """ + self._test_connection(key_filename=test_path('test_ecdsa.key')) def test_3_multiple_key_files(self): """ verify that SSHClient accepts and tries multiple key files. """ - threading.Thread(target=self._run).start() - host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key')) - public_host_key = paramiko.RSAKey(data=host_key.asbytes()) - - self.tc = paramiko.SSHClient() - self.tc.get_host_keys().add('[%s]:%d' % (self.addr, self.port), 'ssh-rsa', public_host_key) - self.tc.connect(self.addr, self.port, username='slowdive', key_filename=[test_path('test_rsa.key'), test_path('test_dss.key')]) - - self.event.wait(1.0) - self.assertTrue(self.event.isSet()) - self.assertTrue(self.ts.is_active()) - self.assertEqual('slowdive', self.ts.get_username()) - self.assertEqual(True, self.ts.is_authenticated()) + # This is dumb :( + types_ = { + 'rsa': 'ssh-rsa', + 'dss': 'ssh-dss', + 'ecdsa': 'ecdsa-sha2-nistp256', + } + # Various combos of attempted & valid keys + # TODO: try every possible combo using itertools functions + for attempt, accept in ( + (['rsa', 'dss'], ['dss']), # Original test #3 + (['dss', 'rsa'], ['dss']), # Ordering matters sometimes, sadly + (['dss', 'rsa', 'ecdsa'], ['dss']), # Try ECDSA but fail + (['rsa', 'ecdsa'], ['ecdsa']), # ECDSA success + ): + self._test_connection( + key_filename=[ + test_path('test_{0}.key'.format(x)) for x in attempt + ], + allowed_keys=[types_[x] for x in accept], + ) + + def test_multiple_key_files_failure(self): + """ + Expect failure when multiple keys in play and none are accepted + """ + # Until #387 is fixed we have to catch a high-up exception since + # various platforms trigger different errors here >_< + self.assertRaises(SSHException, + self._test_connection, + key_filename=[test_path('test_rsa.key')], + allowed_keys=['ecdsa-sha2-nistp256'], + ) def test_4_auto_add_policy(self): """ diff --git a/tests/test_file.py b/tests/test_file.py index e11d7fd5..22a34aca 100755 --- a/tests/test_file.py +++ b/tests/test_file.py @@ -23,6 +23,7 @@ Some unit tests for the BufferedFile abstraction. import unittest from paramiko.file import BufferedFile from paramiko.common import linefeed_byte, crlf, cr_byte +import sys class LoopbackFile (BufferedFile): @@ -53,7 +54,7 @@ class BufferedFileTest (unittest.TestCase): def test_1_simple(self): f = LoopbackFile('r') try: - f.write('hi') + f.write(b'hi') self.assertTrue(False, 'no exception on write to read-only file') except: pass @@ -69,7 +70,7 @@ class BufferedFileTest (unittest.TestCase): def test_2_readline(self): f = LoopbackFile('r+U') - f.write('First line.\nSecond line.\r\nThird line.\nFinal line non-terminated.') + f.write(b'First line.\nSecond line.\r\nThird line.\nFinal line non-terminated.') self.assertEqual(f.readline(), 'First line.\n') # universal newline mode should convert this linefeed: self.assertEqual(f.readline(), 'Second line.\n') @@ -93,9 +94,9 @@ class BufferedFileTest (unittest.TestCase): try to trick the linefeed detector. """ f = LoopbackFile('r+U') - f.write('First line.\r') + f.write(b'First line.\r') self.assertEqual(f.readline(), 'First line.\n') - f.write('\nSecond.\r\n') + f.write(b'\nSecond.\r\n') self.assertEqual(f.readline(), 'Second.\n') f.close() self.assertEqual(f.newlines, crlf) @@ -105,7 +106,7 @@ class BufferedFileTest (unittest.TestCase): verify that write buffering is on. """ f = LoopbackFile('r+', 1) - f.write('Complete line.\nIncomplete line.') + f.write(b'Complete line.\nIncomplete line.') self.assertEqual(f.readline(), 'Complete line.\n') self.assertEqual(f.readline(), '') f.write('..\n') @@ -118,12 +119,12 @@ class BufferedFileTest (unittest.TestCase): """ f = LoopbackFile('r+', 512) f.write('Not\nquite\n512 bytes.\n') - self.assertEqual(f.read(1), '') + self.assertEqual(f.read(1), b'') f.flush() - self.assertEqual(f.read(5), 'Not\nq') - self.assertEqual(f.read(10), 'uite\n512 b') - self.assertEqual(f.read(9), 'ytes.\n') - self.assertEqual(f.read(3), '') + self.assertEqual(f.read(5), b'Not\nq') + self.assertEqual(f.read(10), b'uite\n512 b') + self.assertEqual(f.read(9), b'ytes.\n') + self.assertEqual(f.read(3), b'') f.close() def test_6_buffering(self): @@ -131,12 +132,12 @@ class BufferedFileTest (unittest.TestCase): verify that flushing happens automatically on buffer crossing. """ f = LoopbackFile('r+', 16) - f.write('Too small.') - self.assertEqual(f.read(4), '') - f.write(' ') - self.assertEqual(f.read(4), '') - f.write('Enough.') - self.assertEqual(f.read(20), 'Too small. Enough.') + f.write(b'Too small.') + self.assertEqual(f.read(4), b'') + f.write(b' ') + self.assertEqual(f.read(4), b'') + f.write(b'Enough.') + self.assertEqual(f.read(20), b'Too small. Enough.') f.close() def test_7_read_all(self): @@ -144,9 +145,23 @@ class BufferedFileTest (unittest.TestCase): verify that read(-1) returns everything left in the file. """ f = LoopbackFile('r+', 16) - f.write('The first thing you need to do is open your eyes. ') - f.write('Then, you need to close them again.\n') + f.write(b'The first thing you need to do is open your eyes. ') + f.write(b'Then, you need to close them again.\n') s = f.read(-1) - self.assertEqual(s, 'The first thing you need to do is open your eyes. Then, you ' + - 'need to close them again.\n') + self.assertEqual(s, b'The first thing you need to do is open your eyes. Then, you ' + + b'need to close them again.\n') f.close() + + def test_8_buffering(self): + """ + verify that buffered objects can be written + """ + if sys.version_info[0] == 2: + f = LoopbackFile('r+', 16) + f.write(buffer(b'Too small.')) + f.close() + +if __name__ == '__main__': + from unittest import main + main() + diff --git a/tests/test_kex.py b/tests/test_kex.py index c522be46..56f1b7c7 100644 --- a/tests/test_kex.py +++ b/tests/test_kex.py @@ -21,7 +21,9 @@ Some unit tests for the key exchange protocols. """ from binascii import hexlify +import os import unittest + import paramiko.util from paramiko.kex_group1 import KexGroup1 from paramiko.kex_gex import KexGex @@ -29,9 +31,8 @@ from paramiko import Message from paramiko.common import byte_chr -class FakeRng (object): - def read(self, n): - return byte_chr(0xcc) * n +def dummy_urandom(n): + return byte_chr(0xcc) * n class FakeKey (object): @@ -41,7 +42,7 @@ class FakeKey (object): def asbytes(self): return b'fake-key' - def sign_ssh_data(self, rng, H): + def sign_ssh_data(self, H): return b'fake-sig' @@ -53,8 +54,7 @@ class FakeModulusPack (object): return self.G, self.P -class FakeTransport (object): - rng = FakeRng() +class FakeTransport(object): local_version = 'SSH-2.0-paramiko_1.0' remote_version = 'SSH-2.0-lame' local_kex_init = 'local-kex-init' @@ -91,10 +91,11 @@ class KexTest (unittest.TestCase): K = 14730343317708716439807310032871972459448364195094179797249681733965528989482751523943515690110179031004049109375612685505881911274101441415545039654102474376472240501616988799699744135291070488314748284283496055223852115360852283821334858541043710301057312858051901453919067023103730011648890038847384890504 def setUp(self): - pass + self._original_urandom = os.urandom + os.urandom = dummy_urandom def tearDown(self): - pass + os.urandom = self._original_urandom def test_1_group1_client(self): transport = FakeTransport() diff --git a/tests/test_packetizer.py b/tests/test_packetizer.py index d4d5544e..a8c0f973 100644 --- a/tests/test_packetizer.py +++ b/tests/test_packetizer.py @@ -21,9 +21,12 @@ Some unit tests for the ssh2 protocol in Transport. """ import unittest +from hashlib import sha1 + from tests.loop import LoopSocket + from Crypto.Cipher import AES -from Crypto.Hash import SHA + from paramiko import Message, Packetizer, util from paramiko.common import byte_chr, zero_byte @@ -41,7 +44,7 @@ class PacketizerTest (unittest.TestCase): p.set_log(util.get_logger('paramiko.transport')) p.set_hexdump(True) cipher = AES.new(zero_byte * 16, AES.MODE_CBC, x55 * 16) - p.set_outbound_cipher(cipher, 16, SHA, 12, x1f * 20) + p.set_outbound_cipher(cipher, 16, sha1, 12, x1f * 20) # message has to be at least 16 bytes long, so we'll have at least one # block of data encrypted that contains zero random padding bytes @@ -64,7 +67,7 @@ class PacketizerTest (unittest.TestCase): p.set_log(util.get_logger('paramiko.transport')) p.set_hexdump(True) cipher = AES.new(zero_byte * 16, AES.MODE_CBC, x55 * 16) - p.set_inbound_cipher(cipher, 16, SHA, 12, x1f * 20) + p.set_inbound_cipher(cipher, 16, sha1, 12, x1f * 20) wsock.send(b'\x43\x91\x97\xbd\x5b\x50\xac\x25\x87\xc2\xc4\x6b\xc7\xe9\x38\xc0\x90\xd2\x16\x56\x0d\x71\x73\x61\x38\x7c\x4c\x3d\xfb\x97\x7d\xe2\x6e\x03\xb1\xa0\xc2\x1c\xd6\x41\x41\x4c\xb4\x59') cmd, m = p.read_message() self.assertEqual(100, cmd) diff --git a/tests/test_pkey.py b/tests/test_pkey.py index 6ff68fc2..1468ee27 100644 --- a/tests/test_pkey.py +++ b/tests/test_pkey.py @@ -20,11 +20,13 @@ Some unit tests for public/private key objects. """ -from binascii import hexlify import unittest +from binascii import hexlify +from hashlib import md5 + from paramiko import RSAKey, DSSKey, ECDSAKey, Message, util from paramiko.py3compat import StringIO, byte_chr, b, bytes -from paramiko.common import rng + from tests.util import test_path # from openssh's ssh-keygen @@ -90,8 +92,7 @@ class KeyTest (unittest.TestCase): pass def test_1_generate_key_bytes(self): - from Crypto.Hash import MD5 - key = util.generate_key_bytes(MD5, x1234, 'happy birthday', 30) + key = util.generate_key_bytes(md5, x1234, 'happy birthday', 30) exp = b'\x61\xE1\xF2\x72\xF4\xC1\xC4\x56\x15\x86\xBD\x32\x24\x98\xC0\xE9\x24\x67\x27\x80\xF4\x7B\xB3\x7D\xDA\x7D\x54\x01\x9E\x64' self.assertEqual(exp, key) @@ -166,7 +167,7 @@ class KeyTest (unittest.TestCase): def test_8_sign_rsa(self): # verify that the rsa private key can sign and verify key = RSAKey.from_private_key_file(test_path('test_rsa.key')) - msg = key.sign_ssh_data(rng, b'ice weasels') + msg = key.sign_ssh_data(b'ice weasels') self.assertTrue(type(msg) is Message) msg.rewind() self.assertEqual('ssh-rsa', msg.get_text()) @@ -179,7 +180,7 @@ class KeyTest (unittest.TestCase): def test_9_sign_dss(self): # verify that the dss private key can sign and verify key = DSSKey.from_private_key_file(test_path('test_dss.key')) - msg = key.sign_ssh_data(rng, b'ice weasels') + msg = key.sign_ssh_data(b'ice weasels') self.assertTrue(type(msg) is Message) msg.rewind() self.assertEqual('ssh-dss', msg.get_text()) @@ -193,13 +194,13 @@ class KeyTest (unittest.TestCase): def test_A_generate_rsa(self): key = RSAKey.generate(1024) - msg = key.sign_ssh_data(rng, b'jerri blank') + msg = key.sign_ssh_data(b'jerri blank') msg.rewind() self.assertTrue(key.verify_ssh_sig(b'jerri blank', msg)) def test_B_generate_dss(self): key = DSSKey.generate(1024) - msg = key.sign_ssh_data(rng, b'jerri blank') + msg = key.sign_ssh_data(b'jerri blank') msg.rewind() self.assertTrue(key.verify_ssh_sig(b'jerri blank', msg)) @@ -240,7 +241,7 @@ class KeyTest (unittest.TestCase): def test_13_sign_ecdsa(self): # verify that the rsa private key can sign and verify key = ECDSAKey.from_private_key_file(test_path('test_ecdsa.key')) - msg = key.sign_ssh_data(rng, b'ice weasels') + msg = key.sign_ssh_data(b'ice weasels') self.assertTrue(type(msg) is Message) msg.rewind() self.assertEqual('ecdsa-sha2-nistp256', msg.get_text()) diff --git a/tests/test_sftp.py b/tests/test_sftp.py index 6417ac90..1ae9781d 100755 --- a/tests/test_sftp.py +++ b/tests/test_sftp.py @@ -37,6 +37,7 @@ from paramiko.common import o777, o600, o666, o644 from tests.stub_sftp import StubServer, StubSFTPServer from tests.loop import LoopSocket from tests.util import test_path +import paramiko.util from paramiko.sftp_attr import SFTPAttributes ARTICLE = ''' @@ -66,6 +67,18 @@ liver insulin receptors. Their sensitivity to insulin is, however, similarly decreased compared with chicken. ''' + +# Here is how unicode characters are encoded over 1 to 6 bytes in utf-8 +# U-00000000 - U-0000007F: 0xxxxxxx +# U-00000080 - U-000007FF: 110xxxxx 10xxxxxx +# U-00000800 - U-0000FFFF: 1110xxxx 10xxxxxx 10xxxxxx +# U-00010000 - U-001FFFFF: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx +# U-00200000 - U-03FFFFFF: 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx +# U-04000000 - U-7FFFFFFF: 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx +# Note that: hex(int('11000011',2)) == '0xc3' +# Thus, the following 2-bytes sequence is not valid utf8: "invalid continuation byte" +NON_UTF8_DATA = b'\xC3\xC3' + FOLDER = os.environ.get('TEST_FOLDER', 'temp-testing000') sftp = None @@ -266,8 +279,8 @@ class SFTPTest (unittest.TestCase): def test_7_listdir(self): """ - verify that a folder can be created, a bunch of files can be placed in it, - and those files show up in sftp.listdir. + verify that a folder can be created, a bunch of files can be placed in + it, and those files show up in sftp.listdir. """ try: sftp.open(FOLDER + '/duck.txt', 'w').close() @@ -285,6 +298,26 @@ class SFTPTest (unittest.TestCase): sftp.remove(FOLDER + '/fish.txt') sftp.remove(FOLDER + '/tertiary.py') + def test_7_5_listdir_iter(self): + """ + listdir_iter version of above test + """ + try: + sftp.open(FOLDER + '/duck.txt', 'w').close() + sftp.open(FOLDER + '/fish.txt', 'w').close() + sftp.open(FOLDER + '/tertiary.py', 'w').close() + + x = [x.filename for x in sftp.listdir_iter(FOLDER)] + self.assertEqual(len(x), 3) + self.assertTrue('duck.txt' in x) + self.assertTrue('fish.txt' in x) + self.assertTrue('tertiary.py' in x) + self.assertTrue('random' not in x) + finally: + sftp.remove(FOLDER + '/duck.txt') + sftp.remove(FOLDER + '/fish.txt') + sftp.remove(FOLDER + '/tertiary.py') + def test_8_setstat(self): """ verify that the setstat functions (chown, chmod, utime, truncate) work. @@ -404,7 +437,7 @@ class SFTPTest (unittest.TestCase): self.assertEqual(sftp.stat(FOLDER + '/testing.txt').st_size, 13) with sftp.open(FOLDER + '/testing.txt', 'r') as f: data = f.read(20) - self.assertEqual(data, 'hello kiddy.\n') + self.assertEqual(data, b'hello kiddy.\n') finally: sftp.remove(FOLDER + '/testing.txt') @@ -465,8 +498,8 @@ class SFTPTest (unittest.TestCase): f.write('?\n') with sftp.open(FOLDER + '/happy.txt', 'r') as f: - self.assertEqual(f.readline(), 'full line?\n') - self.assertEqual(f.read(7), 'partial') + self.assertEqual(f.readline(), u('full line?\n')) + self.assertEqual(f.read(7), b'partial') finally: try: sftp.remove(FOLDER + '/happy.txt') @@ -661,8 +694,8 @@ class SFTPTest (unittest.TestCase): fd, localname = mkstemp() os.close(fd) - text = 'All I wanted was a plastic bunny rabbit.\n' - with open(localname, 'w') as f: + text = b'All I wanted was a plastic bunny rabbit.\n' + with open(localname, 'wb') as f: f.write(text) saved_progress = [] @@ -732,7 +765,40 @@ class SFTPTest (unittest.TestCase): sftp.remove(target) + def test_N_file_with_percent(self): + """ + verify that we can create a file with a '%' in the filename. + ( it needs to be properly escaped by _log() ) + """ + self.assertTrue( paramiko.util.get_logger("paramiko").handlers, "This unit test requires logging to be enabled" ) + f = sftp.open(FOLDER + '/test%file', 'w') + try: + self.assertEqual(f.stat().st_size, 0) + finally: + f.close() + sftp.remove(FOLDER + '/test%file') + + + def test_O_non_utf8_data(self): + """Test write() and read() of non utf8 data""" + try: + with sftp.open('%s/nonutf8data' % FOLDER, 'w') as f: + f.write(NON_UTF8_DATA) + with sftp.open('%s/nonutf8data' % FOLDER, 'r') as f: + data = f.read() + self.assertEqual(data, NON_UTF8_DATA) + with sftp.open('%s/nonutf8data' % FOLDER, 'wb') as f: + f.write(NON_UTF8_DATA) + with sftp.open('%s/nonutf8data' % FOLDER, 'rb') as f: + data = f.read() + self.assertEqual(data, NON_UTF8_DATA) + finally: + sftp.remove('%s/nonutf8data' % FOLDER) + + if __name__ == '__main__': SFTPTest.init_loopback() + # logging is required by test_N_file_with_percent + paramiko.util.log_to_file('test_sftp.log') from unittest import main main() diff --git a/tests/test_sftp_big.py b/tests/test_sftp_big.py index 521fbdc8..abed27b8 100644 --- a/tests/test_sftp_big.py +++ b/tests/test_sftp_big.py @@ -85,7 +85,7 @@ class BigSFTPTest (unittest.TestCase): write a 1MB file with no buffering. """ sftp = get_sftp() - kblob = (1024 * 'x') + kblob = (1024 * b'x') start = time.time() try: with sftp.open('%s/hongry.txt' % FOLDER, 'w') as f: @@ -231,7 +231,7 @@ class BigSFTPTest (unittest.TestCase): without using it, to verify that paramiko doesn't get confused. """ sftp = get_sftp() - kblob = (1024 * 'x') + kblob = (1024 * b'x') try: with sftp.open('%s/hongry.txt' % FOLDER, 'w') as f: f.set_pipelined(True) diff --git a/tests/test_util.py b/tests/test_util.py index 6bde4045..394ea553 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -23,7 +23,8 @@ Some unit tests for utility functions. from binascii import hexlify import errno import os -from Crypto.Hash import SHA +from hashlib import sha1 + import paramiko.util from paramiko.util import lookup_ssh_host_config as host_config from paramiko.py3compat import StringIO, byte_ord @@ -136,7 +137,7 @@ class UtilTest(ParamikoTest): ) def test_4_generate_key_bytes(self): - x = paramiko.util.generate_key_bytes(SHA, b'ABCDEFGH', 'This is my secret passphrase.', 64) + x = paramiko.util.generate_key_bytes(sha1, b'ABCDEFGH', 'This is my secret passphrase.', 64) hex = ''.join(['%02x' % byte_ord(c) for c in x]) self.assertEqual(hex, '9110e2f6793b69363e58173e9436b13a5a4b339005741d5c680e505f57d871347b4239f14fb5c46e857d5e100424873ba849ac699cea98d729e57b3e84378e8b') @@ -153,12 +154,6 @@ class UtilTest(ParamikoTest): finally: os.unlink('hostfile.temp') - def test_6_random(self): - from paramiko.common import rng - # just verify that we can pull out 32 bytes and not get an exception. - x = rng.read(32) - self.assertEqual(len(x), 32) - def test_7_host_config_expose_issue_33(self): test_config_file = """ Host www13.* @@ -338,3 +333,109 @@ IdentityFile something_%l_using_fqdn """ config = paramiko.util.parse_ssh_config(StringIO(test_config)) assert config.lookup('meh') # will die during lookup() if bug regresses + + def test_13_config_dos_crlf_succeeds(self): + config_file = StringIO("host abcqwerty\r\nHostName 127.0.0.1\r\n") + config = paramiko.SSHConfig() + config.parse(config_file) + self.assertEqual(config.lookup("abcqwerty")["hostname"], "127.0.0.1") + + def test_quoted_host_names(self): + test_config_file = """\ +Host "param pam" param "pam" + Port 1111 + +Host "param2" + Port 2222 + +Host param3 parara + Port 3333 + +Host param4 "p a r" "p" "par" para + Port 4444 +""" + res = { + 'param pam': {'hostname': 'param pam', 'port': '1111'}, + 'param': {'hostname': 'param', 'port': '1111'}, + 'pam': {'hostname': 'pam', 'port': '1111'}, + + 'param2': {'hostname': 'param2', 'port': '2222'}, + + 'param3': {'hostname': 'param3', 'port': '3333'}, + 'parara': {'hostname': 'parara', 'port': '3333'}, + + 'param4': {'hostname': 'param4', 'port': '4444'}, + 'p a r': {'hostname': 'p a r', 'port': '4444'}, + 'p': {'hostname': 'p', 'port': '4444'}, + 'par': {'hostname': 'par', 'port': '4444'}, + 'para': {'hostname': 'para', 'port': '4444'}, + } + f = StringIO(test_config_file) + config = paramiko.util.parse_ssh_config(f) + for host, values in res.items(): + self.assertEquals( + paramiko.util.lookup_ssh_host_config(host, config), + values + ) + + def test_quoted_params_in_config(self): + test_config_file = """\ +Host "param pam" param "pam" + IdentityFile id_rsa + +Host "param2" + IdentityFile "test rsa key" + +Host param3 parara + IdentityFile id_rsa + IdentityFile "test rsa key" +""" + res = { + 'param pam': {'hostname': 'param pam', 'identityfile': ['id_rsa']}, + 'param': {'hostname': 'param', 'identityfile': ['id_rsa']}, + 'pam': {'hostname': 'pam', 'identityfile': ['id_rsa']}, + + 'param2': {'hostname': 'param2', 'identityfile': ['test rsa key']}, + + 'param3': {'hostname': 'param3', 'identityfile': ['id_rsa', 'test rsa key']}, + 'parara': {'hostname': 'parara', 'identityfile': ['id_rsa', 'test rsa key']}, + } + f = StringIO(test_config_file) + config = paramiko.util.parse_ssh_config(f) + for host, values in res.items(): + self.assertEquals( + paramiko.util.lookup_ssh_host_config(host, config), + values + ) + + def test_quoted_host_in_config(self): + conf = SSHConfig() + correct_data = { + 'param': ['param'], + '"param"': ['param'], + + 'param pam': ['param', 'pam'], + '"param" "pam"': ['param', 'pam'], + '"param" pam': ['param', 'pam'], + 'param "pam"': ['param', 'pam'], + + 'param "pam" p': ['param', 'pam', 'p'], + '"param" pam "p"': ['param', 'pam', 'p'], + + '"pa ram"': ['pa ram'], + '"pa ram" pam': ['pa ram', 'pam'], + 'param "p a m"': ['param', 'p a m'], + } + incorrect_data = [ + 'param"', + '"param', + 'param "pam', + 'param "pam" "p a', + ] + for host, values in correct_data.items(): + self.assertEquals( + conf._get_hosts(host), + values + ) + for host in incorrect_data: + self.assertRaises(Exception, conf._get_hosts, host) @@ -1,5 +1,5 @@ [tox] -envlist = py25,py26,py27,py32,py33 +envlist = py26,py27,py32,py33,py34 [testenv] commands = pip install --use-mirrors -q -r tox-requirements.txt |