Skip to content

Commit

Permalink
Merge branch 'master' into 335-int
Browse files Browse the repository at this point in the history
  • Loading branch information
bitprophet committed Sep 5, 2014
2 parents 8455a8a + b802286 commit 8ece907
Show file tree
Hide file tree
Showing 24 changed files with 378 additions and 85 deletions.
7 changes: 4 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ python:
- "2.7"
- "3.2"
- "3.3"
- "3.4"
install:
# Self-install for setup.py-driven deps
- pip install -e .
Expand All @@ -17,16 +18,16 @@ 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"
template:
- "%{repository}@%{branch}: %{message} (%{build_url})"
on_success: change
on_failure: change
use_notice: true
email: false
after_success:
- coveralls
5 changes: 2 additions & 3 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,10 +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.6.0
alabaster>=0.6.1
releases>=0.5.2
wheel==0.23.0
3 changes: 1 addition & 2 deletions paramiko/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <[email protected]>"
__version__ = "1.14.0"
__version_info__ = tuple([ int(d) for d in __version__.split(".") ])
__license__ = "GNU Lesser General Public License (LGPL)"


Expand Down
2 changes: 2 additions & 0 deletions paramiko/_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
__version_info__ = (1, 15, 0)
__version__ = '.'.join(map(str, __version_info__))
14 changes: 7 additions & 7 deletions paramiko/channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ def exit_status_ready(self):
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.
Expand Down Expand Up @@ -330,7 +330,7 @@ def request_x11(self, screen_number=0, auth_protocol=None, auth_cookie=None,
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
Expand All @@ -339,7 +339,7 @@ def request_x11(self, screen_number=0, auth_protocol=None, auth_cookie=None,
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
Expand Down Expand Up @@ -744,10 +744,10 @@ def sendall(self, s):
: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.
"""
Expand All @@ -771,7 +771,7 @@ def sendall_stderr(self, s):
: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
"""
Expand Down Expand Up @@ -812,7 +812,7 @@ def makefile_stderr(self, *params):
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
Expand Down
102 changes: 63 additions & 39 deletions paramiko/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
import socket

SSH_PORT = 22
proxy_re = re.compile(r"^(proxycommand)\s*=*\s*(.*)", re.I)


class SSHConfig (object):
Expand All @@ -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.
Expand All @@ -53,44 +54,39 @@ def parse(self, file_obj):
: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):
Expand All @@ -111,8 +107,10 @@ def lookup(self, hostname):
: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:
Expand All @@ -128,7 +126,7 @@ def lookup(self, hostname):
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:]):
Expand Down Expand Up @@ -200,12 +198,38 @@ def _expand_variables(self, config, hostname):
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):
"""
Expand Down
2 changes: 1 addition & 1 deletion paramiko/hostkeys.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ def keys(self):
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
Expand Down
4 changes: 4 additions & 0 deletions paramiko/py3compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ def b(s, encoding='utf8'): # NOQA
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)

Expand All @@ -49,6 +51,8 @@ def u(s, encoding='utf8'): # NOQA
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)

Expand Down
Loading

0 comments on commit 8ece907

Please sign in to comment.