Compare commits

..

16 Commits

Author SHA1 Message Date
Dorian bec140c933 [Python 3]: Migration path for raw_input to input in demos. 2013-10-28 07:44:04 -04:00
Dorian 3cb91a0e9e [Python 3]: Added fix for bytestring instead of unicode string decoding in Python 3. 2013-10-25 17:29:32 -04:00
Dorian 9ffd9efb20 [Python 3]: Added fix for enabling proper adding of integers under both Python 2 and 3. 2013-10-25 17:28:54 -04:00
Dorian 3c33c763a7 [Python 3]: Wrapped unicode and string type checks in six's type definitions. Absolute import fixes for Windows agents. 2013-10-25 07:47:01 -04:00
Dorian 387f243c1d [Python 3]: Workaround epydoc exceptions hack, since epydoc does not work on Python 3. 2013-10-24 20:00:36 -04:00
Dorian 2c538bd5fe [Python 3]: Hack around __slots__ and properties conflict. 2013-10-24 19:59:57 -04:00
Dorian 5a9fc81ad9 [Python 3]: Migration for UserDict.DictMixins for Python 3. 2013-10-24 18:26:02 -04:00
Dorian cc2dd9c0f4 [Python 3]: Workaround for long values in Python 3's unified integer types. paramiko.message not worked around for now. 2013-10-24 08:35:38 -04:00
Dorian 1022eec17a [Python 3]: Fixes for StringIO and removed semi-colons. 2013-10-24 08:15:10 -04:00
Dorian b94fce4df9 [Python 3]: Added workaround for unified integer and long types in Python 3. 2013-10-24 01:07:04 -04:00
Dorian b2f74c1291 [Python 3]: Migration path for imports. Using absolute imports. 2013-10-23 07:34:55 -04:00
Dorian a77054d632 [Python 3]: Removed deprecated tuple unpacking. 2013-10-23 07:34:55 -04:00
Dorian 3bad2a13be [Python 3]: Fix for builtins using six. 2013-10-23 07:34:55 -04:00
Dorian 86fe372a2c [Python 3]: New octal syntax. 2013-08-13 17:32:30 -04:00
Dorian b1e235d820 [Python 3]: New except syntax. 2013-08-13 17:32:30 -04:00
Dorian 0847ac780b [Python 3]: Migrated to print functions. 2013-08-13 17:32:21 -04:00
107 changed files with 6241 additions and 7051 deletions

4
.gitignore vendored
View File

@ -5,6 +5,4 @@ dist/
paramiko.egg-info/ paramiko.egg-info/
test.log test.log
docs/ docs/
!sites/docs .idea/
_build
.coverage

View File

@ -1,32 +1,14 @@
language: python language: python
python: python:
- "2.5"
- "2.6" - "2.6"
- "2.7" - "2.7"
- "3.2"
- "3.3"
install: install:
# Self-install for setup.py-driven deps # Self-install for setup.py-driven deps
- pip install -e . - pip install -e .
# Dev (doc/test running) requirements script: python test.py
- pip install coveralls # For coveralls.io specifically
- pip install -r dev-requirements.txt
script:
# Main tests, with coverage!
- invoke coverage
# Ensure documentation & invoke pipeline run OK.
# 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
notifications: notifications:
irc: irc:
channels: "irc.freenode.org#paramiko" channels: "irc.freenode.org#paramiko"
template:
- "%{repository}@%{branch}: %{message} (%{build_url})"
on_success: change on_success: change
on_failure: change on_failure: change
use_notice: true
email: false
after_success:
- coveralls

15
Makefile Normal file
View File

@ -0,0 +1,15 @@
release: docs
python setup.py sdist register upload
docs: paramiko/*
epydoc --no-private -o docs/ paramiko
clean:
rm -rf build dist docs
rm -f MANIFEST *.log demos/*.log
rm -f paramiko/*.pyc
rm -f test.log
rm -rf paramiko.egg-info
test:
python ./test.py

79
NEWS
View File

@ -9,14 +9,83 @@ Issues noted as "'ssh' #NN" can be found at https://github.com/bitprophet/ssh/.
Issues noted as "Fabric #NN" can be found at https://github.com/fabric/fabric/. Issues noted as "Fabric #NN" can be found at https://github.com/fabric/fabric/.
**PLEASE NOTE:** For changes in 1.10.x and newer releases, please see
www.paramiko.org's changelog page, or the source file, sites/www/changelog.rst
Releases Releases
======== ========
v1.11.0 (26th Jul 2013)
-----------------------
* #98: On Windows, when interacting with the PuTTY PAgeant, Paramiko now
creates the shared memory map with explicit Security Attributes of the user,
which is the same technique employed by the canonical PuTTY library to avoid
permissions issues when Paramiko is running under a different UAC context
than the PuTTY Ageant process. Thanks to Jason R. Coombs for the patch.
* #100: Remove use of PyWin32 in `win_pageant` module. Module was already
dependent on ctypes for constructing appropriate structures and had ctypes
implementations of all functionality. Thanks to Jason R. Coombs for the
patch.
* #87: Ensure updates to `known_hosts` files account for any updates to said
files after Paramiko initially read them. (Includes related fix to guard
against duplicate entries during subsequent `known_hosts` loads.) Thanks to
`@sunweaver` for the contribution.
v1.10.2 (26th Jul 2013)
-----------------------
* #153, #67: Warn on parse failure when reading known_hosts file. Thanks to
`@glasserc` for patch.
* #146: Indentation fixes for readability. Thanks to Abhinav Upadhyay for catch
& patch.
v1.10.1 (5th Apr 2013)
----------------------
* #142: (Fabric #811) SFTP put of empty file will still return the attributes
of the put file. Thanks to Jason R. Coombs for the patch.
* #154: (Fabric #876) Forwarded SSH agent connections left stale local pipes
lying around, which could cause local (and sometimes remote or network)
resource starvation when running many agent-using remote commands. Thanks to
Kevin Tegtmeier for catch & patch.
v1.10.0 (1st Mar 2013)
--------------------
* #66: Batch SFTP writes to help speed up file transfers. Thanks to Olle
Lundberg for the patch.
* #133: Fix handling of window-change events to be on-spec and not
attempt to wait for a response from the remote sshd; this fixes problems with
less common targets such as some Cisco devices. Thanks to Phillip Heller for
catch & patch.
* #93: Overhaul SSH config parsing to be in line with `man ssh_config` (& the
behavior of `ssh` itself), including addition of parameter expansion within
config values. Thanks to Olle Lundberg for the patch.
* #110: Honor SSH config `AddressFamily` setting when looking up local
host's FQDN. Thanks to John Hensley for the patch.
* #128: Defer FQDN resolution until needed, when parsing SSH config files.
Thanks to Parantapa Bhattacharya for catch & patch.
* #102: Forego random padding for packets when running under `*-ctr` ciphers.
This corrects some slowdowns on platforms where random byte generation is
inefficient (e.g. Windows). Thanks to `@warthog618` for catch & patch, and
Michael van der Kolff for code/technique review.
* #127: Turn `SFTPFile` into a context manager. Thanks to Michael Williamson
for the patch.
* #116: Limit `Message.get_bytes` to an upper bound of 1MB to protect against
potential DoS vectors. Thanks to `@mvschaik` for catch & patch.
* #115: Add convenience `get_pty` kwarg to `Client.exec_command` so users not
manually controlling a channel object can still toggle PTY creation. Thanks
to Michael van der Kolff for the patch.
* #71: Add `SFTPClient.putfo` and `.getfo` methods to allow direct
uploading/downloading of file-like objects. Thanks to Eric Buehl for the
patch.
* #113: Add `timeout` parameter to `SSHClient.exec_command` for easier setting
of the command's internal channel object's timeout. Thanks to Cernov Vladimir
for the patch.
* #94: Remove duplication of SSH port constant. Thanks to Olle Lundberg for the
catch.
* #80: Expose the internal "is closed" property of the file transfer class
`BufferedFile` as `.closed`, better conforming to Python's file interface.
Thanks to `@smunaut` and James Hiscock for catch & patch.
v1.9.0 (6th Nov 2012) v1.9.0 (6th Nov 2012)
--------------------- ---------------------

13
README
View File

@ -5,7 +5,7 @@ paramiko
:Paramiko: Python SSH module :Paramiko: Python SSH module
:Copyright: Copyright (c) 2003-2009 Robey Pointer <robeypointer@gmail.com> :Copyright: Copyright (c) 2003-2009 Robey Pointer <robeypointer@gmail.com>
:Copyright: Copyright (c) 2013-2014 Jeff Forcier <jeff@bitprophet.org> :Copyright: Copyright (c) 2013 Jeff Forcier <jeff@bitprophet.org>
:License: LGPL :License: LGPL
:Homepage: https://github.com/paramiko/paramiko/ :Homepage: https://github.com/paramiko/paramiko/
:API docs: http://docs.paramiko.org :API docs: http://docs.paramiko.org
@ -15,7 +15,7 @@ What
---- ----
"paramiko" is a combination of the esperanto words for "paranoid" and "paramiko" is a combination of the esperanto words for "paranoid" and
"friend". it's a module for python 2.6+ that implements the SSH2 protocol "friend". it's a module for python 2.5+ that implements the SSH2 protocol
for secure (encrypted and authenticated) connections to remote machines. for secure (encrypted and authenticated) connections to remote machines.
unlike SSL (aka TLS), SSH2 protocol does not require hierarchical unlike SSL (aka TLS), SSH2 protocol does not require hierarchical
certificates signed by a powerful central authority. you may know SSH2 as certificates signed by a powerful central authority. you may know SSH2 as
@ -34,10 +34,8 @@ that should have come with this archive.
Requirements Requirements
------------ ------------
- Python 2.6 or better <http://www.python.org/> - this includes Python - python 2.5 or better <http://www.python.org/>
3.2 and higher as well.
- pycrypto 2.1 or better <https://www.dlitz.net/software/pycrypto/> - pycrypto 2.1 or better <https://www.dlitz.net/software/pycrypto/>
- ecdsa 0.9 or better <https://pypi.python.org/pypi/ecdsa>
If you have setuptools, you can build and install paramiko and all its If you have setuptools, you can build and install paramiko and all its
dependencies with this command (as root):: dependencies with this command (as root)::
@ -125,7 +123,10 @@ Use
--- ---
the demo scripts are probably the best example of how to use this package. the demo scripts are probably the best example of how to use this package.
there is also a lot of documentation, generated with Sphinx autodoc, in the doc/ folder. there is also a lot of documentation, generated with epydoc, in the doc/
folder. point your browser there. seriously, do it. mad props to
epydoc, which actually motivated me to write more documentation than i
ever would have before.
there are also unit tests here:: there are also unit tests here::

View File

@ -9,7 +9,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -18,6 +18,7 @@
# along with Paramiko; if not, write to the Free Software Foundation, Inc., # along with Paramiko; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
from __future__ import print_function
import base64 import base64
from binascii import hexlify from binascii import hexlify
@ -28,13 +29,14 @@ import socket
import sys import sys
import time import time
import traceback import traceback
from paramiko.py3compat import input
import paramiko import paramiko
import interactive
try: try:
import interactive input = raw_input
except ImportError: except NameError:
from . import interactive pass
def agent_auth(transport, username): def agent_auth(transport, username):
@ -49,7 +51,7 @@ def agent_auth(transport, username):
return return
for key in agent_keys: for key in agent_keys:
print('Trying ssh-agent key %s' % hexlify(key.get_fingerprint())) print('Trying ssh-agent key %s' % hexlify(key.get_fingerprint()), end=' ')
try: try:
transport.auth_publickey(username, key) transport.auth_publickey(username, key)
print('... success!') print('... success!')
@ -137,9 +139,9 @@ try:
# check server's host key -- this is important. # check server's host key -- this is important.
key = t.get_remote_server_key() key = t.get_remote_server_key()
if hostname not in keys: if not keys.has_key(hostname):
print('*** WARNING: Unknown host key!') print('*** WARNING: Unknown host key!')
elif key.get_name() not in keys[hostname]: elif not keys[hostname].has_key(key.get_name()):
print('*** WARNING: Unknown host key!') print('*** WARNING: Unknown host key!')
elif keys[hostname][key.get_name()] != key: elif keys[hostname][key.get_name()] != key:
print('*** WARNING: Host key has changed!!!') print('*** WARNING: Host key has changed!!!')
@ -165,7 +167,8 @@ try:
chan = t.open_session() chan = t.open_session()
chan.get_pty() chan.get_pty()
chan.invoke_shell() chan.invoke_shell()
print('*** Here we go!\n') print('*** Here we go!')
print()
interactive.interactive_shell(chan) interactive.interactive_shell(chan)
chan.close() chan.close()
t.close() t.close()

View File

@ -9,7 +9,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -18,6 +18,9 @@
# along with Paramiko; if not, write to the Free Software Foundation, Inc., # along with Paramiko; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
from __future__ import print_function, with_statement
import string
import sys import sys
from binascii import hexlify from binascii import hexlify
@ -26,7 +29,6 @@ from optparse import OptionParser
from paramiko import DSSKey from paramiko import DSSKey
from paramiko import RSAKey from paramiko import RSAKey
from paramiko.ssh_exception import SSHException from paramiko.ssh_exception import SSHException
from paramiko.py3compat import u
usage=""" usage="""
%prog [-v] [-b bits] -t type [-N new_passphrase] [-f output_keyfile]""" %prog [-v] [-b bits] -t type [-N new_passphrase] [-f output_keyfile]"""
@ -46,16 +48,16 @@ key_dispatch_table = {
def progress(arg=None): def progress(arg=None):
if not arg: if not arg:
sys.stdout.write('0%\x08\x08\x08 ') print('0%\x08\x08\x08', end=' ')
sys.stdout.flush() sys.stdout.flush()
elif arg[0] == 'p': elif arg[0] == 'p':
sys.stdout.write('25%\x08\x08\x08\x08 ') print('25%\x08\x08\x08\x08', end=' ')
sys.stdout.flush() sys.stdout.flush()
elif arg[0] == 'h': elif arg[0] == 'h':
sys.stdout.write('50%\x08\x08\x08\x08 ') print('50%\x08\x08\x08\x08', end=' ')
sys.stdout.flush() sys.stdout.flush()
elif arg[0] == 'x': elif arg[0] == 'x':
sys.stdout.write('75%\x08\x08\x08\x08 ') print('75%\x08\x08\x08\x08', end=' ')
sys.stdout.flush() sys.stdout.flush()
if __name__ == '__main__': if __name__ == '__main__':
@ -91,8 +93,8 @@ if __name__ == '__main__':
parser.print_help() parser.print_help()
sys.exit(0) sys.exit(0)
for o in list(default_values.keys()): for o in default_values.keys():
globals()[o] = getattr(options, o, default_values[o.lower()]) globals()[o] = getattr(options, o, default_values[string.lower(o)])
if options.newphrase: if options.newphrase:
phrase = getattr(options, 'newphrase') phrase = getattr(options, 'newphrase')
@ -105,7 +107,7 @@ if __name__ == '__main__':
if ktype == 'dsa' and bits > 1024: if ktype == 'dsa' and bits > 1024:
raise SSHException("DSA Keys must be 1024 bits") raise SSHException("DSA Keys must be 1024 bits")
if ktype not in key_dispatch_table: if not key_dispatch_table.has_key(ktype):
raise SSHException("Unknown %s algorithm to generate keys pair" % ktype) raise SSHException("Unknown %s algorithm to generate keys pair" % ktype)
# generating private key # generating private key
@ -122,5 +124,5 @@ if __name__ == '__main__':
if options.verbose: if options.verbose:
print("done.") print("done.")
hash = u(hexlify(pub.get_fingerprint())) hash = hexlify(pub.get_fingerprint())
print("Fingerprint: %d %s %s.pub (%s)" % (bits, ":".join([ hash[i:2+i] for i in range(0, len(hash), 2)]), filename, ktype.upper())) print("Fingerprint: %d %s %s.pub (%s)" % (bits, ":".join([ hash[i:2 + i] for i in range(0, len(hash), 2)]), filename, string.upper(ktype)))

View File

@ -9,7 +9,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -18,6 +18,8 @@
# along with Paramiko; if not, write to the Free Software Foundation, Inc., # along with Paramiko; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
from __future__ import print_function
import base64 import base64
from binascii import hexlify from binascii import hexlify
import os import os
@ -27,7 +29,6 @@ import threading
import traceback import traceback
import paramiko import paramiko
from paramiko.py3compat import b, u, decodebytes
# setup logging # setup logging
@ -36,17 +37,17 @@ paramiko.util.log_to_file('demo_server.log')
host_key = paramiko.RSAKey(filename='test_rsa.key') host_key = paramiko.RSAKey(filename='test_rsa.key')
#host_key = paramiko.DSSKey(filename='test_dss.key') #host_key = paramiko.DSSKey(filename='test_dss.key')
print('Read key: ' + u(hexlify(host_key.get_fingerprint()))) print('Read key: ' + hexlify(host_key.get_fingerprint()))
class Server (paramiko.ServerInterface): class Server (paramiko.ServerInterface):
# 'data' is the output of base64.encodestring(str(key)) # 'data' is the output of base64.encodestring(str(key))
# (using the "user_rsa_key" files) # (using the "user_rsa_key" files)
data = (b'AAAAB3NzaC1yc2EAAAABIwAAAIEAyO4it3fHlmGZWJaGrfeHOVY7RWO3P9M7hp' data = 'AAAAB3NzaC1yc2EAAAABIwAAAIEAyO4it3fHlmGZWJaGrfeHOVY7RWO3P9M7hp' + \
b'fAu7jJ2d7eothvfeuoRFtJwhUmZDluRdFyhFY/hFAh76PJKGAusIqIQKlkJxMC' 'fAu7jJ2d7eothvfeuoRFtJwhUmZDluRdFyhFY/hFAh76PJKGAusIqIQKlkJxMC' + \
b'KDqIexkgHAfID/6mqvmnSJf0b5W8v5h2pI/stOSwTQ+pxVhwJ9ctYDhRSlF0iT' 'KDqIexkgHAfID/6mqvmnSJf0b5W8v5h2pI/stOSwTQ+pxVhwJ9ctYDhRSlF0iT' + \
b'UWT10hcuO4Ks8=') 'UWT10hcuO4Ks8='
good_pub_key = paramiko.RSAKey(data=decodebytes(data)) good_pub_key = paramiko.RSAKey(data=base64.decodestring(data))
def __init__(self): def __init__(self):
self.event = threading.Event() self.event = threading.Event()
@ -62,7 +63,7 @@ class Server (paramiko.ServerInterface):
return paramiko.AUTH_FAILED return paramiko.AUTH_FAILED
def check_auth_publickey(self, username, key): def check_auth_publickey(self, username, key):
print('Auth attempt with key: ' + u(hexlify(key.get_fingerprint()))) print('Auth attempt with key: ' + hexlify(key.get_fingerprint()))
if (username == 'robey') and (key == self.good_pub_key): if (username == 'robey') and (key == self.good_pub_key):
return paramiko.AUTH_SUCCESSFUL return paramiko.AUTH_SUCCESSFUL
return paramiko.AUTH_FAILED return paramiko.AUTH_FAILED
@ -111,7 +112,7 @@ try:
server = Server() server = Server()
try: try:
t.start_server(server=server) t.start_server(server=server)
except paramiko.SSHException: except paramiko.SSHException as x:
print('*** SSH negotiation failed.') print('*** SSH negotiation failed.')
sys.exit(1) sys.exit(1)

View File

@ -9,7 +9,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -20,6 +20,8 @@
# based on code provided by raymond mosteller (thanks!) # based on code provided by raymond mosteller (thanks!)
from __future__ import print_function
import base64 import base64
import getpass import getpass
import os import os
@ -28,9 +30,13 @@ import sys
import traceback import traceback
import paramiko import paramiko
from paramiko.py3compat import input
try:
input = raw_input
except NameError:
pass
# setup logging # setup logging
paramiko.util.log_to_file('demo_sftp.log') paramiko.util.log_to_file('demo_sftp.log')
@ -73,7 +79,7 @@ except IOError:
print('*** Unable to open host keys file') print('*** Unable to open host keys file')
host_keys = {} host_keys = {}
if hostname in host_keys: if host_keys.has_key(hostname):
hostkeytype = host_keys[hostname].keys()[0] hostkeytype = host_keys[hostname].keys()[0]
hostkey = host_keys[hostname][hostkeytype] hostkey = host_keys[hostname][hostkeytype]
print('Using host key of type %s' % hostkeytype) print('Using host key of type %s' % hostkeytype)
@ -87,27 +93,23 @@ try:
# dirlist on remote host # dirlist on remote host
dirlist = sftp.listdir('.') dirlist = sftp.listdir('.')
print("Dirlist: %s" % dirlist) print("Dirlist:", dirlist)
# copy this demo onto the server # copy this demo onto the server
try: try:
sftp.mkdir("demo_sftp_folder") sftp.mkdir("demo_sftp_folder")
except IOError: except IOError:
print('(assuming demo_sftp_folder/ already exists)') print('(assuming demo_sftp_folder/ already exists)')
with sftp.open('demo_sftp_folder/README', 'w') as f: sftp.open('demo_sftp_folder/README', 'w').write('This was created by demo_sftp.py.\n')
f.write('This was created by demo_sftp.py.\n') data = open('demo_sftp.py', 'r').read()
with open('demo_sftp.py', 'r') as f:
data = f.read()
sftp.open('demo_sftp_folder/demo_sftp.py', 'w').write(data) sftp.open('demo_sftp_folder/demo_sftp.py', 'w').write(data)
print('created demo_sftp_folder/ on the server') print('created demo_sftp_folder/ on the server')
# copy the README back here # copy the README back here
with sftp.open('demo_sftp_folder/README', 'r') as f: data = sftp.open('demo_sftp_folder/README', 'r').read()
data = f.read() open('README_demo_sftp', 'w').write(data)
with open('README_demo_sftp', 'w') as f:
f.write(data)
print('copied README back here') print('copied README back here')
# BETTER: use the get() and put() methods # BETTER: use the get() and put() methods
sftp.put('demo_sftp.py', 'demo_sftp_folder/demo_sftp.py') sftp.put('demo_sftp.py', 'demo_sftp_folder/demo_sftp.py')
sftp.get('demo_sftp_folder/README', 'README_demo_sftp') sftp.get('demo_sftp_folder/README', 'README_demo_sftp')

View File

@ -9,7 +9,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -18,6 +18,7 @@
# along with Paramiko; if not, write to the Free Software Foundation, Inc., # along with Paramiko; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
from __future__ import print_function
import base64 import base64
import getpass import getpass
@ -25,14 +26,14 @@ import os
import socket import socket
import sys import sys
import traceback import traceback
from paramiko.py3compat import input
import paramiko import paramiko
try: import interactive
import interactive
except ImportError:
from . import interactive
try:
input = raw_input
except NameError:
pass
# setup logging # setup logging
paramiko.util.log_to_file('demo_simple.log') paramiko.util.log_to_file('demo_simple.log')
@ -67,12 +68,13 @@ password = getpass.getpass('Password for %s@%s: ' % (username, hostname))
try: try:
client = paramiko.SSHClient() client = paramiko.SSHClient()
client.load_system_host_keys() client.load_system_host_keys()
client.set_missing_host_key_policy(paramiko.WarningPolicy()) client.set_missing_host_key_policy(paramiko.WarningPolicy)
print('*** Connecting...') print('*** Connecting...')
client.connect(hostname, port, username, password) client.connect(hostname, port, username, password)
chan = client.invoke_shell() chan = client.invoke_shell()
print(repr(client.get_transport())) print(repr(client.get_transport()))
print('*** Here we go!\n') print('*** Here we go!')
print()
interactive.interactive_shell(chan) interactive.interactive_shell(chan)
chan.close() chan.close()
client.close() client.close()

View File

@ -9,7 +9,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -26,15 +26,13 @@ forwarding (the openssh -L option) from a local port through a tunneled
connection to a destination reachable from the SSH server machine. connection to a destination reachable from the SSH server machine.
""" """
from __future__ import print_function
import getpass import getpass
import os import os
import socket import socket
import select import select
try: import SocketServer
import SocketServer
except ImportError:
import socketserver as SocketServer
import sys import sys
from optparse import OptionParser from optparse import OptionParser
@ -82,11 +80,9 @@ class Handler (SocketServer.BaseRequestHandler):
if len(data) == 0: if len(data) == 0:
break break
self.request.send(data) self.request.send(data)
peername = self.request.getpeername()
chan.close() chan.close()
self.request.close() self.request.close()
verbose('Tunnel closed from %r' % (peername,)) verbose('Tunnel closed from %r' % (self.request.getpeername(),))
def forward_tunnel(local_port, remote_host, remote_port, transport): def forward_tunnel(local_port, remote_host, remote_port, transport):

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -16,10 +16,10 @@
# along with Paramiko; if not, write to the Free Software Foundation, Inc., # along with Paramiko; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
from __future__ import print_function
import socket import socket
import sys import sys
from paramiko.py3compat import u
# windows does not have termios... # windows does not have termios...
try: try:
@ -50,9 +50,9 @@ def posix_shell(chan):
r, w, e = select.select([chan, sys.stdin], [], []) r, w, e = select.select([chan, sys.stdin], [], [])
if chan in r: if chan in r:
try: try:
x = u(chan.recv(1024)) x = chan.recv(1024)
if len(x) == 0: if len(x) == 0:
sys.stdout.write('\r\n*** EOF\r\n') print('\r\n*** EOF\r\n', end=' ')
break break
sys.stdout.write(x) sys.stdout.write(x)
sys.stdout.flush() sys.stdout.flush()

View File

@ -9,7 +9,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -26,6 +26,8 @@ forwarding (the openssh -R option) from a remote port through a tunneled
connection to a destination reachable from the local machine. connection to a destination reachable from the local machine.
""" """
from __future__ import print_function
import getpass import getpass
import os import os
import socket import socket

View File

@ -1,10 +1,2 @@
# Older junk
tox>=1.4,<1.5 tox>=1.4,<1.5
# For newer tasks like building Sphinx docs. epydoc>=3.0,<3.1
# NOTE: Requires Python >=2.6
invoke>=0.7.0
invocations>=0.5.0
sphinx>=1.1.3
alabaster>=0.6.0
releases>=0.5.2
wheel==0.23.0

39
fabfile.py vendored Normal file
View File

@ -0,0 +1,39 @@
from fabric.api import task, sudo, env, local, hosts
from fabric.contrib.project import rsync_project
from fabric.contrib.console import confirm
@task
@hosts("paramiko.org")
def upload_docs():
target = "/var/www/paramiko.org"
staging = "/tmp/paramiko_docs"
sudo("mkdir -p %s" % staging)
sudo("chown -R %s %s" % (env.user, staging))
sudo("rm -rf %s/*" % target)
rsync_project(local_dir='docs/', remote_dir=staging, delete=True)
sudo("cp -R %s/* %s/" % (staging, target))
@task
def build_docs():
local("epydoc --no-private -o docs/ paramiko")
@task
def clean():
local("rm -rf build dist docs")
local("rm -f MANIFEST *.log demos/*.log")
local("rm -f paramiko/*.pyc")
local("rm -f test.log")
local("rm -rf paramiko.egg-info")
@task
def test():
local("python ./test.py")
@task
def release():
confirm("Only hit Enter if you remembered to update the version!")
confirm("Also, did you remember to tag your release?")
build_docs()
local("python setup.py sdist register upload")
upload_docs()

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -16,15 +16,49 @@
# along with Paramiko; if not, write to the Free Software Foundation, Inc., # along with Paramiko; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
"""
I{Paramiko} (a combination of the esperanto words for "paranoid" and "friend")
is a module for python 2.5 or greater that implements the SSH2 protocol for
secure (encrypted and authenticated) connections to remote machines. Unlike
SSL (aka TLS), the SSH2 protocol does not require hierarchical certificates
signed by a powerful central authority. You may know SSH2 as the protocol that
replaced C{telnet} and C{rsh} for secure access to remote shells, but the
protocol also includes the ability to open arbitrary channels to remote
services across an encrypted tunnel. (This is how C{sftp} works, for example.)
The high-level client API starts with creation of an L{SSHClient} object.
For more direct control, pass a socket (or socket-like object) to a
L{Transport}, and use L{start_server <Transport.start_server>} or
L{start_client <Transport.start_client>} to negoatite
with the remote host as either a server or client. As a client, you are
responsible for authenticating using a password or private key, and checking
the server's host key. I{(Key signature and verification is done by paramiko,
but you will need to provide private keys and check that the content of a
public key matches what you expected to see.)} As a server, you are
responsible for deciding which users, passwords, and keys to allow, and what
kind of channels to allow.
Once you have finished, either side may request flow-controlled L{Channel}s to
the other side, which are python objects that act like sockets, but send and
receive data over the encrypted session.
Paramiko is written entirely in python (no C or platform-dependent code) and is
released under the GNU Lesser General Public License (LGPL).
Website: U{https://github.com/paramiko/paramiko/}
Mailing list: U{paramiko@librelist.com<mailto:paramiko@librelist.com>}
"""
from __future__ import absolute_import
import sys import sys
if sys.version_info < (2, 6): if sys.version_info < (2, 5):
raise RuntimeError('You need Python 2.6+ for this module.') raise RuntimeError('You need python 2.5+ for this module.')
__author__ = "Jeff Forcier <jeff@bitprophet.org>" __author__ = "Jeff Forcier <jeff@bitprophet.org>"
__version__ = "1.14.0" __version__ = "1.11.0"
__version_info__ = tuple([ int(d) for d in __version__.split(".") ])
__license__ = "GNU Lesser General Public License (LGPL)" __license__ = "GNU Lesser General Public License (LGPL)"
@ -38,7 +72,6 @@ from paramiko.ssh_exception import SSHException, PasswordRequiredException, \
from paramiko.server import ServerInterface, SubsystemHandler, InteractiveQuery from paramiko.server import ServerInterface, SubsystemHandler, InteractiveQuery
from paramiko.rsakey import RSAKey from paramiko.rsakey import RSAKey
from paramiko.dsskey import DSSKey from paramiko.dsskey import DSSKey
from paramiko.ecdsakey import ECDSAKey
from paramiko.sftp import SFTPError, BaseSFTP from paramiko.sftp import SFTPError, BaseSFTP
from paramiko.sftp_client import SFTP, SFTPClient from paramiko.sftp_client import SFTP, SFTPClient
from paramiko.sftp_server import SFTPServer from paramiko.sftp_server import SFTPServer
@ -55,12 +88,23 @@ from paramiko.hostkeys import HostKeys
from paramiko.config import SSHConfig from paramiko.config import SSHConfig
from paramiko.proxy import ProxyCommand from paramiko.proxy import ProxyCommand
import six
if not six.PY3:
# Skipping port to Python 3 since, Epydoc has not been ported to Python 3.
# fix module names for epydoc
for c in locals().values():
if issubclass(type(c), type) or type(c).__name__ == 'classobj':
# classobj for exceptions :/
c.__module__ = __name__
del c
from paramiko.common import AUTH_SUCCESSFUL, AUTH_PARTIALLY_SUCCESSFUL, AUTH_FAILED, \ from paramiko.common import AUTH_SUCCESSFUL, AUTH_PARTIALLY_SUCCESSFUL, AUTH_FAILED, \
OPEN_SUCCEEDED, OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED, OPEN_FAILED_CONNECT_FAILED, \ OPEN_SUCCEEDED, OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED, OPEN_FAILED_CONNECT_FAILED, \
OPEN_FAILED_UNKNOWN_CHANNEL_TYPE, OPEN_FAILED_RESOURCE_SHORTAGE OPEN_FAILED_UNKNOWN_CHANNEL_TYPE, OPEN_FAILED_RESOURCE_SHORTAGE
from paramiko.sftp import SFTP_OK, SFTP_EOF, SFTP_NO_SUCH_FILE, SFTP_PERMISSION_DENIED, SFTP_FAILURE, \ from paramiko.sftp import SFTP_OK, SFTP_EOF, SFTP_NO_SUCH_FILE, SFTP_PERMISSION_DENIED, SFTP_FAILURE, \
SFTP_BAD_MESSAGE, SFTP_NO_CONNECTION, SFTP_CONNECTION_LOST, SFTP_OP_UNSUPPORTED SFTP_BAD_MESSAGE, SFTP_NO_CONNECTION, SFTP_CONNECTION_LOST, SFTP_OP_UNSUPPORTED
from paramiko.common import io_sleep from paramiko.common import io_sleep

View File

@ -8,96 +8,90 @@ in jaraco.windows and asking the author to port the fixes back here.
import ctypes import ctypes
import ctypes.wintypes import ctypes.wintypes
from paramiko.py3compat import u from six.moves import builtins
try:
import builtins
except ImportError:
import __builtin__ as builtins
try:
USHORT = ctypes.wintypes.USHORT
except AttributeError:
USHORT = ctypes.c_ushort
###################### ######################
# jaraco.windows.error # jaraco.windows.error
def format_system_message(errno): def format_system_message(errno):
""" """
Call FormatMessage with a system error number to retrieve Call FormatMessage with a system error number to retrieve
the descriptive error message. the descriptive error message.
""" """
# first some flags used by FormatMessageW # first some flags used by FormatMessageW
ALLOCATE_BUFFER = 0x100 ALLOCATE_BUFFER = 0x100
ARGUMENT_ARRAY = 0x2000 ARGUMENT_ARRAY = 0x2000
FROM_HMODULE = 0x800 FROM_HMODULE = 0x800
FROM_STRING = 0x400 FROM_STRING = 0x400
FROM_SYSTEM = 0x1000 FROM_SYSTEM = 0x1000
IGNORE_INSERTS = 0x200 IGNORE_INSERTS = 0x200
# Let FormatMessageW allocate the buffer (we'll free it below) # Let FormatMessageW allocate the buffer (we'll free it below)
# Also, let it know we want a system error message. # Also, let it know we want a system error message.
flags = ALLOCATE_BUFFER | FROM_SYSTEM flags = ALLOCATE_BUFFER | FROM_SYSTEM
source = None source = None
message_id = errno message_id = errno
language_id = 0 language_id = 0
result_buffer = ctypes.wintypes.LPWSTR() result_buffer = ctypes.wintypes.LPWSTR()
buffer_size = 0 buffer_size = 0
arguments = None arguments = None
format_bytes = ctypes.windll.kernel32.FormatMessageW( bytes = ctypes.windll.kernel32.FormatMessageW(
flags, flags,
source, source,
message_id, message_id,
language_id, language_id,
ctypes.byref(result_buffer), ctypes.byref(result_buffer),
buffer_size, buffer_size,
arguments, arguments,
) )
# note the following will cause an infinite loop if GetLastError # note the following will cause an infinite loop if GetLastError
# repeatedly returns an error that cannot be formatted, although # repeatedly returns an error that cannot be formatted, although
# this should not happen. # this should not happen.
handle_nonzero_success(format_bytes) handle_nonzero_success(bytes)
message = result_buffer.value message = result_buffer.value
ctypes.windll.kernel32.LocalFree(result_buffer) ctypes.windll.kernel32.LocalFree(result_buffer)
return message return message
class WindowsError(builtins.WindowsError): class WindowsError(builtins.WindowsError):
"more info about errors at http://msdn.microsoft.com/en-us/library/ms681381(VS.85).aspx" "more info about errors at http://msdn.microsoft.com/en-us/library/ms681381(VS.85).aspx"
def __init__(self, value=None): def __init__(self, value=None):
if value is None: if value is None:
value = ctypes.windll.kernel32.GetLastError() value = ctypes.windll.kernel32.GetLastError()
strerror = format_system_message(value) strerror = format_system_message(value)
super(WindowsError, self).__init__(value, strerror) super(WindowsError, self).__init__(value, strerror)
@property @property
def message(self): def message(self):
return self.strerror return self.strerror
@property @property
def code(self): def code(self):
return self.winerror return self.winerror
def __str__(self): def __str__(self):
return self.message return self.message
def __repr__(self): def __repr__(self):
return '{self.__class__.__name__}({self.winerror})'.format(**vars()) return '{self.__class__.__name__}({self.winerror})'.format(**vars())
def handle_nonzero_success(result): def handle_nonzero_success(result):
if result == 0: if result == 0:
raise WindowsError() raise WindowsError()
#####################
# jaraco.windows.mmap
CreateFileMapping = ctypes.windll.kernel32.CreateFileMappingW CreateFileMapping = ctypes.windll.kernel32.CreateFileMappingW
CreateFileMapping.argtypes = [ CreateFileMapping.argtypes = [
ctypes.wintypes.HANDLE, ctypes.wintypes.HANDLE,
ctypes.c_void_p, ctypes.c_void_p,
ctypes.wintypes.DWORD, ctypes.wintypes.DWORD,
ctypes.wintypes.DWORD, ctypes.wintypes.DWORD,
ctypes.wintypes.DWORD, ctypes.wintypes.DWORD,
ctypes.wintypes.LPWSTR, ctypes.wintypes.LPWSTR,
] ]
CreateFileMapping.restype = ctypes.wintypes.HANDLE CreateFileMapping.restype = ctypes.wintypes.HANDLE
@ -105,174 +99,171 @@ MapViewOfFile = ctypes.windll.kernel32.MapViewOfFile
MapViewOfFile.restype = ctypes.wintypes.HANDLE MapViewOfFile.restype = ctypes.wintypes.HANDLE
class MemoryMap(object): class MemoryMap(object):
""" """
A memory map object which can have security attributes overrideden. A memory map object which can have security attributes overrideden.
""" """
def __init__(self, name, length, security_attributes=None): def __init__(self, name, length, security_attributes=None):
self.name = name self.name = name
self.length = length self.length = length
self.security_attributes = security_attributes self.security_attributes = security_attributes
self.pos = 0 self.pos = 0
def __enter__(self): def __enter__(self):
p_SA = ( p_SA = (
ctypes.byref(self.security_attributes) ctypes.byref(self.security_attributes)
if self.security_attributes else None if self.security_attributes else None
) )
INVALID_HANDLE_VALUE = -1 INVALID_HANDLE_VALUE = -1
PAGE_READWRITE = 0x4 PAGE_READWRITE = 0x4
FILE_MAP_WRITE = 0x2 FILE_MAP_WRITE = 0x2
filemap = ctypes.windll.kernel32.CreateFileMappingW( filemap = ctypes.windll.kernel32.CreateFileMappingW(
INVALID_HANDLE_VALUE, p_SA, PAGE_READWRITE, 0, self.length, INVALID_HANDLE_VALUE, p_SA, PAGE_READWRITE, 0, self.length,
u(self.name)) builtins.unicode(self.name))
handle_nonzero_success(filemap) handle_nonzero_success(filemap)
if filemap == INVALID_HANDLE_VALUE: if filemap == INVALID_HANDLE_VALUE:
raise Exception("Failed to create file mapping") raise Exception("Failed to create file mapping")
self.filemap = filemap self.filemap = filemap
self.view = MapViewOfFile(filemap, FILE_MAP_WRITE, 0, 0, 0) self.view = MapViewOfFile(filemap, FILE_MAP_WRITE, 0, 0, 0)
return self return self
def seek(self, pos): def seek(self, pos):
self.pos = pos self.pos = pos
def write(self, msg): def write(self, msg):
n = len(msg) ctypes.windll.msvcrt.memcpy(self.view + self.pos, msg, len(msg))
if self.pos + n >= self.length: # A little safety. self.pos += len(msg)
raise ValueError("Refusing to write %d bytes" % n)
ctypes.windll.kernel32.RtlMoveMemory(self.view + self.pos, msg, n)
self.pos += n
def read(self, n): def read(self, n):
""" """
Read n bytes from mapped view. Read n bytes from mapped view.
""" """
out = ctypes.create_string_buffer(n) out = ctypes.create_string_buffer(n)
ctypes.windll.kernel32.RtlMoveMemory(out, self.view + self.pos, n) ctypes.windll.msvcrt.memcpy(out, self.view + self.pos, n)
self.pos += n self.pos += n
return out.raw return out.raw
def __exit__(self, exc_type, exc_val, tb): def __exit__(self, exc_type, exc_val, tb):
ctypes.windll.kernel32.UnmapViewOfFile(self.view) ctypes.windll.kernel32.UnmapViewOfFile(self.view)
ctypes.windll.kernel32.CloseHandle(self.filemap) ctypes.windll.kernel32.CloseHandle(self.filemap)
######################### #########################
# jaraco.windows.security # jaraco.windows.security
class TokenInformationClass: class TokenInformationClass:
TokenUser = 1 TokenUser = 1
class TOKEN_USER(ctypes.Structure): class TOKEN_USER(ctypes.Structure):
num = 1 num = 1
_fields_ = [ _fields_ = [
('SID', ctypes.c_void_p), ('SID', ctypes.c_void_p),
('ATTRIBUTES', ctypes.wintypes.DWORD), ('ATTRIBUTES', ctypes.wintypes.DWORD),
] ]
class SECURITY_DESCRIPTOR(ctypes.Structure): class SECURITY_DESCRIPTOR(ctypes.Structure):
""" """
typedef struct _SECURITY_DESCRIPTOR typedef struct _SECURITY_DESCRIPTOR
{ {
UCHAR Revision; UCHAR Revision;
UCHAR Sbz1; UCHAR Sbz1;
SECURITY_DESCRIPTOR_CONTROL Control; SECURITY_DESCRIPTOR_CONTROL Control;
PSID Owner; PSID Owner;
PSID Group; PSID Group;
PACL Sacl; PACL Sacl;
PACL Dacl; PACL Dacl;
} SECURITY_DESCRIPTOR; } SECURITY_DESCRIPTOR;
""" """
SECURITY_DESCRIPTOR_CONTROL = USHORT SECURITY_DESCRIPTOR_CONTROL = ctypes.wintypes.USHORT
REVISION = 1 REVISION = 1
_fields_ = [ _fields_ = [
('Revision', ctypes.c_ubyte), ('Revision', ctypes.c_ubyte),
('Sbz1', ctypes.c_ubyte), ('Sbz1', ctypes.c_ubyte),
('Control', SECURITY_DESCRIPTOR_CONTROL), ('Control', SECURITY_DESCRIPTOR_CONTROL),
('Owner', ctypes.c_void_p), ('Owner', ctypes.c_void_p),
('Group', ctypes.c_void_p), ('Group', ctypes.c_void_p),
('Sacl', ctypes.c_void_p), ('Sacl', ctypes.c_void_p),
('Dacl', ctypes.c_void_p), ('Dacl', ctypes.c_void_p),
] ]
class SECURITY_ATTRIBUTES(ctypes.Structure): class SECURITY_ATTRIBUTES(ctypes.Structure):
""" """
typedef struct _SECURITY_ATTRIBUTES { typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength; DWORD nLength;
LPVOID lpSecurityDescriptor; LPVOID lpSecurityDescriptor;
BOOL bInheritHandle; BOOL bInheritHandle;
} SECURITY_ATTRIBUTES; } SECURITY_ATTRIBUTES;
""" """
_fields_ = [ _fields_ = [
('nLength', ctypes.wintypes.DWORD), ('nLength', ctypes.wintypes.DWORD),
('lpSecurityDescriptor', ctypes.c_void_p), ('lpSecurityDescriptor', ctypes.c_void_p),
('bInheritHandle', ctypes.wintypes.BOOL), ('bInheritHandle', ctypes.wintypes.BOOL),
] ]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(SECURITY_ATTRIBUTES, self).__init__(*args, **kwargs) super(SECURITY_ATTRIBUTES, self).__init__(*args, **kwargs)
self.nLength = ctypes.sizeof(SECURITY_ATTRIBUTES) self.nLength = ctypes.sizeof(SECURITY_ATTRIBUTES)
def _get_descriptor(self): def _get_descriptor(self):
return self._descriptor return self._descriptor
def _set_descriptor(self, descriptor): def _set_descriptor(self, descriptor):
self._descriptor = descriptor self._descriptor = descriptor
self.lpSecurityDescriptor = ctypes.addressof(descriptor) self.lpSecurityDescriptor = ctypes.addressof(descriptor)
descriptor = property(_get_descriptor, _set_descriptor) descriptor = property(_get_descriptor, _set_descriptor)
def GetTokenInformation(token, information_class): def GetTokenInformation(token, information_class):
""" """
Given a token, get the token information for it. Given a token, get the token information for it.
""" """
data_size = ctypes.wintypes.DWORD() data_size = ctypes.wintypes.DWORD()
ctypes.windll.advapi32.GetTokenInformation(token, information_class.num, ctypes.windll.advapi32.GetTokenInformation(token, information_class.num,
0, 0, ctypes.byref(data_size)) 0, 0, ctypes.byref(data_size))
data = ctypes.create_string_buffer(data_size.value) data = ctypes.create_string_buffer(data_size.value)
handle_nonzero_success(ctypes.windll.advapi32.GetTokenInformation(token, handle_nonzero_success(ctypes.windll.advapi32.GetTokenInformation(token,
information_class.num, information_class.num,
ctypes.byref(data), ctypes.sizeof(data), ctypes.byref(data), ctypes.sizeof(data),
ctypes.byref(data_size))) ctypes.byref(data_size)))
return ctypes.cast(data, ctypes.POINTER(TOKEN_USER)).contents return ctypes.cast(data, ctypes.POINTER(TOKEN_USER)).contents
class TokenAccess: class TokenAccess:
TOKEN_QUERY = 0x8 TOKEN_QUERY = 0x8
def OpenProcessToken(proc_handle, access): def OpenProcessToken(proc_handle, access):
result = ctypes.wintypes.HANDLE() result = ctypes.wintypes.HANDLE()
proc_handle = ctypes.wintypes.HANDLE(proc_handle) proc_handle = ctypes.wintypes.HANDLE(proc_handle)
handle_nonzero_success(ctypes.windll.advapi32.OpenProcessToken( handle_nonzero_success(ctypes.windll.advapi32.OpenProcessToken(
proc_handle, access, ctypes.byref(result))) proc_handle, access, ctypes.byref(result)))
return result return result
def get_current_user(): def get_current_user():
""" """
Return a TOKEN_USER for the owner of this process. Return a TOKEN_USER for the owner of this process.
""" """
process = OpenProcessToken( process = OpenProcessToken(
ctypes.windll.kernel32.GetCurrentProcess(), ctypes.windll.kernel32.GetCurrentProcess(),
TokenAccess.TOKEN_QUERY, TokenAccess.TOKEN_QUERY,
) )
return GetTokenInformation(process, TOKEN_USER) return GetTokenInformation(process, TOKEN_USER)
def get_security_attributes_for_user(user=None): def get_security_attributes_for_user(user=None):
""" """
Return a SECURITY_ATTRIBUTES structure with the SID set to the Return a SECURITY_ATTRIBUTES structure with the SID set to the
specified user (uses current user if none is specified). specified user (uses current user if none is specified).
""" """
if user is None: if user is None:
user = get_current_user() user = get_current_user()
assert isinstance(user, TOKEN_USER), "user must be TOKEN_USER instance" assert isinstance(user, TOKEN_USER), "user must be TOKEN_USER instance"
SD = SECURITY_DESCRIPTOR() SD = SECURITY_DESCRIPTOR()
SA = SECURITY_ATTRIBUTES() SA = SECURITY_ATTRIBUTES()
# by attaching the actual security descriptor, it will be garbage- # by attaching the actual security descriptor, it will be garbage-
# collected with the security attributes # collected with the security attributes
SA.descriptor = SD SA.descriptor = SD
SA.bInheritHandle = 1 SA.bInheritHandle = 1
ctypes.windll.advapi32.InitializeSecurityDescriptor(ctypes.byref(SD), ctypes.windll.advapi32.InitializeSecurityDescriptor(ctypes.byref(SD),
SECURITY_DESCRIPTOR.REVISION) SECURITY_DESCRIPTOR.REVISION)
ctypes.windll.advapi32.SetSecurityDescriptorOwner(ctypes.byref(SD), ctypes.windll.advapi32.SetSecurityDescriptorOwner(ctypes.byref(SD),
user.SID, 0) user.SID, 0)
return SA return SA

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -17,7 +17,7 @@
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
""" """
SSH Agent interface SSH Agent interface for Unix clients.
""" """
import os import os
@ -29,21 +29,28 @@ import time
import tempfile import tempfile
import stat import stat
from select import select from select import select
from paramiko.common import asbytes, io_sleep
from paramiko.py3compat import byte_chr
from paramiko.ssh_exception import SSHException from paramiko.ssh_exception import SSHException
from paramiko.message import Message from paramiko.message import Message
from paramiko.pkey import PKey from paramiko.pkey import PKey
from paramiko.channel import Channel
from paramiko.common import io_sleep
from paramiko.util import retry_on_signal from paramiko.util import retry_on_signal
cSSH2_AGENTC_REQUEST_IDENTITIES = byte_chr(11) SSH2_AGENTC_REQUEST_IDENTITIES, SSH2_AGENT_IDENTITIES_ANSWER, \
SSH2_AGENT_IDENTITIES_ANSWER = 12 SSH2_AGENTC_SIGN_REQUEST, SSH2_AGENT_SIGN_RESPONSE = range(11, 15)
cSSH2_AGENTC_SIGN_REQUEST = byte_chr(13)
SSH2_AGENT_SIGN_RESPONSE = 14
class AgentSSH(object): class AgentSSH(object):
"""
Client interface for using private keys from an SSH agent running on the
local machine. If an SSH agent is running, this class can be used to
connect to it and retreive L{PKey} objects which can be used when
attempting to authenticate to remote SSH servers.
Because the SSH agent protocol uses environment variables and unix-domain
sockets, this probably doesn't work on Windows. It does work on most
posix platforms though (Linux and MacOS X, for example).
"""
def __init__(self): def __init__(self):
self._conn = None self._conn = None
self._keys = () self._keys = ()
@ -54,20 +61,19 @@ class AgentSSH(object):
no SSH agent was running (or it couldn't be contacted), an empty list no SSH agent was running (or it couldn't be contacted), an empty list
will be returned. will be returned.
:return: @return: a list of keys available on the SSH agent
a tuple of `.AgentKey` objects representing keys available on the @rtype: tuple of L{AgentKey}
SSH agent
""" """
return self._keys return self._keys
def _connect(self, conn): def _connect(self, conn):
self._conn = conn self._conn = conn
ptype, result = self._send_message(cSSH2_AGENTC_REQUEST_IDENTITIES) ptype, result = self._send_message(chr(SSH2_AGENTC_REQUEST_IDENTITIES))
if ptype != SSH2_AGENT_IDENTITIES_ANSWER: if ptype != SSH2_AGENT_IDENTITIES_ANSWER:
raise SSHException('could not get keys from ssh-agent') raise SSHException('could not get keys from ssh-agent')
keys = [] keys = []
for i in range(result.get_int()): for i in range(result.get_int()):
keys.append(AgentKey(self, result.get_binary())) keys.append(AgentKey(self, result.get_string()))
result.get_string() result.get_string()
self._keys = tuple(keys) self._keys = tuple(keys)
@ -77,7 +83,7 @@ class AgentSSH(object):
self._keys = () self._keys = ()
def _send_message(self, msg): def _send_message(self, msg):
msg = asbytes(msg) msg = str(msg)
self._conn.send(struct.pack('>I', len(msg)) + msg) self._conn.send(struct.pack('>I', len(msg)) + msg)
l = self._read_all(4) l = self._read_all(4)
msg = Message(self._read_all(struct.unpack('>I', l)[0])) msg = Message(self._read_all(struct.unpack('>I', l)[0]))
@ -94,11 +100,8 @@ class AgentSSH(object):
result += extra result += extra
return result return result
class AgentProxyThread(threading.Thread): class AgentProxyThread(threading.Thread):
""" """ Class in charge of communication between two chan """
Class in charge of communication between two channels.
"""
def __init__(self, agent): def __init__(self, agent):
threading.Thread.__init__(self, target=self.run) threading.Thread.__init__(self, target=self.run)
self._agent = agent self._agent = agent
@ -106,7 +109,7 @@ class AgentProxyThread(threading.Thread):
def run(self): def run(self):
try: try:
(r, addr) = self.get_connection() (r,addr) = self.get_connection()
self.__inr = r self.__inr = r
self.__addr = addr self.__addr = addr
self._agent.connect() self._agent.connect()
@ -143,7 +146,6 @@ class AgentProxyThread(threading.Thread):
self.__inr.close() self.__inr.close()
self._agent._conn.close() self._agent._conn.close()
class AgentLocalProxy(AgentProxyThread): class AgentLocalProxy(AgentProxyThread):
""" """
Class to be used when wanting to ask a local SSH Agent being Class to be used when wanting to ask a local SSH Agent being
@ -153,20 +155,18 @@ class AgentLocalProxy(AgentProxyThread):
AgentProxyThread.__init__(self, agent) AgentProxyThread.__init__(self, agent)
def get_connection(self): def get_connection(self):
""" """ Return a pair of socket object and string address
Return a pair of socket object and string address. May Block !
May block!
""" """
conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try: try:
conn.bind(self._agent._get_filename()) conn.bind(self._agent._get_filename())
conn.listen(1) conn.listen(1)
(r, addr) = conn.accept() (r,addr) = conn.accept()
return r, addr return (r, addr)
except: except:
raise raise
return None
class AgentRemoteProxy(AgentProxyThread): class AgentRemoteProxy(AgentProxyThread):
""" """
@ -177,20 +177,22 @@ class AgentRemoteProxy(AgentProxyThread):
self.__chan = chan self.__chan = chan
def get_connection(self): def get_connection(self):
return self.__chan, None """
Class to be used when wanting to ask a local SSH Agent being
asked from a remote fake agent (so use a unix socket for ex.)
"""
return (self.__chan, None)
class AgentClientProxy(object): class AgentClientProxy(object):
""" """
Class proxying request as a client: Class proxying request as a client:
-> client ask for a request_forward_agent()
#. client ask for a request_forward_agent() -> server creates a proxy and a fake SSH Agent
#. server creates a proxy and a fake SSH Agent -> server ask for establishing a connection when needed,
#. server ask for establishing a connection when needed,
calling the forward_agent_handler at client side. calling the forward_agent_handler at client side.
#. the forward_agent_handler launch a thread for connecting -> the forward_agent_handler launch a thread for connecting
the remote fake agent and the local agent the remote fake agent and the local agent
#. Communication occurs ... -> Communication occurs ...
""" """
def __init__(self, chanRemote): def __init__(self, chanRemote):
self._conn = None self._conn = None
@ -203,7 +205,7 @@ class AgentClientProxy(object):
def connect(self): def connect(self):
""" """
Method automatically called by ``AgentProxyThread.run``. Method automatically called by the run() method of the AgentProxyThread
""" """
if ('SSH_AUTH_SOCK' in os.environ) and (sys.platform != 'win32'): if ('SSH_AUTH_SOCK' in os.environ) and (sys.platform != 'win32'):
conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
@ -213,7 +215,7 @@ class AgentClientProxy(object):
# probably a dangling env var: the ssh agent is gone # probably a dangling env var: the ssh agent is gone
return return
elif sys.platform == 'win32': elif sys.platform == 'win32':
import paramiko.win_pageant as win_pageant from paramiko import win_pageant
if win_pageant.can_talk_to_agent(): if win_pageant.can_talk_to_agent():
conn = win_pageant.PageantConnection() conn = win_pageant.PageantConnection()
else: else:
@ -234,12 +236,11 @@ class AgentClientProxy(object):
if self._conn is not None: if self._conn is not None:
self._conn.close() self._conn.close()
class AgentServerProxy(AgentSSH): class AgentServerProxy(AgentSSH):
""" """
:param .Transport t: Transport used for SSH Agent communication forwarding @param t : transport used for the Forward for SSH Agent communication
:raises SSHException: mostly if we lost the agent @raise SSHException: mostly if we lost the agent
""" """
def __init__(self, t): def __init__(self, t):
AgentSSH.__init__(self) AgentSSH.__init__(self)
@ -275,15 +276,16 @@ class AgentServerProxy(AgentSSH):
""" """
Helper for the environnement under unix Helper for the environnement under unix
:return: @return: the SSH_AUTH_SOCK Environnement variables
a dict containing the ``SSH_AUTH_SOCK`` environnement variables @rtype: dict
""" """
return {'SSH_AUTH_SOCK': self._get_filename()} env = {}
env['SSH_AUTH_SOCK'] = self._get_filename()
return env
def _get_filename(self): def _get_filename(self):
return self._file return self._file
class AgentRequestHandler(object): class AgentRequestHandler(object):
def __init__(self, chanClient): def __init__(self, chanClient):
self._conn = None self._conn = None
@ -301,22 +303,27 @@ class AgentRequestHandler(object):
for p in self.__clientProxys: for p in self.__clientProxys:
p.close() p.close()
class Agent(AgentSSH): class Agent(AgentSSH):
""" """
Client interface for using private keys from an SSH agent running on the Client interface for using private keys from an SSH agent running on the
local machine. If an SSH agent is running, this class can be used to local machine. If an SSH agent is running, this class can be used to
connect to it and retreive `.PKey` objects which can be used when connect to it and retreive L{PKey} objects which can be used when
attempting to authenticate to remote SSH servers. attempting to authenticate to remote SSH servers.
Upon initialization, a session with the local machine's SSH agent is Because the SSH agent protocol uses environment variables and unix-domain
opened, if one is running. If no agent is running, initialization will sockets, this probably doesn't work on Windows. It does work on most
succeed, but `get_keys` will return an empty tuple. posix platforms though (Linux and MacOS X, for example).
:raises SSHException:
if an SSH agent is found, but speaks an incompatible protocol
""" """
def __init__(self): def __init__(self):
"""
Open a session with the local machine's SSH agent, if one is running.
If no agent is running, initialization will succeed, but L{get_keys}
will return an empty tuple.
@raise SSHException: if an SSH agent is found, but speaks an
incompatible protocol
"""
AgentSSH.__init__(self) AgentSSH.__init__(self)
if ('SSH_AUTH_SOCK' in os.environ) and (sys.platform != 'win32'): if ('SSH_AUTH_SOCK' in os.environ) and (sys.platform != 'win32'):
@ -327,7 +334,7 @@ class Agent(AgentSSH):
# probably a dangling env var: the ssh agent is gone # probably a dangling env var: the ssh agent is gone
return return
elif sys.platform == 'win32': elif sys.platform == 'win32':
from . import win_pageant from paramiko import win_pageant
if win_pageant.can_talk_to_agent(): if win_pageant.can_talk_to_agent():
conn = win_pageant.PageantConnection() conn = win_pageant.PageantConnection()
else: else:
@ -343,34 +350,31 @@ class Agent(AgentSSH):
""" """
self._close() self._close()
class AgentKey(PKey): class AgentKey(PKey):
""" """
Private key held in a local SSH agent. This type of key can be used for Private key held in a local SSH agent. This type of key can be used for
authenticating to a remote server (signing). Most other key operations authenticating to a remote server (signing). Most other key operations
work as expected. work as expected.
""" """
def __init__(self, agent, blob): def __init__(self, agent, blob):
self.agent = agent self.agent = agent
self.blob = blob self.blob = blob
self.name = Message(blob).get_text() self.name = Message(blob).get_string()
def asbytes(self):
return self.blob
def __str__(self): def __str__(self):
return self.asbytes() return self.blob
def get_name(self): def get_name(self):
return self.name return self.name
def sign_ssh_data(self, data): def sign_ssh_data(self, rng, data):
msg = Message() msg = Message()
msg.add_byte(cSSH2_AGENTC_SIGN_REQUEST) msg.add_byte(chr(SSH2_AGENTC_SIGN_REQUEST))
msg.add_string(self.blob) msg.add_string(self.blob)
msg.add_string(data) msg.add_string(data)
msg.add_int(0) msg.add_int(0)
ptype, result = self.agent._send_message(msg) ptype, result = self.agent._send_message(msg)
if ptype != SSH2_AGENT_SIGN_RESPONSE: if ptype != SSH2_AGENT_SIGN_RESPONSE:
raise SSHException('key cannot be used for signing') raise SSHException('key cannot be used for signing')
return result.get_binary() return result.get_string()

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -17,21 +17,19 @@
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
""" """
`.AuthHandler` L{AuthHandler}
""" """
import threading
import weakref import weakref
from paramiko.common import cMSG_SERVICE_REQUEST, cMSG_DISCONNECT, \
DISCONNECT_SERVICE_NOT_AVAILABLE, DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE, \
cMSG_USERAUTH_REQUEST, cMSG_SERVICE_ACCEPT, DEBUG, AUTH_SUCCESSFUL, INFO, \
cMSG_USERAUTH_SUCCESS, cMSG_USERAUTH_FAILURE, AUTH_PARTIALLY_SUCCESSFUL, \
cMSG_USERAUTH_INFO_REQUEST, WARNING, AUTH_FAILED, cMSG_USERAUTH_PK_OK, \
cMSG_USERAUTH_INFO_RESPONSE, MSG_SERVICE_REQUEST, MSG_SERVICE_ACCEPT, \
MSG_USERAUTH_REQUEST, MSG_USERAUTH_SUCCESS, MSG_USERAUTH_FAILURE, \
MSG_USERAUTH_BANNER, MSG_USERAUTH_INFO_REQUEST, MSG_USERAUTH_INFO_RESPONSE
# this helps freezing utils
import encodings.utf_8
import six
from paramiko.common import *
from paramiko import util
from paramiko.message import Message from paramiko.message import Message
from paramiko.py3compat import bytestring
from paramiko.ssh_exception import SSHException, AuthenticationException, \ from paramiko.ssh_exception import SSHException, AuthenticationException, \
BadAuthenticationType, PartialAuthentication BadAuthenticationType, PartialAuthentication
from paramiko.server import InteractiveQuery from paramiko.server import InteractiveQuery
@ -48,7 +46,6 @@ class AuthHandler (object):
self.authenticated = False self.authenticated = False
self.auth_event = None self.auth_event = None
self.auth_method = '' self.auth_method = ''
self.banner = None
self.password = None self.password = None
self.private_key = None self.private_key = None
self.interactive_handler = None self.interactive_handler = None
@ -117,17 +114,19 @@ class AuthHandler (object):
if self.auth_event is not None: if self.auth_event is not None:
self.auth_event.set() self.auth_event.set()
### internals... ### internals...
def _request_auth(self): def _request_auth(self):
m = Message() m = Message()
m.add_byte(cMSG_SERVICE_REQUEST) m.add_byte(chr(MSG_SERVICE_REQUEST))
m.add_string('ssh-userauth') m.add_string('ssh-userauth')
self.transport._send_message(m) self.transport._send_message(m)
def _disconnect_service_not_available(self): def _disconnect_service_not_available(self):
m = Message() m = Message()
m.add_byte(cMSG_DISCONNECT) m.add_byte(chr(MSG_DISCONNECT))
m.add_int(DISCONNECT_SERVICE_NOT_AVAILABLE) m.add_int(DISCONNECT_SERVICE_NOT_AVAILABLE)
m.add_string('Service not available') m.add_string('Service not available')
m.add_string('en') m.add_string('en')
@ -136,7 +135,7 @@ class AuthHandler (object):
def _disconnect_no_more_auth(self): def _disconnect_no_more_auth(self):
m = Message() m = Message()
m.add_byte(cMSG_DISCONNECT) m.add_byte(chr(MSG_DISCONNECT))
m.add_int(DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE) m.add_int(DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE)
m.add_string('No more auth methods available') m.add_string('No more auth methods available')
m.add_string('en') m.add_string('en')
@ -146,14 +145,14 @@ class AuthHandler (object):
def _get_session_blob(self, key, service, username): def _get_session_blob(self, key, service, username):
m = Message() m = Message()
m.add_string(self.transport.session_id) m.add_string(self.transport.session_id)
m.add_byte(cMSG_USERAUTH_REQUEST) m.add_byte(chr(MSG_USERAUTH_REQUEST))
m.add_string(username) m.add_string(username)
m.add_string(service) m.add_string(service)
m.add_string('publickey') m.add_string('publickey')
m.add_boolean(True) m.add_boolean(1)
m.add_string(key.get_name()) m.add_string(key.get_name())
m.add_string(key) m.add_string(str(key))
return m.asbytes() return str(m)
def wait_for_response(self, event): def wait_for_response(self, event):
while True: while True:
@ -169,7 +168,7 @@ class AuthHandler (object):
e = self.transport.get_exception() e = self.transport.get_exception()
if e is None: if e is None:
e = AuthenticationException('Authentication failed.') e = AuthenticationException('Authentication failed.')
# this is horrible. Python Exception isn't yet descended from # this is horrible. python Exception isn't yet descended from
# object, so type(e) won't work. :( # object, so type(e) won't work. :(
if issubclass(e.__class__, PartialAuthentication): if issubclass(e.__class__, PartialAuthentication):
return e.allowed_types return e.allowed_types
@ -177,11 +176,11 @@ class AuthHandler (object):
return [] return []
def _parse_service_request(self, m): def _parse_service_request(self, m):
service = m.get_text() service = m.get_string()
if self.transport.server_mode and (service == 'ssh-userauth'): if self.transport.server_mode and (service == 'ssh-userauth'):
# accepted # accepted
m = Message() m = Message()
m.add_byte(cMSG_SERVICE_ACCEPT) m.add_byte(chr(MSG_SERVICE_ACCEPT))
m.add_string(service) m.add_string(service)
self.transport._send_message(m) self.transport._send_message(m)
return return
@ -189,25 +188,27 @@ class AuthHandler (object):
self._disconnect_service_not_available() self._disconnect_service_not_available()
def _parse_service_accept(self, m): def _parse_service_accept(self, m):
service = m.get_text() service = m.get_string()
if service == 'ssh-userauth': if service == 'ssh-userauth':
self.transport._log(DEBUG, 'userauth is OK') self.transport._log(DEBUG, 'userauth is OK')
m = Message() m = Message()
m.add_byte(cMSG_USERAUTH_REQUEST) m.add_byte(chr(MSG_USERAUTH_REQUEST))
m.add_string(self.username) m.add_string(self.username)
m.add_string('ssh-connection') m.add_string('ssh-connection')
m.add_string(self.auth_method) m.add_string(self.auth_method)
if self.auth_method == 'password': if self.auth_method == 'password':
m.add_boolean(False) m.add_boolean(False)
password = bytestring(self.password) password = self.password
if isinstance(password, six.text_type):
password = password.encode('UTF-8')
m.add_string(password) m.add_string(password)
elif self.auth_method == 'publickey': elif self.auth_method == 'publickey':
m.add_boolean(True) m.add_boolean(True)
m.add_string(self.private_key.get_name()) m.add_string(self.private_key.get_name())
m.add_string(self.private_key) m.add_string(str(self.private_key))
blob = self._get_session_blob(self.private_key, 'ssh-connection', self.username) blob = self._get_session_blob(self.private_key, 'ssh-connection', self.username)
sig = self.private_key.sign_ssh_data(blob) sig = self.private_key.sign_ssh_data(self.transport.rng, blob)
m.add_string(sig) m.add_string(str(sig))
elif self.auth_method == 'keyboard-interactive': elif self.auth_method == 'keyboard-interactive':
m.add_string('') m.add_string('')
m.add_string(self.submethods) m.add_string(self.submethods)
@ -224,16 +225,16 @@ class AuthHandler (object):
m = Message() m = Message()
if result == AUTH_SUCCESSFUL: if result == AUTH_SUCCESSFUL:
self.transport._log(INFO, 'Auth granted (%s).' % method) self.transport._log(INFO, 'Auth granted (%s).' % method)
m.add_byte(cMSG_USERAUTH_SUCCESS) m.add_byte(chr(MSG_USERAUTH_SUCCESS))
self.authenticated = True self.authenticated = True
else: else:
self.transport._log(INFO, 'Auth rejected (%s).' % method) self.transport._log(INFO, 'Auth rejected (%s).' % method)
m.add_byte(cMSG_USERAUTH_FAILURE) m.add_byte(chr(MSG_USERAUTH_FAILURE))
m.add_string(self.transport.server_object.get_allowed_auths(username)) m.add_string(self.transport.server_object.get_allowed_auths(username))
if result == AUTH_PARTIALLY_SUCCESSFUL: if result == AUTH_PARTIALLY_SUCCESSFUL:
m.add_boolean(True) m.add_boolean(1)
else: else:
m.add_boolean(False) m.add_boolean(0)
self.auth_fail_count += 1 self.auth_fail_count += 1
self.transport._send_message(m) self.transport._send_message(m)
if self.auth_fail_count >= 10: if self.auth_fail_count >= 10:
@ -244,10 +245,10 @@ class AuthHandler (object):
def _interactive_query(self, q): def _interactive_query(self, q):
# make interactive query instead of response # make interactive query instead of response
m = Message() m = Message()
m.add_byte(cMSG_USERAUTH_INFO_REQUEST) m.add_byte(chr(MSG_USERAUTH_INFO_REQUEST))
m.add_string(q.name) m.add_string(q.name)
m.add_string(q.instructions) m.add_string(q.instructions)
m.add_string(bytes()) m.add_string('')
m.add_int(len(q.prompts)) m.add_int(len(q.prompts))
for p in q.prompts: for p in q.prompts:
m.add_string(p[0]) m.add_string(p[0])
@ -258,17 +259,17 @@ class AuthHandler (object):
if not self.transport.server_mode: if not self.transport.server_mode:
# er, uh... what? # er, uh... what?
m = Message() m = Message()
m.add_byte(cMSG_USERAUTH_FAILURE) m.add_byte(chr(MSG_USERAUTH_FAILURE))
m.add_string('none') m.add_string('none')
m.add_boolean(False) m.add_boolean(0)
self.transport._send_message(m) self.transport._send_message(m)
return return
if self.authenticated: if self.authenticated:
# ignore # ignore
return return
username = m.get_text() username = m.get_string()
service = m.get_text() service = m.get_string()
method = m.get_text() method = m.get_string()
self.transport._log(DEBUG, 'Auth request (type=%s) service=%s, username=%s' % (method, service, username)) self.transport._log(DEBUG, 'Auth request (type=%s) service=%s, username=%s' % (method, service, username))
if service != 'ssh-connection': if service != 'ssh-connection':
self._disconnect_service_not_available() self._disconnect_service_not_available()
@ -283,7 +284,7 @@ class AuthHandler (object):
result = self.transport.server_object.check_auth_none(username) result = self.transport.server_object.check_auth_none(username)
elif method == 'password': elif method == 'password':
changereq = m.get_boolean() changereq = m.get_boolean()
password = m.get_binary() password = m.get_string()
try: try:
password = password.decode('UTF-8') password = password.decode('UTF-8')
except UnicodeError: except UnicodeError:
@ -294,7 +295,7 @@ class AuthHandler (object):
# always treated as failure, since we don't support changing passwords, but collect # always treated as failure, since we don't support changing passwords, but collect
# the list of valid auth types from the callback anyway # the list of valid auth types from the callback anyway
self.transport._log(DEBUG, 'Auth request to change passwords (rejected)') self.transport._log(DEBUG, 'Auth request to change passwords (rejected)')
newpassword = m.get_binary() newpassword = m.get_string()
try: try:
newpassword = newpassword.decode('UTF-8', 'replace') newpassword = newpassword.decode('UTF-8', 'replace')
except UnicodeError: except UnicodeError:
@ -304,8 +305,8 @@ class AuthHandler (object):
result = self.transport.server_object.check_auth_password(username, password) result = self.transport.server_object.check_auth_password(username, password)
elif method == 'publickey': elif method == 'publickey':
sig_attached = m.get_boolean() sig_attached = m.get_boolean()
keytype = m.get_text() keytype = m.get_string()
keyblob = m.get_binary() keyblob = m.get_string()
try: try:
key = self.transport._key_info[keytype](Message(keyblob)) key = self.transport._key_info[keytype](Message(keyblob))
except SSHException as e: except SSHException as e:
@ -325,12 +326,12 @@ class AuthHandler (object):
# client wants to know if this key is acceptable, before it # client wants to know if this key is acceptable, before it
# signs anything... send special "ok" message # signs anything... send special "ok" message
m = Message() m = Message()
m.add_byte(cMSG_USERAUTH_PK_OK) m.add_byte(chr(MSG_USERAUTH_PK_OK))
m.add_string(keytype) m.add_string(keytype)
m.add_string(keyblob) m.add_string(keyblob)
self.transport._send_message(m) self.transport._send_message(m)
return return
sig = Message(m.get_binary()) sig = Message(m.get_string())
blob = self._get_session_blob(key, service, username) blob = self._get_session_blob(key, service, username)
if not key.verify_ssh_sig(blob, sig): if not key.verify_ssh_sig(blob, sig):
self.transport._log(INFO, 'Auth rejected: invalid signature') self.transport._log(INFO, 'Auth rejected: invalid signature')
@ -352,7 +353,7 @@ class AuthHandler (object):
self.transport._log(INFO, 'Authentication (%s) successful!' % self.auth_method) self.transport._log(INFO, 'Authentication (%s) successful!' % self.auth_method)
self.authenticated = True self.authenticated = True
self.transport._auth_trigger() self.transport._auth_trigger()
if self.auth_event is not None: if self.auth_event != None:
self.auth_event.set() self.auth_event.set()
def _parse_userauth_failure(self, m): def _parse_userauth_failure(self, m):
@ -370,30 +371,29 @@ class AuthHandler (object):
self.transport._log(INFO, 'Authentication (%s) failed.' % self.auth_method) self.transport._log(INFO, 'Authentication (%s) failed.' % self.auth_method)
self.authenticated = False self.authenticated = False
self.username = None self.username = None
if self.auth_event is not None: if self.auth_event != None:
self.auth_event.set() self.auth_event.set()
def _parse_userauth_banner(self, m): def _parse_userauth_banner(self, m):
banner = m.get_string() banner = m.get_string()
self.banner = banner
lang = m.get_string() lang = m.get_string()
self.transport._log(INFO, 'Auth banner: %s' % banner) self.transport._log(INFO, 'Auth banner: ' + banner)
# who cares. # who cares.
def _parse_userauth_info_request(self, m): def _parse_userauth_info_request(self, m):
if self.auth_method != 'keyboard-interactive': if self.auth_method != 'keyboard-interactive':
raise SSHException('Illegal info request from server') raise SSHException('Illegal info request from server')
title = m.get_text() title = m.get_string()
instructions = m.get_text() instructions = m.get_string()
m.get_binary() # lang m.get_string() # lang
prompts = m.get_int() prompts = m.get_int()
prompt_list = [] prompt_list = []
for i in range(prompts): for i in range(prompts):
prompt_list.append((m.get_text(), m.get_boolean())) prompt_list.append((m.get_string(), m.get_boolean()))
response_list = self.interactive_handler(title, instructions, prompt_list) response_list = self.interactive_handler(title, instructions, prompt_list)
m = Message() m = Message()
m.add_byte(cMSG_USERAUTH_INFO_RESPONSE) m.add_byte(chr(MSG_USERAUTH_INFO_RESPONSE))
m.add_int(len(response_list)) m.add_int(len(response_list))
for r in response_list: for r in response_list:
m.add_string(r) m.add_string(r)
@ -405,13 +405,14 @@ class AuthHandler (object):
n = m.get_int() n = m.get_int()
responses = [] responses = []
for i in range(n): for i in range(n):
responses.append(m.get_text()) responses.append(m.get_string())
result = self.transport.server_object.check_auth_interactive_response(responses) result = self.transport.server_object.check_auth_interactive_response(responses)
if isinstance(type(result), InteractiveQuery): if isinstance(type(result), InteractiveQuery):
# make interactive query instead of response # make interactive query instead of response
self._interactive_query(result) self._interactive_query(result)
return return
self._send_auth_result(self.auth_username, 'keyboard-interactive', result) self._send_auth_result(self.auth_username, 'keyboard-interactive', result)
_handler_table = { _handler_table = {
MSG_SERVICE_REQUEST: _parse_service_request, MSG_SERVICE_REQUEST: _parse_service_request,
@ -423,3 +424,4 @@ class AuthHandler (object):
MSG_USERAUTH_INFO_REQUEST: _parse_userauth_info_request, MSG_USERAUTH_INFO_REQUEST: _parse_userauth_info_request,
MSG_USERAUTH_INFO_RESPONSE: _parse_userauth_info_response, MSG_USERAUTH_INFO_RESPONSE: _parse_userauth_info_response,
} }

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -15,10 +15,11 @@
# You should have received a copy of the GNU Lesser General Public License # You should have received a copy of the GNU Lesser General Public License
# along with Paramiko; if not, write to the Free Software Foundation, Inc., # along with Paramiko; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
from paramiko.common import max_byte, zero_byte
from paramiko.py3compat import b, byte_ord, byte_chr, long
import paramiko.util as util from __future__ import absolute_import
from paramiko import util
import six
class BERException (Exception): class BERException (Exception):
@ -30,15 +31,12 @@ class BER(object):
Robey's tiny little attempt at a BER decoder. Robey's tiny little attempt at a BER decoder.
""" """
def __init__(self, content=bytes()): def __init__(self, content=''):
self.content = b(content) self.content = content
self.idx = 0 self.idx = 0
def asbytes(self):
return self.content
def __str__(self): def __str__(self):
return self.asbytes() return self.content
def __repr__(self): def __repr__(self):
return 'BER(\'' + repr(self.content) + '\')' return 'BER(\'' + repr(self.content) + '\')'
@ -49,13 +47,13 @@ class BER(object):
def decode_next(self): def decode_next(self):
if self.idx >= len(self.content): if self.idx >= len(self.content):
return None return None
ident = byte_ord(self.content[self.idx]) ident = ord(self.content[self.idx])
self.idx += 1 self.idx += 1
if (ident & 31) == 31: if (ident & 31) == 31:
# identifier > 30 # identifier > 30
ident = 0 ident = 0
while self.idx < len(self.content): while self.idx < len(self.content):
t = byte_ord(self.content[self.idx]) t = ord(self.content[self.idx])
self.idx += 1 self.idx += 1
ident = (ident << 7) | (t & 0x7f) ident = (ident << 7) | (t & 0x7f)
if not (t & 0x80): if not (t & 0x80):
@ -63,7 +61,7 @@ class BER(object):
if self.idx >= len(self.content): if self.idx >= len(self.content):
return None return None
# now fetch length # now fetch length
size = byte_ord(self.content[self.idx]) size = ord(self.content[self.idx])
self.idx += 1 self.idx += 1
if size & 0x80: if size & 0x80:
# more complimicated... # more complimicated...
@ -71,12 +69,12 @@ class BER(object):
t = size & 0x7f t = size & 0x7f
if self.idx + t > len(self.content): if self.idx + t > len(self.content):
return None return None
size = util.inflate_long(self.content[self.idx: self.idx + t], True) size = util.inflate_long(self.content[self.idx : self.idx + t], True)
self.idx += t self.idx += t
if self.idx + size > len(self.content): if self.idx + size > len(self.content):
# can't fit # can't fit
return None return None
data = self.content[self.idx: self.idx + size] data = self.content[self.idx : self.idx + size]
self.idx += size self.idx += size
# now switch on id # now switch on id
if ident == 0x30: if ident == 0x30:
@ -91,9 +89,9 @@ class BER(object):
def decode_sequence(data): def decode_sequence(data):
out = [] out = []
ber = BER(data) b = BER(data)
while True: while True:
x = ber.decode_next() x = b.decode_next()
if x is None: if x is None:
break break
out.append(x) out.append(x)
@ -102,21 +100,21 @@ class BER(object):
def encode_tlv(self, ident, val): def encode_tlv(self, ident, val):
# no need to support ident > 31 here # no need to support ident > 31 here
self.content += byte_chr(ident) self.content += chr(ident)
if len(val) > 0x7f: if len(val) > 0x7f:
lenstr = util.deflate_long(len(val)) lenstr = util.deflate_long(len(val))
self.content += byte_chr(0x80 + len(lenstr)) + lenstr self.content += chr(0x80 + len(lenstr)) + lenstr
else: else:
self.content += byte_chr(len(val)) self.content += chr(len(val))
self.content += val self.content += val
def encode(self, x): def encode(self, x):
if type(x) is bool: if type(x) is bool:
if x: if x:
self.encode_tlv(1, max_byte) self.encode_tlv(1, '\xff')
else: else:
self.encode_tlv(1, zero_byte) self.encode_tlv(1, '\x00')
elif (type(x) is int) or (type(x) is long): elif isinstance(x, six.integer_types):
self.encode_tlv(2, util.deflate_long(x)) self.encode_tlv(2, util.deflate_long(x))
elif type(x) is str: elif type(x) is str:
self.encode_tlv(4, x) self.encode_tlv(4, x)
@ -126,8 +124,8 @@ class BER(object):
raise BERException('Unknown type for encoding: %s' % repr(type(x))) raise BERException('Unknown type for encoding: %s' % repr(type(x)))
def encode_sequence(data): def encode_sequence(data):
ber = BER() b = BER()
for item in data: for item in data:
ber.encode(item) b.encode(item)
return ber.asbytes() return str(b)
encode_sequence = staticmethod(encode_sequence) encode_sequence = staticmethod(encode_sequence)

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -17,7 +17,7 @@
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
""" """
Attempt to generalize the "feeder" part of a `.Channel`: an object which can be Attempt to generalize the "feeder" part of a Channel: an object which can be
read from and closed, but is reading from a buffer fed by another thread. The read from and closed, but is reading from a buffer fed by another thread. The
read operations are blocking and can have a timeout set. read operations are blocking and can have a timeout set.
""" """
@ -25,12 +25,11 @@ read operations are blocking and can have a timeout set.
import array import array
import threading import threading
import time import time
from paramiko.py3compat import PY2, b
class PipeTimeout (IOError): class PipeTimeout (IOError):
""" """
Indicates that a timeout was reached on a read from a `.BufferedPipe`. Indicates that a timeout was reached on a read from a L{BufferedPipe}.
""" """
pass pass
@ -39,7 +38,7 @@ class BufferedPipe (object):
""" """
A buffer that obeys normal read (with timeout) & close semantics for a A buffer that obeys normal read (with timeout) & close semantics for a
file or socket, but is fed data from another thread. This is used by file or socket, but is fed data from another thread. This is used by
`.Channel`. L{Channel}.
""" """
def __init__(self): def __init__(self):
@ -49,26 +48,14 @@ class BufferedPipe (object):
self._buffer = array.array('B') self._buffer = array.array('B')
self._closed = False self._closed = False
if PY2:
def _buffer_frombytes(self, data):
self._buffer.fromstring(data)
def _buffer_tobytes(self, limit=None):
return self._buffer[:limit].tostring()
else:
def _buffer_frombytes(self, data):
self._buffer.frombytes(data)
def _buffer_tobytes(self, limit=None):
return self._buffer[:limit].tobytes()
def set_event(self, event): def set_event(self, event):
""" """
Set an event on this buffer. When data is ready to be read (or the Set an event on this buffer. When data is ready to be read (or the
buffer has been closed), the event will be set. When no data is buffer has been closed), the event will be set. When no data is
ready, the event will be cleared. ready, the event will be cleared.
:param threading.Event event: the event to set/clear @param event: the event to set/clear
@type event: Event
""" """
self._event = event self._event = event
if len(self._buffer) > 0: if len(self._buffer) > 0:
@ -81,13 +68,14 @@ class BufferedPipe (object):
Feed new data into this pipe. This method is assumed to be called Feed new data into this pipe. This method is assumed to be called
from a separate thread, so synchronization is done. from a separate thread, so synchronization is done.
:param data: the data to add, as a `str` @param data: the data to add
@type data: str
""" """
self._lock.acquire() self._lock.acquire()
try: try:
if self._event is not None: if self._event is not None:
self._event.set() self._event.set()
self._buffer_frombytes(b(data)) self._buffer.fromstring(data)
self._cv.notifyAll() self._cv.notifyAll()
finally: finally:
self._lock.release() self._lock.release()
@ -95,12 +83,12 @@ class BufferedPipe (object):
def read_ready(self): def read_ready(self):
""" """
Returns true if data is buffered and ready to be read from this Returns true if data is buffered and ready to be read from this
feeder. A ``False`` result does not mean that the feeder has closed; feeder. A C{False} result does not mean that the feeder has closed;
it means you may need to wait before more data arrives. it means you may need to wait before more data arrives.
:return: @return: C{True} if a L{read} call would immediately return at least
``True`` if a `read` call would immediately return at least one one byte; C{False} otherwise.
byte; ``False`` otherwise. @rtype: bool
""" """
self._lock.acquire() self._lock.acquire()
try: try:
@ -114,24 +102,26 @@ class BufferedPipe (object):
""" """
Read data from the pipe. The return value is a string representing Read data from the pipe. The return value is a string representing
the data received. The maximum amount of data to be received at once the data received. The maximum amount of data to be received at once
is specified by ``nbytes``. If a string of length zero is returned, is specified by C{nbytes}. If a string of length zero is returned,
the pipe has been closed. the pipe has been closed.
The optional ``timeout`` argument can be a nonnegative float expressing The optional C{timeout} argument can be a nonnegative float expressing
seconds, or ``None`` for no timeout. If a float is given, a seconds, or C{None} for no timeout. If a float is given, a
`.PipeTimeout` will be raised if the timeout period value has elapsed C{PipeTimeout} will be raised if the timeout period value has
before any data arrives. elapsed before any data arrives.
:param int nbytes: maximum number of bytes to read @param nbytes: maximum number of bytes to read
:param float timeout: @type nbytes: int
maximum seconds to wait (or ``None``, the default, to wait forever) @param timeout: maximum seconds to wait (or C{None}, the default, to
:return: the read data, as a `str` wait forever)
@type timeout: float
@return: data
@rtype: str
:raises PipeTimeout: @raise PipeTimeout: if a timeout was specified and no data was ready
if a timeout was specified and no data was ready before that before that timeout
timeout
""" """
out = bytes() out = ''
self._lock.acquire() self._lock.acquire()
try: try:
if len(self._buffer) == 0: if len(self._buffer) == 0:
@ -152,12 +142,12 @@ class BufferedPipe (object):
# something's in the buffer and we have the lock! # something's in the buffer and we have the lock!
if len(self._buffer) <= nbytes: if len(self._buffer) <= nbytes:
out = self._buffer_tobytes() out = self._buffer.tostring()
del self._buffer[:] del self._buffer[:]
if (self._event is not None) and not self._closed: if (self._event is not None) and not self._closed:
self._event.clear() self._event.clear()
else: else:
out = self._buffer_tobytes(nbytes) out = self._buffer[:nbytes].tostring()
del self._buffer[:nbytes] del self._buffer[:nbytes]
finally: finally:
self._lock.release() self._lock.release()
@ -168,13 +158,12 @@ class BufferedPipe (object):
""" """
Clear out the buffer and return all data that was in it. Clear out the buffer and return all data that was in it.
:return: @return: any data that was in the buffer prior to clearing it out
any data that was in the buffer prior to clearing it out, as a @rtype: str
`str`
""" """
self._lock.acquire() self._lock.acquire()
try: try:
out = self._buffer_tobytes() out = self._buffer.tostring()
del self._buffer[:] del self._buffer[:]
if (self._event is not None) and not self._closed: if (self._event is not None) and not self._closed:
self._event.clear() self._event.clear()
@ -184,7 +173,7 @@ class BufferedPipe (object):
def close(self): def close(self):
""" """
Close this pipe object. Future calls to `read` after the buffer Close this pipe object. Future calls to L{read} after the buffer
has been emptied will return immediately with an empty string. has been emptied will return immediately with an empty string.
""" """
self._lock.acquire() self._lock.acquire()
@ -200,10 +189,12 @@ class BufferedPipe (object):
""" """
Return the number of bytes buffered. Return the number of bytes buffered.
:return: number (`int`) of bytes buffered @return: number of bytes bufferes
@rtype: int
""" """
self._lock.acquire() self._lock.acquire()
try: try:
return len(self._buffer) return len(self._buffer)
finally: finally:
self._lock.release() self._lock.release()

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -17,7 +17,7 @@
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
""" """
SSH client & key policies L{SSHClient}.
""" """
from binascii import hexlify from binascii import hexlify
@ -25,13 +25,13 @@ import getpass
import os import os
import socket import socket
import warnings import warnings
import six
from paramiko.agent import Agent from paramiko.agent import Agent
from paramiko.common import DEBUG from paramiko.common import *
from paramiko.config import SSH_PORT from paramiko.config import SSH_PORT
from paramiko.dsskey import DSSKey from paramiko.dsskey import DSSKey
from paramiko.hostkeys import HostKeys from paramiko.hostkeys import HostKeys
from paramiko.py3compat import string_types
from paramiko.resource import ResourceManager from paramiko.resource import ResourceManager
from paramiko.rsakey import RSAKey from paramiko.rsakey import RSAKey
from paramiko.ssh_exception import SSHException, BadHostKeyException from paramiko.ssh_exception import SSHException, BadHostKeyException
@ -39,10 +39,67 @@ from paramiko.transport import Transport
from paramiko.util import retry_on_signal from paramiko.util import retry_on_signal
class MissingHostKeyPolicy (object):
"""
Interface for defining the policy that L{SSHClient} should use when the
SSH server's hostname is not in either the system host keys or the
application's keys. Pre-made classes implement policies for automatically
adding the key to the application's L{HostKeys} object (L{AutoAddPolicy}),
and for automatically rejecting the key (L{RejectPolicy}).
This function may be used to ask the user to verify the key, for example.
"""
def missing_host_key(self, client, hostname, key):
"""
Called when an L{SSHClient} receives a server key for a server that
isn't in either the system or local L{HostKeys} object. To accept
the key, simply return. To reject, raised an exception (which will
be passed to the calling application).
"""
pass
class AutoAddPolicy (MissingHostKeyPolicy):
"""
Policy for automatically adding the hostname and new host key to the
local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
"""
def missing_host_key(self, client, hostname, key):
client._host_keys.add(hostname, key.get_name(), key)
if client._host_keys_filename is not None:
client.save_host_keys(client._host_keys_filename)
client._log(DEBUG, 'Adding %s host key for %s: %s' %
(key.get_name(), hostname, hexlify(key.get_fingerprint())))
class RejectPolicy (MissingHostKeyPolicy):
"""
Policy for automatically rejecting the unknown hostname & key. This is
used by L{SSHClient}.
"""
def missing_host_key(self, client, hostname, key):
client._log(DEBUG, 'Rejecting %s host key for %s: %s' %
(key.get_name(), hostname, hexlify(key.get_fingerprint())))
raise SSHException('Server %r not found in known_hosts' % hostname)
class WarningPolicy (MissingHostKeyPolicy):
"""
Policy for logging a python-style warning for an unknown host key, but
accepting it. This is used by L{SSHClient}.
"""
def missing_host_key(self, client, hostname, key):
warnings.warn('Unknown %s host key for %s: %s' %
(key.get_name(), hostname, hexlify(key.get_fingerprint())))
class SSHClient (object): class SSHClient (object):
""" """
A high-level representation of a session with an SSH server. This class A high-level representation of a session with an SSH server. This class
wraps `.Transport`, `.Channel`, and `.SFTPClient` to take care of most wraps L{Transport}, L{Channel}, and L{SFTPClient} to take care of most
aspects of authenticating and opening channels. A typical use case is:: aspects of authenticating and opening channels. A typical use case is::
client = SSHClient() client = SSHClient()
@ -54,7 +111,7 @@ class SSHClient (object):
checking. The default mechanism is to try to use local key files or an checking. The default mechanism is to try to use local key files or an
SSH agent (if one is running). SSH agent (if one is running).
.. versionadded:: 1.6 @since: 1.6
""" """
def __init__(self): def __init__(self):
@ -72,21 +129,22 @@ class SSHClient (object):
def load_system_host_keys(self, filename=None): def load_system_host_keys(self, filename=None):
""" """
Load host keys from a system (read-only) file. Host keys read with Load host keys from a system (read-only) file. Host keys read with
this method will not be saved back by `save_host_keys`. this method will not be saved back by L{save_host_keys}.
This method can be called multiple times. Each new set of host keys This method can be called multiple times. Each new set of host keys
will be merged with the existing set (new replacing old if there are will be merged with the existing set (new replacing old if there are
conflicts). conflicts).
If ``filename`` is left as ``None``, an attempt will be made to read If C{filename} is left as C{None}, an attempt will be made to read
keys from the user's local "known hosts" file, as used by OpenSSH, keys from the user's local "known hosts" file, as used by OpenSSH,
and no exception will be raised if the file can't be read. This is and no exception will be raised if the file can't be read. This is
probably only useful on posix. probably only useful on posix.
:param str filename: the filename to read, or ``None`` @param filename: the filename to read, or C{None}
@type filename: str
:raises IOError: @raise IOError: if a filename was provided and the file could not be
if a filename was provided and the file could not be read read
""" """
if filename is None: if filename is None:
# try the user's .ssh key file, and mask exceptions # try the user's .ssh key file, and mask exceptions
@ -101,18 +159,19 @@ class SSHClient (object):
def load_host_keys(self, filename): def load_host_keys(self, filename):
""" """
Load host keys from a local host-key file. Host keys read with this Load host keys from a local host-key file. Host keys read with this
method will be checked after keys loaded via `load_system_host_keys`, method will be checked I{after} keys loaded via L{load_system_host_keys},
but will be saved back by `save_host_keys` (so they can be modified). but will be saved back by L{save_host_keys} (so they can be modified).
The missing host key policy `.AutoAddPolicy` adds keys to this set and The missing host key policy L{AutoAddPolicy} adds keys to this set and
saves them, when connecting to a previously-unknown server. saves them, when connecting to a previously-unknown server.
This method can be called multiple times. Each new set of host keys This method can be called multiple times. Each new set of host keys
will be merged with the existing set (new replacing old if there are will be merged with the existing set (new replacing old if there are
conflicts). When automatically saving, the last hostname is used. conflicts). When automatically saving, the last hostname is used.
:param str filename: the filename to read @param filename: the filename to read
@type filename: str
:raises IOError: if the filename could not be read @raise IOError: if the filename could not be read
""" """
self._host_keys_filename = filename self._host_keys_filename = filename
self._host_keys.load(filename) self._host_keys.load(filename)
@ -120,52 +179,56 @@ class SSHClient (object):
def save_host_keys(self, filename): def save_host_keys(self, filename):
""" """
Save the host keys back to a file. Only the host keys loaded with Save the host keys back to a file. Only the host keys loaded with
`load_host_keys` (plus any added directly) will be saved -- not any L{load_host_keys} (plus any added directly) will be saved -- not any
host keys loaded with `load_system_host_keys`. host keys loaded with L{load_system_host_keys}.
:param str filename: the filename to save to @param filename: the filename to save to
@type filename: str
:raises IOError: if the file could not be written @raise IOError: if the file could not be written
""" """
# update local host keys from file (in case other SSH clients # update local host keys from file (in case other SSH clients
# have written to the known_hosts file meanwhile. # have written to the known_hosts file meanwhile.
if self._host_keys_filename is not None: if self.known_hosts is not None:
self.load_host_keys(self._host_keys_filename) self.load_host_keys(self.known_hosts)
with open(filename, 'w') as f: f = open(filename, 'w')
for hostname, keys in self._host_keys.items(): for hostname, keys in self._host_keys.iteritems():
for keytype, key in keys.items(): for keytype, key in keys.iteritems():
f.write('%s %s %s\n' % (hostname, keytype, key.get_base64())) f.write('%s %s %s\n' % (hostname, keytype, key.get_base64()))
f.close()
def get_host_keys(self): def get_host_keys(self):
""" """
Get the local `.HostKeys` object. This can be used to examine the Get the local L{HostKeys} object. This can be used to examine the
local host keys or change them. local host keys or change them.
:return: the local host keys as a `.HostKeys` object. @return: the local host keys
@rtype: L{HostKeys}
""" """
return self._host_keys return self._host_keys
def set_log_channel(self, name): def set_log_channel(self, name):
""" """
Set the channel for logging. The default is ``"paramiko.transport"`` Set the channel for logging. The default is C{"paramiko.transport"}
but it can be set to anything you want. but it can be set to anything you want.
:param str name: new channel name for logging @param name: new channel name for logging
@type name: str
""" """
self._log_channel = name self._log_channel = name
def set_missing_host_key_policy(self, policy): def set_missing_host_key_policy(self, policy):
""" """
Set the policy to use when connecting to a server that doesn't have a Set the policy to use when connecting to a server that doesn't have a
host key in either the system or local `.HostKeys` objects. The host key in either the system or local L{HostKeys} objects. The
default policy is to reject all unknown servers (using `.RejectPolicy`). default policy is to reject all unknown servers (using L{RejectPolicy}).
You may substitute `.AutoAddPolicy` or write your own policy class. You may substitute L{AutoAddPolicy} or write your own policy class.
:param .MissingHostKeyPolicy policy: @param policy: the policy to use when receiving a host key from a
the policy to use when receiving a host key from a
previously-unknown server previously-unknown server
@type policy: L{MissingHostKeyPolicy}
""" """
self._policy = policy self._policy = policy
@ -174,49 +237,56 @@ class SSHClient (object):
compress=False, sock=None): compress=False, sock=None):
""" """
Connect to an SSH server and authenticate to it. The server's host key Connect to an SSH server and authenticate to it. The server's host key
is checked against the system host keys (see `load_system_host_keys`) is checked against the system host keys (see L{load_system_host_keys})
and any local host keys (`load_host_keys`). If the server's hostname and any local host keys (L{load_host_keys}). If the server's hostname
is not found in either set of host keys, the missing host key policy is not found in either set of host keys, the missing host key policy
is used (see `set_missing_host_key_policy`). The default policy is is used (see L{set_missing_host_key_policy}). The default policy is
to reject the key and raise an `.SSHException`. to reject the key and raise an L{SSHException}.
Authentication is attempted in the following order of priority: Authentication is attempted in the following order of priority:
- The ``pkey`` or ``key_filename`` passed in (if any) - The C{pkey} or C{key_filename} passed in (if any)
- Any key we can find through an SSH agent - Any key we can find through an SSH agent
- Any "id_rsa" or "id_dsa" key discoverable in ``~/.ssh/`` - Any "id_rsa" or "id_dsa" key discoverable in C{~/.ssh/}
- Plain username/password auth, if a password was given - Plain username/password auth, if a password was given
If a private key requires a password to unlock it, and a password is If a private key requires a password to unlock it, and a password is
passed in, that password will be used to attempt to unlock the key. passed in, that password will be used to attempt to unlock the key.
:param str hostname: the server to connect to @param hostname: the server to connect to
:param int port: the server port to connect to @type hostname: str
:param str username: @param port: the server port to connect to
the username to authenticate as (defaults to the current local @type port: int
username) @param username: the username to authenticate as (defaults to the
:param str password: current local username)
a password to use for authentication or for unlocking a private key @type username: str
:param .PKey pkey: an optional private key to use for authentication @param password: a password to use for authentication or for unlocking
:param str key_filename: a private key
the filename, or list of filenames, of optional private key(s) to @type password: str
try for authentication @param pkey: an optional private key to use for authentication
:param float timeout: an optional timeout (in seconds) for the TCP connect @type pkey: L{PKey}
:param bool allow_agent: set to False to disable connecting to the SSH agent @param key_filename: the filename, or list of filenames, of optional
:param bool look_for_keys: private key(s) to try for authentication
set to False to disable searching for discoverable private key @type key_filename: str or list(str)
files in ``~/.ssh/`` @param timeout: an optional timeout (in seconds) for the TCP connect
:param bool compress: set to True to turn on compression @type timeout: float
:param socket sock: @param allow_agent: set to False to disable connecting to the SSH agent
an open socket or socket-like object (such as a `.Channel`) to use @type allow_agent: bool
for communication to the target host @param look_for_keys: set to False to disable searching for discoverable
private key files in C{~/.ssh/}
@type look_for_keys: bool
@param compress: set to True to turn on compression
@type compress: bool
@param sock: an open socket or socket-like object (such as a
L{Channel}) to use for communication to the target host
@type sock: socket
:raises BadHostKeyException: if the server's host key could not be @raise BadHostKeyException: if the server's host key could not be
verified verified
:raises AuthenticationException: if authentication failed @raise AuthenticationException: if authentication failed
:raises SSHException: if there was any other error connecting or @raise SSHException: if there was any other error connecting or
establishing an SSH session establishing an SSH session
:raises socket.error: if a socket error occurred while connecting @raise socket.error: if a socket error occurred while connecting
""" """
if not sock: if not sock:
for (family, socktype, proto, canonname, sockaddr) in socket.getaddrinfo(hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM): for (family, socktype, proto, canonname, sockaddr) in socket.getaddrinfo(hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM):
@ -266,70 +336,74 @@ class SSHClient (object):
if key_filename is None: if key_filename is None:
key_filenames = [] key_filenames = []
elif isinstance(key_filename, string_types): elif isinstance(key_filename, (six.binary_type, six.text_type)):
key_filenames = [key_filename] key_filenames = [ key_filename ]
else: else:
key_filenames = key_filename key_filenames = key_filename
self._auth(username, password, pkey, key_filenames, allow_agent, look_for_keys) self._auth(username, password, pkey, key_filenames, allow_agent, look_for_keys)
def close(self): def close(self):
""" """
Close this SSHClient and its underlying `.Transport`. Close this SSHClient and its underlying L{Transport}.
""" """
if self._transport is None: if self._transport is None:
return return
self._transport.close() self._transport.close()
self._transport = None self._transport = None
if self._agent is not None: if self._agent != None:
self._agent.close() self._agent.close()
self._agent = None self._agent = None
def exec_command(self, command, bufsize=-1, timeout=None, get_pty=False): def exec_command(self, command, bufsize=-1, timeout=None, get_pty=False):
""" """
Execute a command on the SSH server. A new `.Channel` is opened and Execute a command on the SSH server. A new L{Channel} is opened and
the requested command is executed. The command's input and output the requested command is executed. The command's input and output
streams are returned as Python ``file``-like objects representing streams are returned as python C{file}-like objects representing
stdin, stdout, and stderr. stdin, stdout, and stderr.
:param str command: the command to execute @param command: the command to execute
:param int bufsize: @type command: str
interpreted the same way as by the built-in ``file()`` function in @param bufsize: interpreted the same way as by the built-in C{file()} function in python
Python @type bufsize: int
:param int timeout: @param timeout: set command's channel timeout. See L{Channel.settimeout}.settimeout
set command's channel timeout. See `Channel.settimeout`.settimeout @type timeout: int
:return: @return: the stdin, stdout, and stderr of the executing command
the stdin, stdout, and stderr of the executing command, as a @rtype: tuple(L{ChannelFile}, L{ChannelFile}, L{ChannelFile})
3-tuple
:raises SSHException: if the server fails to execute the command @raise SSHException: if the server fails to execute the command
""" """
chan = self._transport.open_session() chan = self._transport.open_session()
if get_pty: if(get_pty):
chan.get_pty() chan.get_pty()
chan.settimeout(timeout) chan.settimeout(timeout)
chan.exec_command(command) chan.exec_command(command)
stdin = chan.makefile('wb', bufsize) stdin = chan.makefile('wb', bufsize)
stdout = chan.makefile('r', bufsize) stdout = chan.makefile('rb', bufsize)
stderr = chan.makefile_stderr('r', bufsize) stderr = chan.makefile_stderr('rb', bufsize)
return stdin, stdout, stderr return stdin, stdout, stderr
def invoke_shell(self, term='vt100', width=80, height=24, width_pixels=0, def invoke_shell(self, term='vt100', width=80, height=24, width_pixels=0,
height_pixels=0): height_pixels=0):
""" """
Start an interactive shell session on the SSH server. A new `.Channel` Start an interactive shell session on the SSH server. A new L{Channel}
is opened and connected to a pseudo-terminal using the requested is opened and connected to a pseudo-terminal using the requested
terminal type and size. terminal type and size.
:param str term: @param term: the terminal type to emulate (for example, C{"vt100"})
the terminal type to emulate (for example, ``"vt100"``) @type term: str
:param int width: the width (in characters) of the terminal window @param width: the width (in characters) of the terminal window
:param int height: the height (in characters) of the terminal window @type width: int
:param int width_pixels: the width (in pixels) of the terminal window @param height: the height (in characters) of the terminal window
:param int height_pixels: the height (in pixels) of the terminal window @type height: int
:return: a new `.Channel` connected to the remote shell @param width_pixels: the width (in pixels) of the terminal window
@type width_pixels: int
@param height_pixels: the height (in pixels) of the terminal window
@type height_pixels: int
@return: a new channel connected to the remote shell
@rtype: L{Channel}
:raises SSHException: if the server fails to invoke a shell @raise SSHException: if the server fails to invoke a shell
""" """
chan = self._transport.open_session() chan = self._transport.open_session()
chan.get_pty(term, width, height, width_pixels, height_pixels) chan.get_pty(term, width, height, width_pixels, height_pixels)
@ -340,17 +414,19 @@ class SSHClient (object):
""" """
Open an SFTP session on the SSH server. Open an SFTP session on the SSH server.
:return: a new `.SFTPClient` session object @return: a new SFTP session object
@rtype: L{SFTPClient}
""" """
return self._transport.open_sftp_client() return self._transport.open_sftp_client()
def get_transport(self): def get_transport(self):
""" """
Return the underlying `.Transport` object for this SSH connection. Return the underlying L{Transport} object for this SSH connection.
This can be used to perform lower-level tasks, like opening specific This can be used to perform lower-level tasks, like opening specific
kinds of channels. kinds of channels.
:return: the `.Transport` for this connection @return: the Transport for this connection
@rtype: L{Transport}
""" """
return self._transport return self._transport
@ -395,7 +471,7 @@ class SSHClient (object):
saved_exception = e saved_exception = e
if not two_factor and allow_agent: if not two_factor and allow_agent:
if self._agent is None: if self._agent == None:
self._agent = Agent() self._agent = Agent()
for key in self._agent.get_keys(): for key in self._agent.get_keys():
@ -439,7 +515,9 @@ class SSHClient (object):
if not two_factor: if not two_factor:
return return
break break
except (SSHException, IOError) as e: except SSHException as e:
saved_exception = e
except IOError as e:
saved_exception = e saved_exception = e
if password is not None: if password is not None:
@ -459,59 +537,3 @@ class SSHClient (object):
def _log(self, level, msg): def _log(self, level, msg):
self._transport._log(level, msg) self._transport._log(level, msg)
class MissingHostKeyPolicy (object):
"""
Interface for defining the policy that `.SSHClient` should use when the
SSH server's hostname is not in either the system host keys or the
application's keys. Pre-made classes implement policies for automatically
adding the key to the application's `.HostKeys` object (`.AutoAddPolicy`),
and for automatically rejecting the key (`.RejectPolicy`).
This function may be used to ask the user to verify the key, for example.
"""
def missing_host_key(self, client, hostname, key):
"""
Called when an `.SSHClient` receives a server key for a server that
isn't in either the system or local `.HostKeys` object. To accept
the key, simply return. To reject, raised an exception (which will
be passed to the calling application).
"""
pass
class AutoAddPolicy (MissingHostKeyPolicy):
"""
Policy for automatically adding the hostname and new host key to the
local `.HostKeys` object, and saving it. This is used by `.SSHClient`.
"""
def missing_host_key(self, client, hostname, key):
client._host_keys.add(hostname, key.get_name(), key)
if client._host_keys_filename is not None:
client.save_host_keys(client._host_keys_filename)
client._log(DEBUG, 'Adding %s host key for %s: %s' %
(key.get_name(), hostname, hexlify(key.get_fingerprint())))
class RejectPolicy (MissingHostKeyPolicy):
"""
Policy for automatically rejecting the unknown hostname & key. This is
used by `.SSHClient`.
"""
def missing_host_key(self, client, hostname, key):
client._log(DEBUG, 'Rejecting %s host key for %s: %s' %
(key.get_name(), hostname, hexlify(key.get_fingerprint())))
raise SSHException('Server %r not found in known_hosts' % hostname)
class WarningPolicy (MissingHostKeyPolicy):
"""
Policy for logging a Python-style warning for an unknown host key, but
accepting it. This is used by `.SSHClient`.
"""
def missing_host_key(self, client, hostname, key):
warnings.warn('Unknown %s host key for %s: %s' %
(key.get_name(), hostname, hexlify(key.get_fingerprint())))

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -19,14 +19,12 @@
""" """
Common constants and global variables. Common constants and global variables.
""" """
import logging
from paramiko.py3compat import byte_chr, PY2, bytes_types, string_types, b, long
MSG_DISCONNECT, MSG_IGNORE, MSG_UNIMPLEMENTED, MSG_DEBUG, MSG_SERVICE_REQUEST, \ MSG_DISCONNECT, MSG_IGNORE, MSG_UNIMPLEMENTED, MSG_DEBUG, MSG_SERVICE_REQUEST, \
MSG_SERVICE_ACCEPT = range(1, 7) MSG_SERVICE_ACCEPT = range(1, 7)
MSG_KEXINIT, MSG_NEWKEYS = range(20, 22) MSG_KEXINIT, MSG_NEWKEYS = range(20, 22)
MSG_USERAUTH_REQUEST, MSG_USERAUTH_FAILURE, MSG_USERAUTH_SUCCESS, \ MSG_USERAUTH_REQUEST, MSG_USERAUTH_FAILURE, MSG_USERAUTH_SUCCESS, \
MSG_USERAUTH_BANNER = range(50, 54) MSG_USERAUTH_BANNER = range(50, 54)
MSG_USERAUTH_PK_OK = 60 MSG_USERAUTH_PK_OK = 60
MSG_USERAUTH_INFO_REQUEST, MSG_USERAUTH_INFO_RESPONSE = range(60, 62) MSG_USERAUTH_INFO_REQUEST, MSG_USERAUTH_INFO_RESPONSE = range(60, 62)
MSG_GLOBAL_REQUEST, MSG_REQUEST_SUCCESS, MSG_REQUEST_FAILURE = range(80, 83) MSG_GLOBAL_REQUEST, MSG_REQUEST_SUCCESS, MSG_REQUEST_FAILURE = range(80, 83)
@ -35,35 +33,6 @@ MSG_CHANNEL_OPEN, MSG_CHANNEL_OPEN_SUCCESS, MSG_CHANNEL_OPEN_FAILURE, \
MSG_CHANNEL_EOF, MSG_CHANNEL_CLOSE, MSG_CHANNEL_REQUEST, \ MSG_CHANNEL_EOF, MSG_CHANNEL_CLOSE, MSG_CHANNEL_REQUEST, \
MSG_CHANNEL_SUCCESS, MSG_CHANNEL_FAILURE = range(90, 101) MSG_CHANNEL_SUCCESS, MSG_CHANNEL_FAILURE = range(90, 101)
cMSG_DISCONNECT = byte_chr(MSG_DISCONNECT)
cMSG_IGNORE = byte_chr(MSG_IGNORE)
cMSG_UNIMPLEMENTED = byte_chr(MSG_UNIMPLEMENTED)
cMSG_DEBUG = byte_chr(MSG_DEBUG)
cMSG_SERVICE_REQUEST = byte_chr(MSG_SERVICE_REQUEST)
cMSG_SERVICE_ACCEPT = byte_chr(MSG_SERVICE_ACCEPT)
cMSG_KEXINIT = byte_chr(MSG_KEXINIT)
cMSG_NEWKEYS = byte_chr(MSG_NEWKEYS)
cMSG_USERAUTH_REQUEST = byte_chr(MSG_USERAUTH_REQUEST)
cMSG_USERAUTH_FAILURE = byte_chr(MSG_USERAUTH_FAILURE)
cMSG_USERAUTH_SUCCESS = byte_chr(MSG_USERAUTH_SUCCESS)
cMSG_USERAUTH_BANNER = byte_chr(MSG_USERAUTH_BANNER)
cMSG_USERAUTH_PK_OK = byte_chr(MSG_USERAUTH_PK_OK)
cMSG_USERAUTH_INFO_REQUEST = byte_chr(MSG_USERAUTH_INFO_REQUEST)
cMSG_USERAUTH_INFO_RESPONSE = byte_chr(MSG_USERAUTH_INFO_RESPONSE)
cMSG_GLOBAL_REQUEST = byte_chr(MSG_GLOBAL_REQUEST)
cMSG_REQUEST_SUCCESS = byte_chr(MSG_REQUEST_SUCCESS)
cMSG_REQUEST_FAILURE = byte_chr(MSG_REQUEST_FAILURE)
cMSG_CHANNEL_OPEN = byte_chr(MSG_CHANNEL_OPEN)
cMSG_CHANNEL_OPEN_SUCCESS = byte_chr(MSG_CHANNEL_OPEN_SUCCESS)
cMSG_CHANNEL_OPEN_FAILURE = byte_chr(MSG_CHANNEL_OPEN_FAILURE)
cMSG_CHANNEL_WINDOW_ADJUST = byte_chr(MSG_CHANNEL_WINDOW_ADJUST)
cMSG_CHANNEL_DATA = byte_chr(MSG_CHANNEL_DATA)
cMSG_CHANNEL_EXTENDED_DATA = byte_chr(MSG_CHANNEL_EXTENDED_DATA)
cMSG_CHANNEL_EOF = byte_chr(MSG_CHANNEL_EOF)
cMSG_CHANNEL_CLOSE = byte_chr(MSG_CHANNEL_CLOSE)
cMSG_CHANNEL_REQUEST = byte_chr(MSG_CHANNEL_REQUEST)
cMSG_CHANNEL_SUCCESS = byte_chr(MSG_CHANNEL_SUCCESS)
cMSG_CHANNEL_FAILURE = byte_chr(MSG_CHANNEL_FAILURE)
# for debugging: # for debugging:
MSG_NAMES = { MSG_NAMES = {
@ -100,7 +69,7 @@ MSG_NAMES = {
MSG_CHANNEL_REQUEST: 'channel-request', MSG_CHANNEL_REQUEST: 'channel-request',
MSG_CHANNEL_SUCCESS: 'channel-success', MSG_CHANNEL_SUCCESS: 'channel-success',
MSG_CHANNEL_FAILURE: 'channel-failure' MSG_CHANNEL_FAILURE: 'channel-failure'
} }
# authentication request return codes: # authentication request return codes:
@ -126,43 +95,30 @@ CONNECTION_FAILED_CODE = {
DISCONNECT_SERVICE_NOT_AVAILABLE, DISCONNECT_AUTH_CANCELLED_BY_USER, \ DISCONNECT_SERVICE_NOT_AVAILABLE, DISCONNECT_AUTH_CANCELLED_BY_USER, \
DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE = 7, 13, 14 DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE = 7, 13, 14
zero_byte = byte_chr(0) from Crypto import Random
one_byte = byte_chr(1)
four_byte = byte_chr(4)
max_byte = byte_chr(0xff)
cr_byte = byte_chr(13)
linefeed_byte = byte_chr(10)
crlf = cr_byte + linefeed_byte
if PY2: # keep a crypto-strong PRNG nearby
cr_byte_value = cr_byte rng = Random.new()
linefeed_byte_value = linefeed_byte
import sys
if sys.version_info < (2, 3):
try:
import logging
except:
import logging22 as logging
import select
PY22 = True
import socket
if not hasattr(socket, 'timeout'):
class timeout(socket.error): pass
socket.timeout = timeout
del timeout
else: else:
cr_byte_value = 13 import logging
linefeed_byte_value = 10 PY22 = False
def asbytes(s):
if not isinstance(s, bytes_types):
if isinstance(s, string_types):
s = b(s)
else:
try:
s = s.asbytes()
except Exception:
raise Exception('Unknown type')
return s
xffffffff = long(0xffffffff)
x80000000 = long(0x80000000)
o666 = 438
o660 = 432
o644 = 420
o600 = 384
o777 = 511
o700 = 448
o70 = 56
DEBUG = logging.DEBUG DEBUG = logging.DEBUG
INFO = logging.INFO INFO = logging.INFO
WARNING = logging.WARNING WARNING = logging.WARNING

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.

View File

@ -8,7 +8,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -18,7 +18,7 @@
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
""" """
Configuration file (aka ``ssh_config``) support. L{SSHConfig}.
""" """
import fnmatch import fnmatch
@ -30,15 +30,60 @@ SSH_PORT = 22
proxy_re = re.compile(r"^(proxycommand)\s*=*\s*(.*)", re.I) proxy_re = re.compile(r"^(proxycommand)\s*=*\s*(.*)", re.I)
class LazyFqdn(object):
"""
Returns the host's fqdn on request as string.
"""
def __init__(self, config):
self.fqdn = None
self.config = config
def __str__(self):
if self.fqdn is None:
#
# If the SSH config contains AddressFamily, use that when
# determining the local host's FQDN. Using socket.getfqdn() from
# the standard library is the most general solution, but can
# result in noticeable delays on some platforms when IPv6 is
# misconfigured or not available, as it calls getaddrinfo with no
# address family specified, so both IPv4 and IPv6 are checked.
#
# Handle specific option
fqdn = None
address_family = self.config.get('addressfamily', 'any').lower()
if address_family != 'any':
family = socket.AF_INET if address_family == 'inet' \
else socket.AF_INET6
results = socket.getaddrinfo(host,
None,
family,
socket.SOCK_DGRAM,
socket.IPPROTO_IP,
socket.AI_CANONNAME)
for res in results:
af, socktype, proto, canonname, sa = res
if canonname and '.' in canonname:
fqdn = canonname
break
# Handle 'any' / unspecified
if fqdn is None:
fqdn = socket.getfqdn()
# Cache
self.fqdn = fqdn
return self.fqdn
class SSHConfig (object): class SSHConfig (object):
""" """
Representation of config information as stored in the format used by Representation of config information as stored in the format used by
OpenSSH. Queries can be made via `lookup`. The format is described in OpenSSH. Queries can be made via L{lookup}. The format is described in
OpenSSH's ``ssh_config`` man page. This class is provided primarily as a OpenSSH's C{ssh_config} man page. This class is provided primarily as a
convenience to posix users (since the OpenSSH format is a de-facto convenience to posix users (since the OpenSSH format is a de-facto
standard on posix) but should work fine on Windows too. standard on posix) but should work fine on Windows too.
.. versionadded:: 1.6 @since: 1.6
""" """
def __init__(self): def __init__(self):
@ -51,7 +96,8 @@ class SSHConfig (object):
""" """
Read an OpenSSH config from the given file object. Read an OpenSSH config from the given file object.
:param file file_obj: a file-like object to read the config file from @param file_obj: a file-like object to read the config file from
@type file_obj: file
""" """
host = {"host": ['*'], "config": {}} host = {"host": ['*'], "config": {}}
for line in file_obj: for line in file_obj:
@ -80,15 +126,14 @@ class SSHConfig (object):
self._config.append(host) self._config.append(host)
value = value.split() value = value.split()
host = {key: value, 'config': {}} host = {key: value, 'config': {}}
#identityfile, localforward, remoteforward keys are special cases, since they are allowed to be #identityfile is a special case, since it is allowed to be
# specified multiple times and they should be tried in order # specified multiple times and they should be tried in order
# of specification. # of specification.
elif key == 'identityfile':
elif key in ['identityfile', 'localforward', 'remoteforward']:
if key in host['config']: if key in host['config']:
host['config'][key].append(value) host['config']['identityfile'].append(value)
else: else:
host['config'][key] = [value] host['config']['identityfile'] = [value]
elif key not in host['config']: elif key not in host['config']:
host['config'].update({key: value}) host['config'].update({key: value})
self._config.append(host) self._config.append(host)
@ -97,26 +142,28 @@ class SSHConfig (object):
""" """
Return a dict of config options for a given hostname. Return a dict of config options for a given hostname.
The host-matching rules of OpenSSH's ``ssh_config`` man page are used, The host-matching rules of OpenSSH's C{ssh_config} man page are used,
which means that all configuration options from matching host which means that all configuration options from matching host
specifications are merged, with more specific hostmasks taking specifications are merged, with more specific hostmasks taking
precedence. In other words, if ``"Port"`` is set under ``"Host *"`` precedence. In other words, if C{"Port"} is set under C{"Host *"}
and also ``"Host *.example.com"``, and the lookup is for and also C{"Host *.example.com"}, and the lookup is for
``"ssh.example.com"``, then the port entry for ``"Host *.example.com"`` C{"ssh.example.com"}, then the port entry for C{"Host *.example.com"}
will win out. will win out.
The keys in the returned dict are all normalized to lowercase (look for The keys in the returned dict are all normalized to lowercase (look for
``"port"``, not ``"Port"``. The values are processed according to the C{"port"}, not C{"Port"}. The values are processed according to the
rules for substitution variable expansion in ``ssh_config``. rules for substitution variable expansion in C{ssh_config}.
:param str hostname: the hostname to lookup @param hostname: the hostname to lookup
@type hostname: str
""" """
matches = [config for config in self._config if matches = [config for config in self._config if
self._allowed(hostname, config['host'])] self._allowed(hostname, config['host'])]
ret = {} ret = {}
for match in matches: for match in matches:
for key, value in match['config'].items(): for key, value in match['config'].iteritems():
if key not in ret: if key not in ret:
# Create a copy of the original value, # Create a copy of the original value,
# else it will reference the original list # else it will reference the original list
@ -142,11 +189,13 @@ class SSHConfig (object):
Return a dict of config options with expanded substitutions Return a dict of config options with expanded substitutions
for a given hostname. for a given hostname.
Please refer to man ``ssh_config`` for the parameters that Please refer to man C{ssh_config} for the parameters that
are replaced. are replaced.
:param dict config: the config for the hostname @param config: the config for the hostname
:param str hostname: the hostname that the config belongs to @type hostname: dict
@param hostname: the hostname that the config belongs to
@type hostname: str
""" """
if 'hostname' in config: if 'hostname' in config:
@ -166,7 +215,7 @@ class SSHConfig (object):
remoteuser = user remoteuser = user
host = socket.gethostname().split('.')[0] host = socket.gethostname().split('.')[0]
fqdn = LazyFqdn(config, host) fqdn = LazyFqdn(config)
homedir = os.path.expanduser('~') homedir = os.path.expanduser('~')
replacements = {'controlpath': replacements = {'controlpath':
[ [
@ -205,57 +254,3 @@ class SSHConfig (object):
else: else:
config[k] = config[k].replace(find, str(replace)) config[k] = config[k].replace(find, str(replace))
return config return config
class LazyFqdn(object):
"""
Returns the host's fqdn on request as string.
"""
def __init__(self, config, host=None):
self.fqdn = None
self.config = config
self.host = host
def __str__(self):
if self.fqdn is None:
#
# If the SSH config contains AddressFamily, use that when
# determining the local host's FQDN. Using socket.getfqdn() from
# the standard library is the most general solution, but can
# result in noticeable delays on some platforms when IPv6 is
# misconfigured or not available, as it calls getaddrinfo with no
# address family specified, so both IPv4 and IPv6 are checked.
#
# Handle specific option
fqdn = None
address_family = self.config.get('addressfamily', 'any').lower()
if address_family != 'any':
try:
family = socket.AF_INET if address_family == 'inet' \
else socket.AF_INET6
results = socket.getaddrinfo(
self.host,
None,
family,
socket.SOCK_DGRAM,
socket.IPPROTO_IP,
socket.AI_CANONNAME
)
for res in results:
af, socktype, proto, canonname, sa = res
if canonname and '.' in canonname:
fqdn = canonname
break
# giaerror -> socket.getaddrinfo() can't resolve self.host
# (which is from socket.gethostname()). Fall back to the
# getfqdn() call below.
except socket.gaierror:
pass
# Handle 'any' / unspecified
if fqdn is None:
fqdn = socket.getfqdn()
# Cache
self.fqdn = fqdn
return self.fqdn

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -17,17 +17,18 @@
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
""" """
DSS keys. L{DSSKey}
""" """
import os import six
from hashlib import sha1 if six.PY3:
long = lambda x: int(x)
from Crypto.PublicKey import DSA from Crypto.PublicKey import DSA
from Crypto.Hash import SHA
from paramiko.common import *
from paramiko import util from paramiko import util
from paramiko.common import zero_byte
from paramiko.py3compat import long
from paramiko.ssh_exception import SSHException from paramiko.ssh_exception import SSHException
from paramiko.message import Message from paramiko.message import Message
from paramiko.ber import BER, BERException from paramiko.ber import BER, BERException
@ -59,7 +60,7 @@ class DSSKey (PKey):
else: else:
if msg is None: if msg is None:
raise SSHException('Key object may not be empty') raise SSHException('Key object may not be empty')
if msg.get_text() != 'ssh-dss': if msg.get_string() != 'ssh-dss':
raise SSHException('Invalid key') raise SSHException('Invalid key')
self.p = msg.get_mpint() self.p = msg.get_mpint()
self.q = msg.get_mpint() self.q = msg.get_mpint()
@ -67,17 +68,14 @@ class DSSKey (PKey):
self.y = msg.get_mpint() self.y = msg.get_mpint()
self.size = util.bit_length(self.p) self.size = util.bit_length(self.p)
def asbytes(self): def __str__(self):
m = Message() m = Message()
m.add_string('ssh-dss') m.add_string('ssh-dss')
m.add_mpint(self.p) m.add_mpint(self.p)
m.add_mpint(self.q) m.add_mpint(self.q)
m.add_mpint(self.g) m.add_mpint(self.g)
m.add_mpint(self.y) m.add_mpint(self.y)
return m.asbytes() return str(m)
def __str__(self):
return self.asbytes()
def __hash__(self): def __hash__(self):
h = hash(self.get_name()) h = hash(self.get_name())
@ -93,17 +91,17 @@ class DSSKey (PKey):
def get_bits(self): def get_bits(self):
return self.size return self.size
def can_sign(self): def can_sign(self):
return self.x is not None return self.x is not None
def sign_ssh_data(self, data): def sign_ssh_data(self, rng, data):
digest = sha1(data).digest() digest = SHA.new(data).digest()
dss = DSA.construct((long(self.y), long(self.g), long(self.p), long(self.q), long(self.x))) dss = DSA.construct((long(self.y), long(self.g), long(self.p), long(self.q), long(self.x)))
# generate a suitable k # generate a suitable k
qsize = len(util.deflate_long(self.q, 0)) qsize = len(util.deflate_long(self.q, 0))
while True: while True:
k = util.inflate_long(os.urandom(qsize), 1) k = util.inflate_long(rng.read(qsize), 1)
if (k > 2) and (k < self.q): if (k > 2) and (k < self.q):
break break
r, s = dss.sign(util.inflate_long(digest, 1), k) r, s = dss.sign(util.inflate_long(digest, 1), k)
@ -113,26 +111,26 @@ class DSSKey (PKey):
rstr = util.deflate_long(r, 0) rstr = util.deflate_long(r, 0)
sstr = util.deflate_long(s, 0) sstr = util.deflate_long(s, 0)
if len(rstr) < 20: if len(rstr) < 20:
rstr = zero_byte * (20 - len(rstr)) + rstr rstr = '\x00' * (20 - len(rstr)) + rstr
if len(sstr) < 20: if len(sstr) < 20:
sstr = zero_byte * (20 - len(sstr)) + sstr sstr = '\x00' * (20 - len(sstr)) + sstr
m.add_string(rstr + sstr) m.add_string(rstr + sstr)
return m return m
def verify_ssh_sig(self, data, msg): def verify_ssh_sig(self, data, msg):
if len(msg.asbytes()) == 40: if len(str(msg)) == 40:
# spies.com bug: signature has no header # spies.com bug: signature has no header
sig = msg.asbytes() sig = str(msg)
else: else:
kind = msg.get_text() kind = msg.get_string()
if kind != 'ssh-dss': if kind != 'ssh-dss':
return 0 return 0
sig = msg.get_binary() sig = msg.get_string()
# pull out (r, s) which are NOT encoded as mpints # pull out (r, s) which are NOT encoded as mpints
sigR = util.inflate_long(sig[:20], 1) sigR = util.inflate_long(sig[:20], 1)
sigS = util.inflate_long(sig[20:], 1) sigS = util.inflate_long(sig[20:], 1)
sigM = util.inflate_long(sha1(data).digest(), 1) sigM = util.inflate_long(SHA.new(data).digest(), 1)
dss = DSA.construct((long(self.y), long(self.g), long(self.p), long(self.q))) dss = DSA.construct((long(self.y), long(self.g), long(self.p), long(self.q)))
return dss.verify(sigM, (sigR, sigS)) return dss.verify(sigM, (sigR, sigS))
@ -140,13 +138,13 @@ class DSSKey (PKey):
def _encode_key(self): def _encode_key(self):
if self.x is None: if self.x is None:
raise SSHException('Not enough key information') raise SSHException('Not enough key information')
keylist = [0, self.p, self.q, self.g, self.y, self.x] keylist = [ 0, self.p, self.q, self.g, self.y, self.x ]
try: try:
b = BER() b = BER()
b.encode(keylist) b.encode(keylist)
except BERException: except BERException:
raise SSHException('Unable to create ber encoding of key') raise SSHException('Unable to create ber encoding of key')
return b.asbytes() return str(b)
def write_private_key_file(self, filename, password=None): def write_private_key_file(self, filename, password=None):
self._write_private_key_file('DSA', filename, self._encode_key(), password) self._write_private_key_file('DSA', filename, self._encode_key(), password)
@ -159,35 +157,39 @@ class DSSKey (PKey):
Generate a new private DSS key. This factory function can be used to Generate a new private DSS key. This factory function can be used to
generate a new host key or authentication key. generate a new host key or authentication key.
:param int bits: number of bits the generated key should be. @param bits: number of bits the generated key should be.
:param function progress_func: @type bits: int
an optional function to call at key points in key generation (used @param progress_func: an optional function to call at key points in
by ``pyCrypto.PublicKey``). key generation (used by C{pyCrypto.PublicKey}).
:return: new `.DSSKey` private key @type progress_func: function
@return: new private key
@rtype: L{DSSKey}
""" """
dsa = DSA.generate(bits, os.urandom, progress_func) dsa = DSA.generate(bits, rng.read, progress_func)
key = DSSKey(vals=(dsa.p, dsa.q, dsa.g, dsa.y)) key = DSSKey(vals=(dsa.p, dsa.q, dsa.g, dsa.y))
key.x = dsa.x key.x = dsa.x
return key return key
generate = staticmethod(generate) generate = staticmethod(generate)
### internals... ### internals...
def _from_private_key_file(self, filename, password): def _from_private_key_file(self, filename, password):
data = self._read_private_key_file('DSA', filename, password) data = self._read_private_key_file('DSA', filename, password)
self._decode_key(data) self._decode_key(data)
def _from_private_key(self, file_obj, password): def _from_private_key(self, file_obj, password):
data = self._read_private_key('DSA', file_obj, password) data = self._read_private_key('DSA', file_obj, password)
self._decode_key(data) self._decode_key(data)
def _decode_key(self, data): def _decode_key(self, data):
# private key file contains: # private key file contains:
# DSAPrivateKey = { version = 0, p, q, g, y, x } # DSAPrivateKey = { version = 0, p, q, g, y, x }
try: try:
keylist = BER(data).decode() keylist = BER(data).decode()
except BERException as e: except BERException as x:
raise SSHException('Unable to parse key file: ' + str(e)) raise SSHException('Unable to parse key file: ' + str(x))
if (type(keylist) is not list) or (len(keylist) < 6) or (keylist[0] != 0): if (type(keylist) is not list) or (len(keylist) < 6) or (keylist[0] != 0):
raise SSHException('not a valid DSA private key file (bad ber encoding)') raise SSHException('not a valid DSA private key file (bad ber encoding)')
self.p = keylist[1] self.p = keylist[1]

View File

@ -1,180 +0,0 @@
# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com>
#
# This file is part of paramiko.
#
# Paramiko is free software; you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free
# Software Foundation; either version 2.1 of the License, or (at your option)
# any later version.
#
# Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Paramiko; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
"""
L{ECDSAKey}
"""
import binascii
from hashlib import sha256
from ecdsa import SigningKey, VerifyingKey, der, curves
from ecdsa.test_pyecdsa import ECDSA
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
from paramiko.ssh_exception import SSHException
class ECDSAKey (PKey):
"""
Representation of an ECDSA key which can be used to sign and verify SSH2
data.
"""
def __init__(self, msg=None, data=None, filename=None, password=None, vals=None, file_obj=None):
self.verifying_key = None
self.signing_key = None
if file_obj is not None:
self._from_private_key(file_obj, password)
return
if filename is not None:
self._from_private_key_file(filename, password)
return
if (msg is None) and (data is not None):
msg = Message(data)
if vals is not None:
self.verifying_key, self.signing_key = vals
else:
if msg is None:
raise SSHException('Key object may not be empty')
if msg.get_text() != 'ecdsa-sha2-nistp256':
raise SSHException('Invalid key')
curvename = msg.get_text()
if curvename != 'nistp256':
raise SSHException("Can't handle curve of type %s" % curvename)
pointinfo = msg.get_binary()
if pointinfo[0:1] != four_byte:
raise SSHException('Point compression is being used: %s' %
binascii.hexlify(pointinfo))
self.verifying_key = VerifyingKey.from_string(pointinfo[1:],
curve=curves.NIST256p)
self.size = 256
def asbytes(self):
key = self.verifying_key
m = Message()
m.add_string('ecdsa-sha2-nistp256')
m.add_string('nistp256')
point_str = four_byte + key.to_string()
m.add_string(point_str)
return m.asbytes()
def __str__(self):
return self.asbytes()
def __hash__(self):
h = hash(self.get_name())
h = h * 37 + hash(self.verifying_key.pubkey.point.x())
h = h * 37 + hash(self.verifying_key.pubkey.point.y())
return hash(h)
def get_name(self):
return 'ecdsa-sha2-nistp256'
def get_bits(self):
return self.size
def can_sign(self):
return self.signing_key is not None
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)
return m
def verify_ssh_sig(self, data, msg):
if msg.get_text() != 'ecdsa-sha2-nistp256':
return False
sig = msg.get_binary()
# verify the signature by SHA'ing the data and encrypting it
# using the public key.
hash_obj = sha256(data).digest()
return self.verifying_key.verify_digest(sig, hash_obj,
sigdecode=self._sigdecode)
def write_private_key_file(self, filename, password=None):
key = self.signing_key or self.verifying_key
self._write_private_key_file('EC', filename, key.to_der(), password)
def write_private_key(self, file_obj, password=None):
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):
"""
Generate a new private RSA key. This factory function can be used to
generate a new host key or authentication key.
@param bits: number of bits the generated key should be.
@type bits: int
@param progress_func: an optional function to call at key points in
key generation (used by C{pyCrypto.PublicKey}).
@type progress_func: function
@return: new private key
@rtype: L{RSAKey}
"""
signing_key = ECDSA.generate()
key = ECDSAKey(vals=(signing_key, signing_key.get_verifying_key()))
return key
generate = staticmethod(generate)
### internals...
def _from_private_key_file(self, filename, password):
data = self._read_private_key_file('EC', filename, password)
self._decode_key(data)
def _from_private_key(self, file_obj, password):
data = self._read_private_key('EC', file_obj, password)
self._decode_key(data)
ALLOWED_PADDINGS = [one_byte, byte_chr(2) * 2, byte_chr(3) * 3, byte_chr(4) * 4,
byte_chr(5) * 5, byte_chr(6) * 6, byte_chr(7) * 7]
def _decode_key(self, data):
s, padding = der.remove_sequence(data)
if padding:
if padding not in self.ALLOWED_PADDINGS:
raise ValueError("weird padding: %s" % u(binascii.hexlify(data)))
data = data[:-len(padding)]
key = SigningKey.from_der(data)
self.signing_key = key
self.verifying_key = key.get_verifying_key()
self.size = 256
def _sigencode(self, r, s, order):
msg = Message()
msg.add_mpint(r)
msg.add_mpint(s)
return msg.asbytes()
def _sigdecode(self, sig, order):
msg = Message(sig)
r = msg.get_mpint()
s = msg.get_mpint()
return r, s

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -15,14 +15,20 @@
# You should have received a copy of the GNU Lesser General Public License # You should have received a copy of the GNU Lesser General Public License
# along with Paramiko; if not, write to the Free Software Foundation, Inc., # along with Paramiko; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
from paramiko.common import linefeed_byte_value, crlf, cr_byte, linefeed_byte, \
cr_byte_value """
from paramiko.py3compat import BytesIO, PY2, u, b, bytes_types BufferedFile.
"""
try:
from cStringIO import StringIO
except ImportError:
from io import StringIO
class BufferedFile (object): class BufferedFile (object):
""" """
Reusable base class to implement Python-style file buffering around a Reusable base class to implement python-style file buffering around a
simpler stream. simpler stream.
""" """
@ -44,8 +50,8 @@ class BufferedFile (object):
self.newlines = None self.newlines = None
self._flags = 0 self._flags = 0
self._bufsize = self._DEFAULT_BUFSIZE self._bufsize = self._DEFAULT_BUFSIZE
self._wbuffer = BytesIO() self._wbuffer = StringIO()
self._rbuffer = bytes() self._rbuffer = ''
self._at_trailing_cr = False self._at_trailing_cr = False
self._closed = False self._closed = False
# pos - position within the file, according to the user # pos - position within the file, according to the user
@ -64,7 +70,10 @@ class BufferedFile (object):
file. This iterator happens to return the file itself, since a file is file. This iterator happens to return the file itself, since a file is
its own iterator. its own iterator.
:raises ValueError: if the file is closed. @raise ValueError: if the file is closed.
@return: an interator.
@rtype: iterator
""" """
if self._closed: if self._closed:
raise ValueError('I/O operation on closed file') raise ValueError('I/O operation on closed file')
@ -83,57 +92,36 @@ class BufferedFile (object):
buffering is not turned on. buffering is not turned on.
""" """
self._write_all(self._wbuffer.getvalue()) self._write_all(self._wbuffer.getvalue())
self._wbuffer = BytesIO() self._wbuffer = StringIO()
return return
if PY2: def next(self):
def next(self): """
""" Returns the next line from the input, or raises L{StopIteration} when
Returns the next line from the input, or raises EOF is hit. Unlike python file objects, it's okay to mix calls to
`~exceptions.StopIteration` when EOF is hit. Unlike Python file C{next} and L{readline}.
objects, it's okay to mix calls to `next` and `readline`.
:raises StopIteration: when the end of the file is reached. @raise StopIteration: when the end of the file is reached.
:return: a line (`str`) read from the file. @return: a line read from the file.
""" @rtype: str
line = self.readline() """
if not line: line = self.readline()
raise StopIteration if not line:
return line raise StopIteration
else: return line
def __next__(self):
"""
Returns the next line from the input, or raises L{StopIteration} when
EOF is hit. Unlike python file objects, it's okay to mix calls to
C{next} and L{readline}.
@raise StopIteration: when the end of the file is reached.
@return: a line read from the file.
@rtype: str
"""
line = self.readline()
if not line:
raise StopIteration
return line
def read(self, size=None): def read(self, size=None):
""" """
Read at most ``size`` bytes from the file (less if we hit the end of the Read at most C{size} bytes from the file (less if we hit the end of the
file first). If the ``size`` argument is negative or omitted, read all file first). If the C{size} argument is negative or omitted, read all
the remaining data in the file. the remaining data in the file.
.. note:: @param size: maximum number of bytes to read
``'b'`` mode flag is ignored (``self.FLAG_BINARY`` in @type size: int
``self._flags``), because SSH treats all files as binary, since we @return: data read from the file, or an empty string if EOF was
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 bytes), or an empty string if EOF was
encountered immediately encountered immediately
@rtype: str
""" """
if self._closed: if self._closed:
raise IOError('File is closed') raise IOError('File is closed')
@ -142,7 +130,7 @@ class BufferedFile (object):
if (size is None) or (size < 0): if (size is None) or (size < 0):
# go for broke # go for broke
result = self._rbuffer result = self._rbuffer
self._rbuffer = bytes() self._rbuffer = ''
self._pos += len(result) self._pos += len(result)
while True: while True:
try: try:
@ -154,12 +142,12 @@ class BufferedFile (object):
result += new_data result += new_data
self._realpos += len(new_data) self._realpos += len(new_data)
self._pos += len(new_data) self._pos += len(new_data)
return result return result
if size <= len(self._rbuffer): if size <= len(self._rbuffer):
result = self._rbuffer[:size] result = self._rbuffer[:size]
self._rbuffer = self._rbuffer[size:] self._rbuffer = self._rbuffer[size:]
self._pos += len(result) self._pos += len(result)
return result return result
while len(self._rbuffer) < size: while len(self._rbuffer) < size:
read_size = size - len(self._rbuffer) read_size = size - len(self._rbuffer)
if self._flags & self.FLAG_BUFFERED: if self._flags & self.FLAG_BUFFERED:
@ -175,7 +163,7 @@ class BufferedFile (object):
result = self._rbuffer[:size] result = self._rbuffer[:size]
self._rbuffer = self._rbuffer[size:] self._rbuffer = self._rbuffer[size:]
self._pos += len(result) self._pos += len(result)
return result return result
def readline(self, size=None): def readline(self, size=None):
""" """
@ -186,18 +174,14 @@ class BufferedFile (object):
incomplete line may be returned. An empty string is returned only when incomplete line may be returned. An empty string is returned only when
EOF is encountered immediately. EOF is encountered immediately.
.. note:: @note: Unlike stdio's C{fgets()}, the returned string contains null
Unlike stdio's ``fgets``, the returned string contains null characters (C{'\\0'}) if they occurred in the input.
characters (``'\\0'``) if they occurred in the input.
:param int size: maximum length of returned string. @param size: maximum length of returned string.
:return: @type size: int
next line of the file, or an empty string if the end of the @return: next line of the file, or an empty string if the end of the
file has been reached. file has been reached.
@rtype: str
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. # it's almost silly how complex this function is.
if self._closed: if self._closed:
@ -209,11 +193,11 @@ class BufferedFile (object):
if self._at_trailing_cr and (self._flags & self.FLAG_UNIVERSAL_NEWLINE) and (len(line) > 0): if self._at_trailing_cr and (self._flags & self.FLAG_UNIVERSAL_NEWLINE) and (len(line) > 0):
# edge case: the newline may be '\r\n' and we may have read # edge case: the newline may be '\r\n' and we may have read
# only the first '\r' last time. # only the first '\r' last time.
if line[0] == linefeed_byte_value: if line[0] == '\n':
line = line[1:] line = line[1:]
self._record_newline(crlf) self._record_newline('\r\n')
else: else:
self._record_newline(cr_byte) self._record_newline('\r')
self._at_trailing_cr = False self._at_trailing_cr = False
# check size before looking for a linefeed, in case we already have # check size before looking for a linefeed, in case we already have
# enough. # enough.
@ -223,82 +207,84 @@ class BufferedFile (object):
self._rbuffer = line[size:] self._rbuffer = line[size:]
line = line[:size] line = line[:size]
self._pos += len(line) self._pos += len(line)
return line if self._flags & self.FLAG_BINARY else u(line) return line
n = size - len(line) n = size - len(line)
else: else:
n = self._bufsize n = self._bufsize
if (linefeed_byte in line) or ((self._flags & self.FLAG_UNIVERSAL_NEWLINE) and (cr_byte in line)): if ('\n' in line) or ((self._flags & self.FLAG_UNIVERSAL_NEWLINE) and ('\r' in line)):
break break
try: try:
new_data = self._read(n) new_data = self._read(n)
except EOFError: except EOFError:
new_data = None new_data = None
if (new_data is None) or (len(new_data) == 0): if (new_data is None) or (len(new_data) == 0):
self._rbuffer = bytes() self._rbuffer = ''
self._pos += len(line) self._pos += len(line)
return line if self._flags & self.FLAG_BINARY else u(line) return line
line += new_data line += new_data
self._realpos += len(new_data) self._realpos += len(new_data)
# find the newline # find the newline
pos = line.find(linefeed_byte) pos = line.find('\n')
if self._flags & self.FLAG_UNIVERSAL_NEWLINE: if self._flags & self.FLAG_UNIVERSAL_NEWLINE:
rpos = line.find(cr_byte) rpos = line.find('\r')
if (rpos >= 0) and (rpos < pos or pos < 0): if (rpos >= 0) and ((rpos < pos) or (pos < 0)):
pos = rpos pos = rpos
xpos = pos + 1 xpos = pos + 1
if (line[pos] == cr_byte_value) and (xpos < len(line)) and (line[xpos] == linefeed_byte_value): if (line[pos] == '\r') and (xpos < len(line)) and (line[xpos] == '\n'):
xpos += 1 xpos += 1
self._rbuffer = line[xpos:] self._rbuffer = line[xpos:]
lf = line[pos:xpos] lf = line[pos:xpos]
line = line[:pos] + linefeed_byte line = line[:pos] + '\n'
if (len(self._rbuffer) == 0) and (lf == cr_byte): if (len(self._rbuffer) == 0) and (lf == '\r'):
# we could read the line up to a '\r' and there could still be a # we could read the line up to a '\r' and there could still be a
# '\n' following that we read next time. note that and eat it. # '\n' following that we read next time. note that and eat it.
self._at_trailing_cr = True self._at_trailing_cr = True
else: else:
self._record_newline(lf) self._record_newline(lf)
self._pos += len(line) self._pos += len(line)
return line if self._flags & self.FLAG_BINARY else u(line) return line
def readlines(self, sizehint=None): def readlines(self, sizehint=None):
""" """
Read all remaining lines using `readline` and return them as a list. Read all remaining lines using L{readline} and return them as a list.
If the optional ``sizehint`` argument is present, instead of reading up If the optional C{sizehint} argument is present, instead of reading up
to EOF, whole lines totalling approximately sizehint bytes (possibly to EOF, whole lines totalling approximately sizehint bytes (possibly
after rounding up to an internal buffer size) are read. after rounding up to an internal buffer size) are read.
:param int sizehint: desired maximum number of bytes to read. @param sizehint: desired maximum number of bytes to read.
:return: `list` of lines read from the file. @type sizehint: int
@return: list of lines read from the file.
@rtype: list
""" """
lines = [] lines = []
byte_count = 0 bytes = 0
while True: while True:
line = self.readline() line = self.readline()
if len(line) == 0: if len(line) == 0:
break break
lines.append(line) lines.append(line)
byte_count += len(line) bytes += len(line)
if (sizehint is not None) and (byte_count >= sizehint): if (sizehint is not None) and (bytes >= sizehint):
break break
return lines return lines
def seek(self, offset, whence=0): def seek(self, offset, whence=0):
""" """
Set the file's current position, like stdio's ``fseek``. Not all file Set the file's current position, like stdio's C{fseek}. Not all file
objects support seeking. objects support seeking.
.. note:: @note: If a file is opened in append mode (C{'a'} or C{'a+'}), any seek
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 operations will be undone at the next write (as the file position
will move back to the end of the file). will move back to the end of the file).
:param int offset: @param offset: position to move to within the file, relative to
position to move to within the file, relative to ``whence``. C{whence}.
:param int whence: @type offset: int
type of movement: 0 = absolute; 1 = relative to the current @param whence: type of movement: 0 = absolute; 1 = relative to the
position; 2 = relative to the end of the file. current position; 2 = relative to the end of the file.
@type whence: int
:raises IOError: if the file doesn't support random access. @raise IOError: if the file doesn't support random access.
""" """
raise IOError('File does not support seeking.') raise IOError('File does not support seeking.')
@ -308,20 +294,21 @@ class BufferedFile (object):
useful if the underlying file doesn't support random access, or was useful if the underlying file doesn't support random access, or was
opened in append mode. opened in append mode.
:return: file position (`number <int>` of bytes). @return: file position (in bytes).
@rtype: int
""" """
return self._pos return self._pos
def write(self, data): def write(self, data):
""" """
Write data to the file. If write buffering is on (``bufsize`` was Write data to the file. If write buffering is on (C{bufsize} was
specified and non-zero), some or all of the data may not actually be specified and non-zero), some or all of the data may not actually be
written yet. (Use `flush` or `close` to force buffered data to be written yet. (Use L{flush} or L{close} to force buffered data to be
written out.) written out.)
:param str data: data to write @param data: data to write.
@type data: str
""" """
data = b(data)
if self._closed: if self._closed:
raise IOError('File is closed') raise IOError('File is closed')
if not (self._flags & self.FLAG_WRITE): if not (self._flags & self.FLAG_WRITE):
@ -332,12 +319,12 @@ class BufferedFile (object):
self._wbuffer.write(data) self._wbuffer.write(data)
if self._flags & self.FLAG_LINE_BUFFERED: if self._flags & self.FLAG_LINE_BUFFERED:
# only scan the new data for linefeed, to avoid wasting time. # only scan the new data for linefeed, to avoid wasting time.
last_newline_pos = data.rfind(linefeed_byte) last_newline_pos = data.rfind('\n')
if last_newline_pos >= 0: if last_newline_pos >= 0:
wbuf = self._wbuffer.getvalue() wbuf = self._wbuffer.getvalue()
last_newline_pos += len(wbuf) - len(data) last_newline_pos += len(wbuf) - len(data)
self._write_all(wbuf[:last_newline_pos + 1]) self._write_all(wbuf[:last_newline_pos + 1])
self._wbuffer = BytesIO() self._wbuffer = StringIO()
self._wbuffer.write(wbuf[last_newline_pos + 1:]) self._wbuffer.write(wbuf[last_newline_pos + 1:])
return return
# even if we're line buffering, if the buffer has grown past the # even if we're line buffering, if the buffer has grown past the
@ -350,10 +337,11 @@ class BufferedFile (object):
""" """
Write a sequence of strings to the file. The sequence can be any Write a sequence of strings to the file. The sequence can be any
iterable object producing strings, typically a list of strings. (The iterable object producing strings, typically a list of strings. (The
name is intended to match `readlines`; `writelines` does not add line name is intended to match L{readlines}; C{writelines} does not add line
separators.) separators.)
:param iterable sequence: an iterable sequence of strings. @param sequence: an iterable sequence of strings.
@type sequence: sequence
""" """
for line in sequence: for line in sequence:
self.write(line) self.write(line)
@ -361,8 +349,11 @@ class BufferedFile (object):
def xreadlines(self): def xreadlines(self):
""" """
Identical to ``iter(f)``. This is a deprecated file interface that Identical to C{iter(f)}. This is a deprecated file interface that
predates Python iterator support. predates python iterator support.
@return: an iterator.
@rtype: iterator
""" """
return self return self
@ -370,36 +361,40 @@ class BufferedFile (object):
def closed(self): def closed(self):
return self._closed return self._closed
### overrides... ### overrides...
def _read(self, size): def _read(self, size):
""" """
(subclass override) I{(subclass override)}
Read data from the stream. Return ``None`` or raise ``EOFError`` to Read data from the stream. Return C{None} or raise C{EOFError} to
indicate EOF. indicate EOF.
""" """
raise EOFError() raise EOFError()
def _write(self, data): def _write(self, data):
""" """
(subclass override) I{(subclass override)}
Write data into the stream. Write data into the stream.
""" """
raise IOError('write not implemented') raise IOError('write not implemented')
def _get_size(self): def _get_size(self):
""" """
(subclass override) I{(subclass override)}
Return the size of the file. This is called from within `_set_mode` Return the size of the file. This is called from within L{_set_mode}
if the file is opened in append mode, so the file position can be if the file is opened in append mode, so the file position can be
tracked and `seek` and `tell` will work correctly. If the file is tracked and L{seek} and L{tell} will work correctly. If the file is
a stream that can't be randomly accessed, you don't need to override a stream that can't be randomly accessed, you don't need to override
this method, this method,
""" """
return 0 return 0
### internals... ### internals...
def _set_mode(self, mode='r', bufsize=-1): def _set_mode(self, mode='r', bufsize=-1):
""" """
Subclasses call this method to initialize the BufferedFile. Subclasses call this method to initialize the BufferedFile.
@ -427,13 +422,13 @@ class BufferedFile (object):
self._flags |= self.FLAG_READ self._flags |= self.FLAG_READ
if ('w' in mode) or ('+' in mode): if ('w' in mode) or ('+' in mode):
self._flags |= self.FLAG_WRITE self._flags |= self.FLAG_WRITE
if 'a' in mode: if ('a' in mode):
self._flags |= self.FLAG_WRITE | self.FLAG_APPEND self._flags |= self.FLAG_WRITE | self.FLAG_APPEND
self._size = self._get_size() self._size = self._get_size()
self._pos = self._realpos = self._size self._pos = self._realpos = self._size
if 'b' in mode: if ('b' in mode):
self._flags |= self.FLAG_BINARY self._flags |= self.FLAG_BINARY
if 'U' in mode: if ('U' in mode):
self._flags |= self.FLAG_UNIVERSAL_NEWLINE self._flags |= self.FLAG_UNIVERSAL_NEWLINE
# built-in file objects have this attribute to store which kinds of # built-in file objects have this attribute to store which kinds of
# line terminations they've seen: # line terminations they've seen:
@ -462,7 +457,7 @@ class BufferedFile (object):
return return
if self.newlines is None: if self.newlines is None:
self.newlines = newline self.newlines = newline
elif self.newlines != newline and isinstance(self.newlines, bytes_types): elif (type(self.newlines) is str) and (self.newlines != newline):
self.newlines = (self.newlines, newline) self.newlines = (self.newlines, newline)
elif newline not in self.newlines: elif newline not in self.newlines:
self.newlines += (newline,) self.newlines += (newline,)

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -16,45 +16,117 @@
# along with Paramiko; if not, write to the Free Software Foundation, Inc., # along with Paramiko; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
"""
L{HostKeys}
"""
import base64
import binascii import binascii
import os from Crypto.Hash import SHA, HMAC
from hashlib import sha1
from hmac import HMAC
from paramiko.py3compat import b, u, encodebytes, decodebytes
try: try:
from collections import MutableMapping from UserDict import DictMixin
except ImportError: except ImportError:
# noinspection PyUnresolvedReferences from collections import MutableMapping as DictMixin
from UserDict import DictMixin as MutableMapping
from paramiko.common import *
from paramiko.dsskey import DSSKey from paramiko.dsskey import DSSKey
from paramiko.rsakey import RSAKey from paramiko.rsakey import RSAKey
from paramiko.util import get_logger, constant_time_bytes_eq from paramiko.util import get_logger
from paramiko.ecdsakey import ECDSAKey
class HostKeys (MutableMapping): class InvalidHostKey(Exception):
def __init__(self, line, exc):
self.line = line
self.exc = exc
self.args = (line, exc)
class HostKeyEntry:
""" """
Representation of an OpenSSH-style "known hosts" file. Host keys can be Representation of a line in an OpenSSH-style "known hosts" file.
"""
def __init__(self, hostnames=None, key=None):
self.valid = (hostnames is not None) and (key is not None)
self.hostnames = hostnames
self.key = key
def from_line(cls, line, lineno=None):
"""
Parses the given line of text to find the names for the host,
the type of key, and the key data. The line is expected to be in the
format used by the openssh known_hosts file.
Lines are expected to not have leading or trailing whitespace.
We don't bother to check for comments or empty lines. All of
that should be taken care of before sending the line to us.
@param line: a line from an OpenSSH known_hosts file
@type line: str
"""
log = get_logger('paramiko.hostkeys')
fields = line.split(' ')
if len(fields) < 3:
# Bad number of fields
log.info("Not enough fields found in known_hosts in line %s (%r)" %
(lineno, line))
return None
fields = fields[:3]
names, keytype, key = fields
names = names.split(',')
# Decide what kind of key we're looking at and create an object
# to hold it accordingly.
try:
if keytype == 'ssh-rsa':
key = RSAKey(data=base64.decodestring(key))
elif keytype == 'ssh-dss':
key = DSSKey(data=base64.decodestring(key))
else:
log.info("Unable to handle key of type %s" % (keytype,))
return None
except binascii.Error as e:
raise InvalidHostKey(line, e)
return cls(names, key)
from_line = classmethod(from_line)
def to_line(self):
"""
Returns a string in OpenSSH known_hosts file format, or None if
the object is not in a valid state. A trailing newline is
included.
"""
if self.valid:
return '%s %s %s\n' % (','.join(self.hostnames), self.key.get_name(),
self.key.get_base64())
return None
def __repr__(self):
return '<HostKeyEntry %r: %r>' % (self.hostnames, self.key)
class HostKeys (DictMixin):
"""
Representation of an openssh-style "known hosts" file. Host keys can be
read from one or more files, and then individual hosts can be looked up to read from one or more files, and then individual hosts can be looked up to
verify server keys during SSH negotiation. verify server keys during SSH negotiation.
A `.HostKeys` object can be treated like a dict; any dict lookup is A HostKeys object can be treated like a dict; any dict lookup is equivalent
equivalent to calling `lookup`. to calling L{lookup}.
.. versionadded:: 1.5.3 @since: 1.5.3
""" """
def __init__(self, filename=None): def __init__(self, filename=None):
""" """
Create a new HostKeys object, optionally loading keys from an OpenSSH Create a new HostKeys object, optionally loading keys from an openssh
style host-key file. style host-key file.
:param str filename: filename to load host keys from, or ``None`` @param filename: filename to load host keys from, or C{None}
@type filename: str
""" """
# emulate a dict of { hostname: { keytype: PKey } } # emulate a dict of { hostname: { keytype: PKey } }
self._entries = [] self._entries = []
@ -64,11 +136,14 @@ class HostKeys (MutableMapping):
def add(self, hostname, keytype, key): def add(self, hostname, keytype, key):
""" """
Add a host key entry to the table. Any existing entry for a Add a host key entry to the table. Any existing entry for a
``(hostname, keytype)`` pair will be replaced. C{(hostname, keytype)} pair will be replaced.
:param str hostname: the hostname (or IP) to add @param hostname: the hostname (or IP) to add
:param str keytype: key type (``"ssh-rsa"`` or ``"ssh-dss"``) @type hostname: str
:param .PKey key: the key to add @param keytype: key type (C{"ssh-rsa"} or C{"ssh-dss"})
@type keytype: str
@param key: the key to add
@type key: L{PKey}
""" """
for e in self._entries: for e in self._entries:
if (hostname in e.hostnames) and (e.key.get_name() == keytype): if (hostname in e.hostnames) and (e.key.get_name() == keytype):
@ -78,81 +153,73 @@ class HostKeys (MutableMapping):
def load(self, filename): def load(self, filename):
""" """
Read a file of known SSH host keys, in the format used by OpenSSH. Read a file of known SSH host keys, in the format used by openssh.
This type of file unfortunately doesn't exist on Windows, but on This type of file unfortunately doesn't exist on Windows, but on
posix, it will usually be stored in posix, it will usually be stored in
``os.path.expanduser("~/.ssh/known_hosts")``. C{os.path.expanduser("~/.ssh/known_hosts")}.
If this method is called multiple times, the host keys are merged, If this method is called multiple times, the host keys are merged,
not cleared. So multiple calls to `load` will just call `add`, not cleared. So multiple calls to C{load} will just call L{add},
replacing any existing entries and adding new ones. replacing any existing entries and adding new ones.
:param str filename: name of the file to read host keys from @param filename: name of the file to read host keys from
@type filename: str
:raises IOError: if there was an error reading the file @raise IOError: if there was an error reading the file
""" """
with open(filename, 'r') as f: f = open(filename, 'r')
for lineno, line in enumerate(f): for lineno, line in enumerate(f):
line = line.strip() line = line.strip()
if (len(line) == 0) or (line[0] == '#'): if (len(line) == 0) or (line[0] == '#'):
continue continue
e = HostKeyEntry.from_line(line, lineno) e = HostKeyEntry.from_line(line, lineno)
if e is not None: if e is not None:
_hostnames = e.hostnames _hostnames = e.hostnames
for h in _hostnames: for h in _hostnames:
if self.check(h, e.key): if self.check(h, e.key):
e.hostnames.remove(h) e.hostnames.remove(h)
if len(e.hostnames): if len(e.hostnames):
self._entries.append(e) self._entries.append(e)
f.close()
def save(self, filename): def save(self, filename):
""" """
Save host keys into a file, in the format used by OpenSSH. The order of Save host keys into a file, in the format used by openssh. The order of
keys in the file will be preserved when possible (if these keys were keys in the file will be preserved when possible (if these keys were
loaded from a file originally). The single exception is that combined loaded from a file originally). The single exception is that combined
lines will be split into individual key lines, which is arguably a bug. lines will be split into individual key lines, which is arguably a bug.
:param str filename: name of the file to write @param filename: name of the file to write
@type filename: str
:raises IOError: if there was an error writing the file @raise IOError: if there was an error writing the file
.. versionadded:: 1.6.1 @since: 1.6.1
""" """
with open(filename, 'w') as f: f = open(filename, 'w')
for e in self._entries: for e in self._entries:
line = e.to_line() line = e.to_line()
if line: if line:
f.write(line) f.write(line)
f.close()
def lookup(self, hostname): def lookup(self, hostname):
""" """
Find a hostkey entry for a given hostname or IP. If no entry is found, Find a hostkey entry for a given hostname or IP. If no entry is found,
``None`` is returned. Otherwise a dictionary of keytype to key is C{None} is returned. Otherwise a dictionary of keytype to key is
returned. The keytype will be either ``"ssh-rsa"`` or ``"ssh-dss"``. returned. The keytype will be either C{"ssh-rsa"} or C{"ssh-dss"}.
:param str hostname: the hostname (or IP) to lookup @param hostname: the hostname (or IP) to lookup
:return: dict of `str` -> `.PKey` keys associated with this host (or ``None``) @type hostname: str
@return: keys associated with this host (or C{None})
@rtype: dict(str, L{PKey})
""" """
class SubDict (MutableMapping): class SubDict (DictMixin):
def __init__(self, hostname, entries, hostkeys): def __init__(self, hostname, entries, hostkeys):
self._hostname = hostname self._hostname = hostname
self._entries = entries self._entries = entries
self._hostkeys = hostkeys self._hostkeys = hostkeys
def __iter__(self):
for k in self.keys():
yield k
def __len__(self):
return len(self.keys())
def __delitem__(self, key):
for e in list(self._entries):
if e.key.get_name() == key:
self._entries.remove(e)
else:
raise KeyError(key)
def __getitem__(self, key): def __getitem__(self, key):
for e in self._entries: for e in self._entries:
if e.key.get_name() == key: if e.key.get_name() == key:
@ -179,7 +246,7 @@ class HostKeys (MutableMapping):
entries = [] entries = []
for e in self._entries: for e in self._entries:
for h in e.hostnames: 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 (self.hash_host(hostname, h) == h)) or (h == hostname):
entries.append(e) entries.append(e)
if len(entries) == 0: if len(entries) == 0:
return None return None
@ -190,10 +257,13 @@ class HostKeys (MutableMapping):
Return True if the given key is associated with the given hostname Return True if the given key is associated with the given hostname
in this dictionary. in this dictionary.
:param str hostname: hostname (or IP) of the SSH server @param hostname: hostname (or IP) of the SSH server
:param .PKey key: the key to check @type hostname: str
:return: @param key: the key to check
``True`` if the key is associated with the hostname; else ``False`` @type key: L{PKey}
@return: C{True} if the key is associated with the hostname; C{False}
if not
@rtype: bool
""" """
k = self.lookup(hostname) k = self.lookup(hostname)
if k is None: if k is None:
@ -201,7 +271,7 @@ class HostKeys (MutableMapping):
host_key = k.get(key.get_name(), None) host_key = k.get(key.get_name(), None)
if host_key is None: if host_key is None:
return False return False
return host_key.asbytes() == key.asbytes() return str(host_key) == str(key)
def clear(self): def clear(self):
""" """
@ -209,16 +279,6 @@ class HostKeys (MutableMapping):
""" """
self._entries = [] self._entries = []
def __iter__(self):
for k in self.keys():
yield k
def __len__(self):
return len(self.keys())
def __delitem__(self, key):
k = self[key]
def __getitem__(self, key): def __getitem__(self, key):
ret = self.lookup(key) ret = self.lookup(key)
if ret is None: if ret is None:
@ -241,7 +301,7 @@ class HostKeys (MutableMapping):
self._entries.append(HostKeyEntry([hostname], entry[key_type])) self._entries.append(HostKeyEntry([hostname], entry[key_type]))
def keys(self): def keys(self):
# Python 2.4 sets would be nice here. # python 2.4 sets would be nice here.
ret = [] ret = []
for e in self._entries: for e in self._entries:
for h in e.hostnames: for h in e.hostnames:
@ -257,97 +317,25 @@ class HostKeys (MutableMapping):
def hash_host(hostname, salt=None): def hash_host(hostname, salt=None):
""" """
Return a "hashed" form of the hostname, as used by OpenSSH when storing Return a "hashed" form of the hostname, as used by openssh when storing
hashed hostnames in the known_hosts file. hashed hostnames in the known_hosts file.
:param str hostname: the hostname to hash @param hostname: the hostname to hash
:param str salt: optional salt to use when hashing (must be 20 bytes long) @type hostname: str
:return: the hashed hostname as a `str` @param salt: optional salt to use when hashing (must be 20 bytes long)
@type salt: str
@return: the hashed hostname
@rtype: str
""" """
if salt is None: if salt is None:
salt = os.urandom(sha1().digest_size) salt = rng.read(SHA.digest_size)
else: else:
if salt.startswith('|1|'): if salt.startswith('|1|'):
salt = salt.split('|')[2] salt = salt.split('|')[2]
salt = decodebytes(b(salt)) salt = base64.decodestring(salt)
assert len(salt) == sha1().digest_size assert len(salt) == SHA.digest_size
hmac = HMAC(salt, b(hostname), sha1).digest() hmac = HMAC.HMAC(salt, hostname, SHA).digest()
hostkey = '|1|%s|%s' % (u(encodebytes(salt)), u(encodebytes(hmac))) hostkey = '|1|%s|%s' % (base64.encodestring(salt), base64.encodestring(hmac))
return hostkey.replace('\n', '') return hostkey.replace('\n', '')
hash_host = staticmethod(hash_host) hash_host = staticmethod(hash_host)
class InvalidHostKey(Exception):
def __init__(self, line, exc):
self.line = line
self.exc = exc
self.args = (line, exc)
class HostKeyEntry:
"""
Representation of a line in an OpenSSH-style "known hosts" file.
"""
def __init__(self, hostnames=None, key=None):
self.valid = (hostnames is not None) and (key is not None)
self.hostnames = hostnames
self.key = key
def from_line(cls, line, lineno=None):
"""
Parses the given line of text to find the names for the host,
the type of key, and the key data. The line is expected to be in the
format used by the OpenSSH known_hosts file.
Lines are expected to not have leading or trailing whitespace.
We don't bother to check for comments or empty lines. All of
that should be taken care of before sending the line to us.
:param str line: a line from an OpenSSH known_hosts file
"""
log = get_logger('paramiko.hostkeys')
fields = line.split(' ')
if len(fields) < 3:
# Bad number of fields
log.info("Not enough fields found in known_hosts in line %s (%r)" %
(lineno, line))
return None
fields = fields[:3]
names, keytype, key = fields
names = names.split(',')
# Decide what kind of key we're looking at and create an object
# to hold it accordingly.
try:
key = b(key)
if keytype == 'ssh-rsa':
key = RSAKey(data=decodebytes(key))
elif keytype == 'ssh-dss':
key = DSSKey(data=decodebytes(key))
elif keytype == 'ecdsa-sha2-nistp256':
key = ECDSAKey(data=decodebytes(key))
else:
log.info("Unable to handle key of type %s" % (keytype,))
return None
except binascii.Error as e:
raise InvalidHostKey(line, e)
return cls(names, key)
from_line = classmethod(from_line)
def to_line(self):
"""
Returns a string in OpenSSH known_hosts file format, or None if
the object is not in a valid state. A trailing newline is
included.
"""
if self.valid:
return '%s %s %s\n' % (','.join(self.hostnames), self.key.get_name(),
self.key.get_base64())
return None
def __repr__(self):
return '<HostKeyEntry %r: %r>' % (self.hostnames, self.key)

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -17,25 +17,22 @@
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
""" """
Variant on `KexGroup1 <paramiko.kex_group1.KexGroup1>` where the prime "p" and Variant on L{KexGroup1 <paramiko.kex_group1.KexGroup1>} where the prime "p" and
generator "g" are provided by the server. A bit more work is required on the 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. client side, and a B{lot} more on the server side.
""" """
import os from Crypto.Hash import SHA
from hashlib import sha1 from Crypto.Util import number
from paramiko.common import *
from paramiko import util from paramiko import util
from paramiko.common import DEBUG
from paramiko.message import Message from paramiko.message import Message
from paramiko.py3compat import byte_chr, byte_ord, byte_mask
from paramiko.ssh_exception import SSHException from paramiko.ssh_exception import SSHException
_MSG_KEXDH_GEX_REQUEST_OLD, _MSG_KEXDH_GEX_GROUP, _MSG_KEXDH_GEX_INIT, \ _MSG_KEXDH_GEX_REQUEST_OLD, _MSG_KEXDH_GEX_GROUP, _MSG_KEXDH_GEX_INIT, \
_MSG_KEXDH_GEX_REPLY, _MSG_KEXDH_GEX_REQUEST = range(30, 35) _MSG_KEXDH_GEX_REPLY, _MSG_KEXDH_GEX_REQUEST = range(30, 35)
c_MSG_KEXDH_GEX_REQUEST_OLD, c_MSG_KEXDH_GEX_GROUP, c_MSG_KEXDH_GEX_INIT, \
c_MSG_KEXDH_GEX_REPLY, c_MSG_KEXDH_GEX_REQUEST = [byte_chr(c) for c in range(30, 35)]
class KexGex (object): class KexGex (object):
@ -65,11 +62,11 @@ class KexGex (object):
m = Message() m = Message()
if _test_old_style: if _test_old_style:
# only used for unit tests: we shouldn't ever send this # only used for unit tests: we shouldn't ever send this
m.add_byte(c_MSG_KEXDH_GEX_REQUEST_OLD) m.add_byte(chr(_MSG_KEXDH_GEX_REQUEST_OLD))
m.add_int(self.preferred_bits) m.add_int(self.preferred_bits)
self.old_style = True self.old_style = True
else: else:
m.add_byte(c_MSG_KEXDH_GEX_REQUEST) m.add_byte(chr(_MSG_KEXDH_GEX_REQUEST))
m.add_int(self.min_bits) m.add_int(self.min_bits)
m.add_int(self.preferred_bits) m.add_int(self.preferred_bits)
m.add_int(self.max_bits) m.add_int(self.max_bits)
@ -89,21 +86,23 @@ class KexGex (object):
return self._parse_kexdh_gex_request_old(m) return self._parse_kexdh_gex_request_old(m)
raise SSHException('KexGex asked to handle packet type %d' % ptype) raise SSHException('KexGex asked to handle packet type %d' % ptype)
### internals... ### internals...
def _generate_x(self): def _generate_x(self):
# generate an "x" (1 < x < (p-1)/2). # generate an "x" (1 < x < (p-1)/2).
q = (self.p - 1) // 2 q = (self.p - 1) // 2
qnorm = util.deflate_long(q, 0) qnorm = util.deflate_long(q, 0)
qhbyte = byte_ord(qnorm[0]) qhbyte = ord(qnorm[0])
byte_count = len(qnorm) bytes = len(qnorm)
qmask = 0xff qmask = 0xff
while not (qhbyte & 0x80): while not (qhbyte & 0x80):
qhbyte <<= 1 qhbyte <<= 1
qmask >>= 1 qmask >>= 1
while True: while True:
x_bytes = os.urandom(byte_count) x_bytes = self.transport.rng.read(bytes)
x_bytes = byte_mask(x_bytes[0], qmask) + x_bytes[1:] x_bytes = chr(ord(x_bytes[0]) & qmask) + x_bytes[1:]
x = util.inflate_long(x_bytes, 1) x = util.inflate_long(x_bytes, 1)
if (x > 1) and (x < q): if (x > 1) and (x < q):
break break
@ -136,7 +135,7 @@ class KexGex (object):
self.transport._log(DEBUG, 'Picking p (%d <= %d <= %d bits)' % (minbits, preferredbits, maxbits)) self.transport._log(DEBUG, 'Picking p (%d <= %d <= %d bits)' % (minbits, preferredbits, maxbits))
self.g, self.p = pack.get_modulus(minbits, preferredbits, maxbits) self.g, self.p = pack.get_modulus(minbits, preferredbits, maxbits)
m = Message() m = Message()
m.add_byte(c_MSG_KEXDH_GEX_GROUP) m.add_byte(chr(_MSG_KEXDH_GEX_GROUP))
m.add_mpint(self.p) m.add_mpint(self.p)
m.add_mpint(self.g) m.add_mpint(self.g)
self.transport._send_message(m) self.transport._send_message(m)
@ -157,7 +156,7 @@ class KexGex (object):
self.transport._log(DEBUG, 'Picking p (~ %d bits)' % (self.preferred_bits,)) self.transport._log(DEBUG, 'Picking p (~ %d bits)' % (self.preferred_bits,))
self.g, self.p = pack.get_modulus(self.min_bits, self.preferred_bits, self.max_bits) self.g, self.p = pack.get_modulus(self.min_bits, self.preferred_bits, self.max_bits)
m = Message() m = Message()
m.add_byte(c_MSG_KEXDH_GEX_GROUP) m.add_byte(chr(_MSG_KEXDH_GEX_GROUP))
m.add_mpint(self.p) m.add_mpint(self.p)
m.add_mpint(self.g) m.add_mpint(self.g)
self.transport._send_message(m) self.transport._send_message(m)
@ -176,7 +175,7 @@ class KexGex (object):
# now compute e = g^x mod p # now compute e = g^x mod p
self.e = pow(self.g, self.x, self.p) self.e = pow(self.g, self.x, self.p)
m = Message() m = Message()
m.add_byte(c_MSG_KEXDH_GEX_INIT) m.add_byte(chr(_MSG_KEXDH_GEX_INIT))
m.add_mpint(self.e) m.add_mpint(self.e)
self.transport._send_message(m) self.transport._send_message(m)
self.transport._expect_packet(_MSG_KEXDH_GEX_REPLY) self.transport._expect_packet(_MSG_KEXDH_GEX_REPLY)
@ -188,7 +187,7 @@ class KexGex (object):
self._generate_x() self._generate_x()
self.f = pow(self.g, self.x, self.p) self.f = pow(self.g, self.x, self.p)
K = pow(self.e, self.x, self.p) K = pow(self.e, self.x, self.p)
key = self.transport.get_server_key().asbytes() key = str(self.transport.get_server_key())
# okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || min || n || max || p || g || e || f || K) # okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || min || n || max || p || g || e || f || K)
hm = Message() hm = Message()
hm.add(self.transport.remote_version, self.transport.local_version, hm.add(self.transport.remote_version, self.transport.local_version,
@ -204,19 +203,19 @@ class KexGex (object):
hm.add_mpint(self.e) hm.add_mpint(self.e)
hm.add_mpint(self.f) hm.add_mpint(self.f)
hm.add_mpint(K) hm.add_mpint(K)
H = sha1(hm.asbytes()).digest() H = SHA.new(str(hm)).digest()
self.transport._set_K_H(K, H) self.transport._set_K_H(K, H)
# sign it # sign it
sig = self.transport.get_server_key().sign_ssh_data(H) sig = self.transport.get_server_key().sign_ssh_data(self.transport.rng, H)
# send reply # send reply
m = Message() m = Message()
m.add_byte(c_MSG_KEXDH_GEX_REPLY) m.add_byte(chr(_MSG_KEXDH_GEX_REPLY))
m.add_string(key) m.add_string(key)
m.add_mpint(self.f) m.add_mpint(self.f)
m.add_string(sig) m.add_string(str(sig))
self.transport._send_message(m) self.transport._send_message(m)
self.transport._activate_outbound() self.transport._activate_outbound()
def _parse_kexdh_gex_reply(self, m): def _parse_kexdh_gex_reply(self, m):
host_key = m.get_string() host_key = m.get_string()
self.f = m.get_mpint() self.f = m.get_mpint()
@ -239,6 +238,6 @@ class KexGex (object):
hm.add_mpint(self.e) hm.add_mpint(self.e)
hm.add_mpint(self.f) hm.add_mpint(self.f)
hm.add_mpint(K) hm.add_mpint(K)
self.transport._set_K_H(K, sha1(hm.asbytes()).digest()) self.transport._set_K_H(K, SHA.new(str(hm)).digest())
self.transport._verify_key(host_key, sig) self.transport._verify_key(host_key, sig)
self.transport._activate_outbound() self.transport._activate_outbound()

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -21,26 +21,23 @@ 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. 1024 bit key halves, using a known "p" prime and "g" generator.
""" """
import os from Crypto.Hash import SHA
from hashlib import sha1
from paramiko.common import *
from paramiko import util from paramiko import util
from paramiko.common import max_byte, zero_byte
from paramiko.message import Message from paramiko.message import Message
from paramiko.py3compat import byte_chr, long, byte_mask
from paramiko.ssh_exception import SSHException from paramiko.ssh_exception import SSHException
import six
if six.PY3:
long = lambda x: int(x)
_MSG_KEXDH_INIT, _MSG_KEXDH_REPLY = range(30, 32) _MSG_KEXDH_INIT, _MSG_KEXDH_REPLY = range(30, 32)
c_MSG_KEXDH_INIT, c_MSG_KEXDH_REPLY = [byte_chr(c) for c in range(30, 32)]
# draft-ietf-secsh-transport-09.txt, page 17 # draft-ietf-secsh-transport-09.txt, page 17
P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF P = long(0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF)
G = 2 G = 2
b7fffffffffffffff = byte_chr(0x7f) + max_byte * 7
b0000000000000000 = zero_byte * 8
class KexGroup1(object): class KexGroup1(object):
@ -48,9 +45,9 @@ class KexGroup1(object):
def __init__(self, transport): def __init__(self, transport):
self.transport = transport self.transport = transport
self.x = long(0) self.x = 0
self.e = long(0) self.e = 0
self.f = long(0) self.f = 0
def start_kex(self): def start_kex(self):
self._generate_x() self._generate_x()
@ -62,7 +59,7 @@ class KexGroup1(object):
# compute e = g^x mod p (where g=2), and send it # compute e = g^x mod p (where g=2), and send it
self.e = pow(G, self.x, P) self.e = pow(G, self.x, P)
m = Message() m = Message()
m.add_byte(c_MSG_KEXDH_INIT) m.add_byte(chr(_MSG_KEXDH_INIT))
m.add_mpint(self.e) m.add_mpint(self.e)
self.transport._send_message(m) self.transport._send_message(m)
self.transport._expect_packet(_MSG_KEXDH_REPLY) self.transport._expect_packet(_MSG_KEXDH_REPLY)
@ -73,9 +70,11 @@ class KexGroup1(object):
elif not self.transport.server_mode and (ptype == _MSG_KEXDH_REPLY): elif not self.transport.server_mode and (ptype == _MSG_KEXDH_REPLY):
return self._parse_kexdh_reply(m) return self._parse_kexdh_reply(m)
raise SSHException('KexGroup1 asked to handle packet type %d' % ptype) raise SSHException('KexGroup1 asked to handle packet type %d' % ptype)
### internals... ### internals...
def _generate_x(self): def _generate_x(self):
# generate an "x" (1 < x < q), where q is (p-1)/2. # generate an "x" (1 < x < q), where q is (p-1)/2.
# p is a 128-byte (1024-bit) number, where the first 64 bits are 1. # p is a 128-byte (1024-bit) number, where the first 64 bits are 1.
@ -83,10 +82,10 @@ class KexGroup1(object):
# potential x where the first 63 bits are 1, because some of those will be # 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). # larger than q (but this is a tiny tiny subset of potential x).
while 1: while 1:
x_bytes = os.urandom(128) x_bytes = self.transport.rng.read(128)
x_bytes = byte_mask(x_bytes[0], 0x7f) + x_bytes[1:] x_bytes = chr(ord(x_bytes[0]) & 0x7f) + x_bytes[1:]
if (x_bytes[:8] != b7fffffffffffffff and if (x_bytes[:8] != '\x7F\xFF\xFF\xFF\xFF\xFF\xFF\xFF') and \
x_bytes[:8] != b0000000000000000): (x_bytes[:8] != '\x00\x00\x00\x00\x00\x00\x00\x00'):
break break
self.x = util.inflate_long(x_bytes) self.x = util.inflate_long(x_bytes)
@ -96,7 +95,7 @@ class KexGroup1(object):
self.f = m.get_mpint() self.f = m.get_mpint()
if (self.f < 1) or (self.f > P - 1): if (self.f < 1) or (self.f > P - 1):
raise SSHException('Server kex "f" is out of range') raise SSHException('Server kex "f" is out of range')
sig = m.get_binary() sig = m.get_string()
K = pow(self.f, self.x, P) K = pow(self.f, self.x, P)
# okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || e || f || K) # okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || e || f || K)
hm = Message() hm = Message()
@ -106,7 +105,7 @@ class KexGroup1(object):
hm.add_mpint(self.e) hm.add_mpint(self.e)
hm.add_mpint(self.f) hm.add_mpint(self.f)
hm.add_mpint(K) hm.add_mpint(K)
self.transport._set_K_H(K, sha1(hm.asbytes()).digest()) self.transport._set_K_H(K, SHA.new(str(hm)).digest())
self.transport._verify_key(host_key, sig) self.transport._verify_key(host_key, sig)
self.transport._activate_outbound() self.transport._activate_outbound()
@ -116,7 +115,7 @@ class KexGroup1(object):
if (self.e < 1) or (self.e > P - 1): if (self.e < 1) or (self.e > P - 1):
raise SSHException('Client kex "e" is out of range') raise SSHException('Client kex "e" is out of range')
K = pow(self.e, self.x, P) K = pow(self.e, self.x, P)
key = self.transport.get_server_key().asbytes() key = str(self.transport.get_server_key())
# okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || e || f || K) # okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || e || f || K)
hm = Message() hm = Message()
hm.add(self.transport.remote_version, self.transport.local_version, hm.add(self.transport.remote_version, self.transport.local_version,
@ -125,15 +124,15 @@ class KexGroup1(object):
hm.add_mpint(self.e) hm.add_mpint(self.e)
hm.add_mpint(self.f) hm.add_mpint(self.f)
hm.add_mpint(K) hm.add_mpint(K)
H = sha1(hm.asbytes()).digest() H = SHA.new(str(hm)).digest()
self.transport._set_K_H(K, H) self.transport._set_K_H(K, H)
# sign it # sign it
sig = self.transport.get_server_key().sign_ssh_data(H) sig = self.transport.get_server_key().sign_ssh_data(self.transport.rng, H)
# send reply # send reply
m = Message() m = Message()
m.add_byte(c_MSG_KEXDH_REPLY) m.add_byte(chr(_MSG_KEXDH_REPLY))
m.add_string(key) m.add_string(key)
m.add_mpint(self.f) m.add_mpint(self.f)
m.add_string(sig) m.add_string(str(sig))
self.transport._send_message(m) self.transport._send_message(m)
self.transport._activate_outbound() self.transport._activate_outbound()

66
paramiko/logging22.py Normal file
View File

@ -0,0 +1,66 @@
# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com>
#
# This file is part of paramiko.
#
# Paramiko is free software; you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free
# Software Foundation; either version 2.1 of the License, or (at your option)
# any later version.
#
# Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Paramiko; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
"""
Stub out logging on python < 2.3.
"""
DEBUG = 10
INFO = 20
WARNING = 30
ERROR = 40
CRITICAL = 50
def getLogger(name):
return _logger
class logger (object):
def __init__(self):
self.handlers = [ ]
self.level = ERROR
def setLevel(self, level):
self.level = level
def addHandler(self, h):
self.handlers.append(h)
def addFilter(self, filter):
pass
def log(self, level, text):
if level >= self.level:
for h in self.handlers:
h.f.write(text + '\n')
h.f.flush()
class StreamHandler (object):
def __init__(self, f):
self.f = f
def setFormatter(self, f):
pass
class Formatter (object):
def __init__(self, x, y):
pass
_logger = logger()

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -20,57 +20,60 @@
Implementation of an SSH2 "message". Implementation of an SSH2 "message".
""" """
import struct import struct
try:
from cStringIO import StringIO
except ImportError:
from io import StringIO
import six
if six.PY3:
long = lambda x: int(x)
from paramiko import util from paramiko import util
from paramiko.common import zero_byte, max_byte, one_byte, asbytes
from paramiko.py3compat import long, BytesIO, u, integer_types
class Message (object): class Message (object):
""" """
An SSH2 message is a stream of bytes that encodes some combination of An SSH2 I{Message} is a stream of bytes that encodes some combination of
strings, integers, bools, and infinite-precision integers (known in Python strings, integers, bools, and infinite-precision integers (known in python
as longs). This class builds or breaks down such a byte stream. as I{long}s). This class builds or breaks down such a byte stream.
Normally you don't need to deal with anything this low-level, but it's Normally you don't need to deal with anything this low-level, but it's
exposed for people implementing custom extensions, or features that exposed for people implementing custom extensions, or features that
paramiko doesn't support yet. paramiko doesn't support yet.
""" """
big_int = long(0xff000000)
def __init__(self, content=None): def __init__(self, content=None):
""" """
Create a new SSH2 message. Create a new SSH2 Message.
:param str content: @param content: the byte stream to use as the Message content (passed
the byte stream to use as the message content (passed in only when in only when decomposing a Message).
decomposing a message). @type content: string
""" """
if content is not None: if content != None:
self.packet = BytesIO(content) self.packet = StringIO(content)
else: else:
self.packet = BytesIO() self.packet = StringIO()
def __str__(self): def __str__(self):
""" """
Return the byte stream content of this message, as a string/bytes obj. Return the byte stream content of this Message, as a string.
@return: the contents of this Message.
@rtype: string
""" """
return self.asbytes() return self.packet.getvalue()
def __repr__(self): def __repr__(self):
""" """
Returns a string representation of this object, for debugging. Returns a string representation of this object, for debugging.
@rtype: string
""" """
return 'paramiko.Message(' + repr(self.packet.getvalue()) + ')' return 'paramiko.Message(' + repr(self.packet.getvalue()) + ')'
def asbytes(self):
"""
Return the byte stream content of this Message, as bytes.
"""
return self.packet.getvalue()
def rewind(self): def rewind(self):
""" """
Rewind the message to the beginning as if no items had been parsed Rewind the message to the beginning as if no items had been parsed
@ -80,8 +83,11 @@ class Message (object):
def get_remainder(self): def get_remainder(self):
""" """
Return the bytes (as a `str`) of this message that haven't already been Return the bytes of this Message that haven't already been parsed and
parsed and returned. returned.
@return: a string of the bytes not parsed yet.
@rtype: string
""" """
position = self.packet.tell() position = self.packet.tell()
remainder = self.packet.read() remainder = self.packet.read()
@ -90,9 +96,12 @@ class Message (object):
def get_so_far(self): def get_so_far(self):
""" """
Returns the `str` bytes of this message that have been parsed and Returns the bytes of this Message that have been parsed and returned.
returned. The string passed into a message's constructor can be The string passed into a Message's constructor can be regenerated by
regenerated by concatenating ``get_so_far`` and `get_remainder`. concatenating C{get_so_far} and L{get_remainder}.
@return: a string of the bytes parsed so far.
@rtype: string
""" """
position = self.packet.tell() position = self.packet.tell()
self.rewind() self.rewind()
@ -100,51 +109,44 @@ class Message (object):
def get_bytes(self, n): def get_bytes(self, n):
""" """
Return the next ``n`` bytes of the message (as a `str`), without Return the next C{n} bytes of the Message, without decomposing into
decomposing into an int, decoded string, etc. Just the raw bytes are an int, string, etc. Just the raw bytes are returned.
returned. Returns a string of ``n`` zero bytes if there weren't ``n``
bytes remaining in the message. @return: a string of the next C{n} bytes of the Message, or a string
of C{n} zero bytes, if there aren't C{n} bytes remaining.
@rtype: string
""" """
b = self.packet.read(n) b = self.packet.read(n)
max_pad_size = 1 << 20 # Limit padding to 1 MB max_pad_size = 1<<20 # Limit padding to 1 MB
if len(b) < n < max_pad_size: if len(b) < n and n < max_pad_size:
return b + zero_byte * (n - len(b)) return b + '\x00' * (n - len(b))
return b return b
def get_byte(self): def get_byte(self):
""" """
Return the next byte of the message, without decomposing it. This Return the next byte of the Message, without decomposing it. This
is equivalent to `get_bytes(1) <get_bytes>`. is equivalent to L{get_bytes(1)<get_bytes>}.
:return: @return: the next byte of the Message, or C{'\000'} if there aren't
the next (`str`) byte of the message, or ``'\000'`` if there aren't
any bytes remaining. any bytes remaining.
@rtype: string
""" """
return self.get_bytes(1) return self.get_bytes(1)
def get_boolean(self): def get_boolean(self):
""" """
Fetch a boolean from the stream. Fetch a boolean from the stream.
@return: C{True} or C{False} (from the Message).
@rtype: bool
""" """
b = self.get_bytes(1) b = self.get_bytes(1)
return b != zero_byte return b != '\x00'
def get_int(self): def get_int(self):
""" """
Fetch an int from the stream. Fetch an int from the stream.
:return: a 32-bit unsigned `int`.
"""
byte = self.get_bytes(1)
if byte == max_byte:
return util.inflate_long(self.get_binary())
byte += self.get_bytes(3)
return struct.unpack('>I', byte)[0]
def get_size(self):
"""
Fetch an int from the stream.
@return: a 32-bit unsigned integer. @return: a 32-bit unsigned integer.
@rtype: int @rtype: int
""" """
@ -154,7 +156,8 @@ class Message (object):
""" """
Fetch a 64-bit int from the stream. Fetch a 64-bit int from the stream.
:return: a 64-bit unsigned integer (`long`). @return: a 64-bit unsigned integer.
@rtype: long
""" """
return struct.unpack('>Q', self.get_bytes(8))[0] return struct.unpack('>Q', self.get_bytes(8))[0]
@ -162,19 +165,12 @@ class Message (object):
""" """
Fetch a long int (mpint) from the stream. Fetch a long int (mpint) from the stream.
:return: an arbitrary-length integer (`long`). @return: an arbitrary-length integer.
@rtype: long
""" """
return util.inflate_long(self.get_binary()) return util.inflate_long(self.get_string())
def get_string(self): def get_string(self):
"""
Fetch a `str` from the stream. This could be a byte string and may
contain unprintable characters. (It's not unheard of for a string to
contain another byte-stream message.)
"""
return self.get_bytes(self.get_size())
def get_text(self):
""" """
Fetch a string from the stream. This could be a byte string and may Fetch a string from the stream. This could be a byte string and may
contain unprintable characters. (It's not unheard of for a string to contain unprintable characters. (It's not unheard of for a string to
@ -183,33 +179,24 @@ class Message (object):
@return: a string. @return: a string.
@rtype: string @rtype: string
""" """
return u(self.get_bytes(self.get_size())) return self.get_bytes(self.get_int())
#return self.get_bytes(self.get_size())
def get_binary(self):
"""
Fetch a string from the stream. This could be a byte string and may
contain unprintable characters. (It's not unheard of for a string to
contain another byte-stream Message.)
@return: a string.
@rtype: string
"""
return self.get_bytes(self.get_size())
def get_list(self): def get_list(self):
""" """
Fetch a `list` of `strings <str>` from the stream. Fetch a list of strings from the stream. These are trivially encoded
as comma-separated values in a string.
These are trivially encoded as comma-separated values in a string.
@return: a list of strings.
@rtype: list of strings
""" """
return self.get_text().split(',') return self.get_string().split(',')
def add_bytes(self, b): def add_bytes(self, b):
""" """
Write bytes to the stream, without any formatting. Write bytes to the stream, without any formatting.
:param str b: bytes to add @param b: bytes to add
@type b: str
""" """
self.packet.write(b) self.packet.write(b)
return self return self
@ -218,7 +205,8 @@ class Message (object):
""" """
Write a single byte to the stream, without any formatting. Write a single byte to the stream, without any formatting.
:param str b: byte to add @param b: byte to add
@type b: str
""" """
self.packet.write(b) self.packet.write(b)
return self return self
@ -227,41 +215,31 @@ class Message (object):
""" """
Add a boolean value to the stream. Add a boolean value to the stream.
:param bool b: boolean value to add @param b: boolean value to add
@type b: bool
""" """
if b: if b:
self.packet.write(one_byte) self.add_byte('\x01')
else: else:
self.packet.write(zero_byte) self.add_byte('\x00')
return self return self
def add_size(self, n):
"""
Add an integer to the stream.
:param int n: integer to add
"""
self.packet.write(struct.pack('>I', n))
return self
def add_int(self, n): def add_int(self, n):
""" """
Add an integer to the stream. Add an integer to the stream.
:param int n: integer to add @param n: integer to add
@type n: int
""" """
if n >= Message.big_int: self.packet.write(struct.pack('>I', n))
self.packet.write(max_byte)
self.add_string(util.deflate_long(n))
else:
self.packet.write(struct.pack('>I', n))
return self return self
def add_int64(self, n): def add_int64(self, n):
""" """
Add a 64-bit int to the stream. Add a 64-bit int to the stream.
:param long n: long int to add @param n: long int to add
@type n: long
""" """
self.packet.write(struct.pack('>Q', n)) self.packet.write(struct.pack('>Q', n))
return self return self
@ -271,7 +249,8 @@ class Message (object):
Add a long int to the stream, encoded as an infinite-precision Add a long int to the stream, encoded as an infinite-precision
integer. This method only works on positive numbers. integer. This method only works on positive numbers.
:param long z: long int to add @param z: long int to add
@type z: long
""" """
self.add_string(util.deflate_long(z)) self.add_string(util.deflate_long(z))
return self return self
@ -280,10 +259,10 @@ class Message (object):
""" """
Add a string to the stream. Add a string to the stream.
:param str s: string to add @param s: string to add
@type s: str
""" """
s = asbytes(s) self.add_int(len(s))
self.add_size(len(s))
self.packet.write(s) self.packet.write(s)
return self return self
@ -293,30 +272,42 @@ class Message (object):
a single string of values separated by commas. (Yes, really, that's a single string of values separated by commas. (Yes, really, that's
how SSH2 does it.) how SSH2 does it.)
:param list l: list of strings to add @param l: list of strings to add
@type l: list(str)
""" """
self.add_string(','.join(l)) self.add_string(','.join(l))
return self return self
def _add(self, i): def _add(self, i):
if type(i) is bool: if type(i) is str:
return self.add_string(i)
elif type(i) in six.integer_types:
if type(i) is int and not six.PY3:
return self.add_int(i)
else:
if i > long(0xffffffff):
return self.add_mpint(i)
else:
return self.add_int(i)
elif type(i) is bool:
return self.add_boolean(i) return self.add_boolean(i)
elif isinstance(i, integer_types):
return self.add_int(i)
elif type(i) is list: elif type(i) is list:
return self.add_list(i) return self.add_list(i)
else: else:
return self.add_string(i) raise Exception('Unknown type')
def add(self, *seq): def add(self, *seq):
""" """
Add a sequence of items to the stream. The values are encoded based Add a sequence of items to the stream. The values are encoded based
on their type: str, int, bool, list, or long. on their type: str, int, bool, list, or long.
.. warning::
Longs are encoded non-deterministically. Don't use this method.
:param seq: the sequence of items @param seq: the sequence of items
@type seq: sequence
@bug: longs are encoded non-deterministically. Don't use this method.
""" """
for item in seq: for item in seq:
self._add(item) self._add(item)

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -17,27 +17,37 @@
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
""" """
Packet handling Packetizer.
""" """
import errno import errno
import os import select
import socket import socket
import struct import struct
import threading import threading
import time import time
from hmac import HMAC
import six
if six.PY3:
long = lambda x: int(x)
from paramiko.common import *
from paramiko import util from paramiko import util
from paramiko.common import linefeed_byte, cr_byte_value, asbytes, MSG_NAMES, \
DEBUG, xffffffff, zero_byte
from paramiko.py3compat import u, byte_ord
from paramiko.ssh_exception import SSHException, ProxyCommandFailure from paramiko.ssh_exception import SSHException, ProxyCommandFailure
from paramiko.message import Message from paramiko.message import Message
got_r_hmac = False
try:
import r_hmac
got_r_hmac = True
except ImportError:
pass
def compute_hmac(key, message, digest_class): def compute_hmac(key, message, digest_class):
return HMAC(key, message, digest_class).digest() if got_r_hmac:
return r_hmac.HMAC(key, message, digest_class).digest()
from Crypto.Hash import HMAC
return HMAC.HMAC(key, message, digest_class).digest()
class NeedRekeyException (Exception): class NeedRekeyException (Exception):
@ -54,8 +64,8 @@ class Packetizer (object):
REKEY_PACKETS = pow(2, 29) REKEY_PACKETS = pow(2, 29)
REKEY_BYTES = pow(2, 29) REKEY_BYTES = pow(2, 29)
REKEY_PACKETS_OVERFLOW_MAX = pow(2, 29) # Allow receiving this many packets after a re-key request before terminating REKEY_PACKETS_OVERFLOW_MAX = pow(2,29) # Allow receiving this many packets after a re-key request before terminating
REKEY_BYTES_OVERFLOW_MAX = pow(2, 29) # Allow receiving this many bytes after a re-key request before terminating REKEY_BYTES_OVERFLOW_MAX = pow(2,29) # Allow receiving this many bytes after a re-key request before terminating
def __init__(self, socket): def __init__(self, socket):
self.__socket = socket self.__socket = socket
@ -64,7 +74,7 @@ class Packetizer (object):
self.__dump_packets = False self.__dump_packets = False
self.__need_rekey = False self.__need_rekey = False
self.__init_count = 0 self.__init_count = 0
self.__remainder = bytes() self.__remainder = ''
# used for noticing when to re-key: # used for noticing when to re-key:
self.__sent_bytes = 0 self.__sent_bytes = 0
@ -84,8 +94,8 @@ class Packetizer (object):
self.__sdctr_out = False self.__sdctr_out = False
self.__mac_engine_out = None self.__mac_engine_out = None
self.__mac_engine_in = None self.__mac_engine_in = None
self.__mac_key_out = bytes() self.__mac_key_out = ''
self.__mac_key_in = bytes() self.__mac_key_in = ''
self.__compress_engine_out = None self.__compress_engine_out = None
self.__compress_engine_in = None self.__compress_engine_in = None
self.__sequence_number_out = 0 self.__sequence_number_out = 0
@ -101,7 +111,7 @@ class Packetizer (object):
def set_log(self, log): def set_log(self, log):
""" """
Set the Python log object to use for logging. Set the python log object to use for logging.
""" """
self.__logger = log self.__logger = log
@ -166,15 +176,17 @@ class Packetizer (object):
def need_rekey(self): def need_rekey(self):
""" """
Returns ``True`` if a new set of keys needs to be negotiated. This Returns C{True} if a new set of keys needs to be negotiated. This
will be triggered during a packet read or write, so it should be will be triggered during a packet read or write, so it should be
checked after every read or write, or at least after every few. checked after every read or write, or at least after every few.
@return: C{True} if a new set of keys needs to be negotiated
""" """
return self.__need_rekey return self.__need_rekey
def set_keepalive(self, interval, callback): def set_keepalive(self, interval, callback):
""" """
Turn on/off the callback keepalive. If ``interval`` seconds pass with Turn on/off the callback keepalive. If C{interval} seconds pass with
no data read from or written to the socket, the callback will be no data read from or written to the socket, the callback will be
executed and the timer will be reset. executed and the timer will be reset.
""" """
@ -186,18 +198,21 @@ class Packetizer (object):
""" """
Read as close to N bytes as possible, blocking as long as necessary. Read as close to N bytes as possible, blocking as long as necessary.
:param int n: number of bytes to read @param n: number of bytes to read
:return: the data read, as a `str` @type n: int
@return: the data read
:raises EOFError: @rtype: str
if the socket was closed before all the bytes could be read @raise EOFError: if the socket was closed before all the bytes could
be read
""" """
out = bytes() out = ''
# handle over-reading from reading the banner line # handle over-reading from reading the banner line
if len(self.__remainder) > 0: if len(self.__remainder) > 0:
out = self.__remainder[:n] out = self.__remainder[:n]
self.__remainder = self.__remainder[n:] self.__remainder = self.__remainder[n:]
n -= len(out) n -= len(out)
if PY22:
return self._py22_read_all(n, out)
while n > 0: while n > 0:
got_timeout = False got_timeout = False
try: try:
@ -246,7 +261,7 @@ class Packetizer (object):
else: else:
n = -1 n = -1
except ProxyCommandFailure: except ProxyCommandFailure:
raise # so it doesn't get swallowed by the below catchall raise # so it doesn't get swallowed by the below catchall
except Exception: except Exception:
# could be: (32, 'Broken pipe') # could be: (32, 'Broken pipe')
n = -1 n = -1
@ -267,22 +282,22 @@ class Packetizer (object):
line, so it's okay to attempt large reads. line, so it's okay to attempt large reads.
""" """
buf = self.__remainder buf = self.__remainder
while not linefeed_byte in buf: while not '\n' in buf:
buf += self._read_timeout(timeout) buf += self._read_timeout(timeout)
n = buf.index(linefeed_byte) n = buf.index('\n')
self.__remainder = buf[n + 1:] self.__remainder = buf[n+1:]
buf = buf[:n] buf = buf[:n]
if (len(buf) > 0) and (buf[-1] == cr_byte_value): if (len(buf) > 0) and (buf[-1] == '\r'):
buf = buf[:-1] buf = buf[:-1]
return u(buf) return buf
def send_message(self, data): def send_message(self, data):
""" """
Write a block of data using the current cipher, as an SSH block. Write a block of data using the current cipher, as an SSH block.
""" """
# encrypt this sucka # encrypt this sucka
data = asbytes(data) data = str(data)
cmd = byte_ord(data[0]) cmd = ord(data[0])
if cmd in MSG_NAMES: if cmd in MSG_NAMES:
cmd_name = MSG_NAMES[cmd] cmd_name = MSG_NAMES[cmd]
else: else:
@ -296,21 +311,21 @@ class Packetizer (object):
if self.__dump_packets: if self.__dump_packets:
self._log(DEBUG, 'Write packet <%s>, length %d' % (cmd_name, orig_len)) self._log(DEBUG, 'Write packet <%s>, length %d' % (cmd_name, orig_len))
self._log(DEBUG, util.format_binary(packet, 'OUT: ')) self._log(DEBUG, util.format_binary(packet, 'OUT: '))
if self.__block_engine_out is not None: if self.__block_engine_out != None:
out = self.__block_engine_out.encrypt(packet) out = self.__block_engine_out.encrypt(packet)
else: else:
out = packet out = packet
# + mac # + mac
if self.__block_engine_out is not None: if self.__block_engine_out != None:
payload = struct.pack('>I', self.__sequence_number_out) + packet payload = struct.pack('>I', self.__sequence_number_out) + packet
out += compute_hmac(self.__mac_key_out, payload, self.__mac_engine_out)[:self.__mac_size_out] out += compute_hmac(self.__mac_key_out, payload, self.__mac_engine_out)[:self.__mac_size_out]
self.__sequence_number_out = (self.__sequence_number_out + 1) & xffffffff self.__sequence_number_out = (self.__sequence_number_out + 1) & long(0xffffffff)
self.write_all(out) self.write_all(out)
self.__sent_bytes += len(out) self.__sent_bytes += len(out)
self.__sent_packets += 1 self.__sent_packets += 1
if (self.__sent_packets >= self.REKEY_PACKETS or self.__sent_bytes >= self.REKEY_BYTES)\ if ((self.__sent_packets >= self.REKEY_PACKETS) or (self.__sent_bytes >= self.REKEY_BYTES)) \
and not self.__need_rekey: and not self.__need_rekey:
# only ask once for rekeying # only ask once for rekeying
self._log(DEBUG, 'Rekeying (hit %d packets, %d bytes sent)' % self._log(DEBUG, 'Rekeying (hit %d packets, %d bytes sent)' %
(self.__sent_packets, self.__sent_bytes)) (self.__sent_packets, self.__sent_bytes))
@ -325,11 +340,11 @@ class Packetizer (object):
Only one thread should ever be in this function (no other locking is Only one thread should ever be in this function (no other locking is
done). done).
:raises SSHException: if the packet is mangled @raise SSHException: if the packet is mangled
:raises NeedRekeyException: if the transport should rekey @raise NeedRekeyException: if the transport should rekey
""" """
header = self.read_all(self.__block_size_in, check_rekey=True) header = self.read_all(self.__block_size_in, check_rekey=True)
if self.__block_engine_in is not None: if self.__block_engine_in != None:
header = self.__block_engine_in.decrypt(header) header = self.__block_engine_in.decrypt(header)
if self.__dump_packets: if self.__dump_packets:
self._log(DEBUG, util.format_binary(header, 'IN: ')) self._log(DEBUG, util.format_binary(header, 'IN: '))
@ -341,7 +356,7 @@ class Packetizer (object):
buf = self.read_all(packet_size + self.__mac_size_in - len(leftover)) buf = self.read_all(packet_size + self.__mac_size_in - len(leftover))
packet = buf[:packet_size - len(leftover)] packet = buf[:packet_size - len(leftover)]
post_packet = buf[packet_size - len(leftover):] post_packet = buf[packet_size - len(leftover):]
if self.__block_engine_in is not None: if self.__block_engine_in != None:
packet = self.__block_engine_in.decrypt(packet) packet = self.__block_engine_in.decrypt(packet)
if self.__dump_packets: if self.__dump_packets:
self._log(DEBUG, util.format_binary(packet, 'IN: ')) self._log(DEBUG, util.format_binary(packet, 'IN: '))
@ -351,11 +366,11 @@ class Packetizer (object):
mac = post_packet[:self.__mac_size_in] mac = post_packet[:self.__mac_size_in]
mac_payload = struct.pack('>II', self.__sequence_number_in, packet_size) + packet mac_payload = struct.pack('>II', self.__sequence_number_in, packet_size) + packet
my_mac = compute_hmac(self.__mac_key_in, mac_payload, self.__mac_engine_in)[:self.__mac_size_in] my_mac = compute_hmac(self.__mac_key_in, mac_payload, self.__mac_engine_in)[:self.__mac_size_in]
if not util.constant_time_bytes_eq(my_mac, mac): if my_mac != mac:
raise SSHException('Mismatched MAC') raise SSHException('Mismatched MAC')
padding = byte_ord(packet[0]) padding = ord(packet[0])
payload = packet[1:packet_size - padding] payload = packet[1:packet_size - padding]
if self.__dump_packets: if self.__dump_packets:
self._log(DEBUG, 'Got payload (%d bytes, %d padding)' % (packet_size, padding)) self._log(DEBUG, 'Got payload (%d bytes, %d padding)' % (packet_size, padding))
@ -364,7 +379,7 @@ class Packetizer (object):
msg = Message(payload[1:]) msg = Message(payload[1:])
msg.seqno = self.__sequence_number_in msg.seqno = self.__sequence_number_in
self.__sequence_number_in = (self.__sequence_number_in + 1) & xffffffff self.__sequence_number_in = (self.__sequence_number_in + 1) & long(0xffffffff)
# check for rekey # check for rekey
raw_packet_size = packet_size + self.__mac_size_in + 4 raw_packet_size = packet_size + self.__mac_size_in + 4
@ -387,7 +402,7 @@ class Packetizer (object):
self.__received_packets_overflow = 0 self.__received_packets_overflow = 0
self._trigger_rekey() self._trigger_rekey()
cmd = byte_ord(payload[0]) cmd = ord(payload[0])
if cmd in MSG_NAMES: if cmd in MSG_NAMES:
cmd_name = MSG_NAMES[cmd] cmd_name = MSG_NAMES[cmd]
else: else:
@ -396,8 +411,10 @@ class Packetizer (object):
self._log(DEBUG, 'Read packet <%s>, length %d' % (cmd_name, len(payload))) self._log(DEBUG, 'Read packet <%s>, length %d' % (cmd_name, len(payload)))
return cmd, msg return cmd, msg
########## protected ########## protected
def _log(self, level, msg): def _log(self, level, msg):
if self.__logger is None: if self.__logger is None:
return return
@ -409,7 +426,7 @@ class Packetizer (object):
def _check_keepalive(self): def _check_keepalive(self):
if (not self.__keepalive_interval) or (not self.__block_engine_out) or \ if (not self.__keepalive_interval) or (not self.__block_engine_out) or \
self.__need_rekey: self.__need_rekey:
# wait till we're encrypting, and not in the middle of rekeying # wait till we're encrypting, and not in the middle of rekeying
return return
now = time.time() now = time.time()
@ -417,7 +434,40 @@ class Packetizer (object):
self.__keepalive_callback() self.__keepalive_callback()
self.__keepalive_last = now self.__keepalive_last = now
def _py22_read_all(self, n, out):
while n > 0:
r, w, e = select.select([self.__socket], [], [], 0.1)
if self.__socket not in r:
if self.__closed:
raise EOFError()
self._check_keepalive()
else:
x = self.__socket.recv(n)
if len(x) == 0:
raise EOFError()
out += x
n -= len(x)
return out
def _py22_read_timeout(self, timeout):
start = time.time()
while True:
r, w, e = select.select([self.__socket], [], [], 0.1)
if self.__socket in r:
x = self.__socket.recv(1)
if len(x) == 0:
raise EOFError()
break
if self.__closed:
raise EOFError()
now = time.time()
if now - start >= timeout:
raise socket.timeout()
return x
def _read_timeout(self, timeout): def _read_timeout(self, timeout):
if PY22:
return self._py22_read_timeout(timeout)
start = time.time() start = time.time()
while True: while True:
try: try:
@ -428,8 +478,8 @@ class Packetizer (object):
except socket.timeout: except socket.timeout:
pass pass
except EnvironmentError as e: except EnvironmentError as e:
if (type(e.args) is tuple and len(e.args) > 0 and if ((type(e.args) is tuple) and (len(e.args) > 0) and
e.args[0] == errno.EINTR): (e.args[0] == errno.EINTR)):
pass pass
else: else:
raise raise
@ -449,9 +499,9 @@ class Packetizer (object):
if self.__sdctr_out or self.__block_engine_out is None: if self.__sdctr_out or self.__block_engine_out is None:
# cute trick i caught openssh doing: if we're not encrypting or SDCTR mode (RFC4344), # cute trick i caught openssh doing: if we're not encrypting or SDCTR mode (RFC4344),
# don't waste random bytes for the padding # don't waste random bytes for the padding
packet += (zero_byte * padding) packet += (chr(0) * padding)
else: else:
packet += os.urandom(padding) packet += rng.read(padding)
return packet return packet
def _trigger_rekey(self): def _trigger_rekey(self):

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -17,12 +17,11 @@
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
""" """
Abstraction of a one-way pipe where the read end can be used in Abstraction of a one-way pipe where the read end can be used in select().
`select.select`. Normally this is trivial, but Windows makes it nearly Normally this is trivial, but Windows makes it nearly impossible.
impossible.
The pipe acts like an Event, which can be set or cleared. When set, the pipe The pipe acts like an Event, which can be set or cleared. When set, the pipe
will trigger as readable in `select <select.select>`. will trigger as readable in select().
""" """
import sys import sys
@ -30,7 +29,7 @@ import os
import socket import socket
def make_pipe(): def make_pipe ():
if sys.platform[:3] != 'win': if sys.platform[:3] != 'win':
p = PosixPipe() p = PosixPipe()
else: else:
@ -39,34 +38,34 @@ def make_pipe():
class PosixPipe (object): class PosixPipe (object):
def __init__(self): def __init__ (self):
self._rfd, self._wfd = os.pipe() self._rfd, self._wfd = os.pipe()
self._set = False self._set = False
self._forever = False self._forever = False
self._closed = False self._closed = False
def close(self): def close (self):
os.close(self._rfd) os.close(self._rfd)
os.close(self._wfd) os.close(self._wfd)
# used for unit tests: # used for unit tests:
self._closed = True self._closed = True
def fileno(self): def fileno (self):
return self._rfd return self._rfd
def clear(self): def clear (self):
if not self._set or self._forever: if not self._set or self._forever:
return return
os.read(self._rfd, 1) os.read(self._rfd, 1)
self._set = False self._set = False
def set(self): def set (self):
if self._set or self._closed: if self._set or self._closed:
return return
self._set = True self._set = True
os.write(self._wfd, b'*') os.write(self._wfd, '*')
def set_forever(self): def set_forever (self):
self._forever = True self._forever = True
self.set() self.set()
@ -76,7 +75,7 @@ class WindowsPipe (object):
On Windows, only an OS-level "WinSock" may be used in select(), but reads On Windows, only an OS-level "WinSock" may be used in select(), but reads
and writes must be to the actual socket object. and writes must be to the actual socket object.
""" """
def __init__(self): def __init__ (self):
serv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) serv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serv.bind(('127.0.0.1', 0)) serv.bind(('127.0.0.1', 0))
serv.listen(1) serv.listen(1)
@ -91,13 +90,13 @@ class WindowsPipe (object):
self._forever = False self._forever = False
self._closed = False self._closed = False
def close(self): def close (self):
self._rsock.close() self._rsock.close()
self._wsock.close() self._wsock.close()
# used for unit tests: # used for unit tests:
self._closed = True self._closed = True
def fileno(self): def fileno (self):
return self._rsock.fileno() return self._rsock.fileno()
def clear (self): def clear (self):
@ -110,7 +109,7 @@ class WindowsPipe (object):
if self._set or self._closed: if self._set or self._closed:
return return
self._set = True self._set = True
self._wsock.send(b'*') self._wsock.send('*')
def set_forever (self): def set_forever (self):
self._forever = True self._forever = True

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -23,13 +23,14 @@ Common API for all public keys.
import base64 import base64
from binascii import hexlify, unhexlify from binascii import hexlify, unhexlify
import os import os
from hashlib import md5
from Crypto.Hash import MD5
from Crypto.Cipher import DES3, AES from Crypto.Cipher import DES3, AES
import six
from paramiko.common import *
from paramiko import util from paramiko import util
from paramiko.common import o600, zero_byte from paramiko.message import Message
from paramiko.py3compat import u, encodebytes, decodebytes, b
from paramiko.ssh_exception import SSHException, PasswordRequiredException from paramiko.ssh_exception import SSHException, PasswordRequiredException
@ -40,39 +41,40 @@ class PKey (object):
# known encryption types for private key files: # known encryption types for private key files:
_CIPHER_TABLE = { _CIPHER_TABLE = {
'AES-128-CBC': {'cipher': AES, 'keysize': 16, 'blocksize': 16, 'mode': AES.MODE_CBC}, 'AES-128-CBC': { 'cipher': AES, 'keysize': 16, 'blocksize': 16, 'mode': AES.MODE_CBC },
'DES-EDE3-CBC': {'cipher': DES3, 'keysize': 24, 'blocksize': 8, 'mode': DES3.MODE_CBC}, 'DES-EDE3-CBC': { 'cipher': DES3, 'keysize': 24, 'blocksize': 8, 'mode': DES3.MODE_CBC },
} }
def __init__(self, msg=None, data=None): def __init__(self, msg=None, data=None):
""" """
Create a new instance of this public key type. If ``msg`` is given, Create a new instance of this public key type. If C{msg} is given,
the key's public part(s) will be filled in from the message. If the key's public part(s) will be filled in from the message. If
``data`` is given, the key's public part(s) will be filled in from C{data} is given, the key's public part(s) will be filled in from
the string. the string.
:param .Message msg: @param msg: an optional SSH L{Message} containing a public key of this
an optional SSH `.Message` containing a public key of this type. type.
:param str data: an optional string containing a public key of this type @type msg: L{Message}
@param data: an optional string containing a public key of this type
@type data: str
:raises SSHException: @raise SSHException: if a key cannot be created from the C{data} or
if a key cannot be created from the ``data`` or ``msg`` given, or C{msg} given, or no key was passed in.
no key was passed in.
""" """
pass pass
def asbytes(self):
"""
Return a string of an SSH `.Message` made up of the public part(s) of
this key. This string is suitable for passing to `__init__` to
re-create the key object later.
"""
return bytes()
def __str__(self): def __str__(self):
return self.asbytes() """
Return a string of an SSH L{Message} made up of the public part(s) of
this key. This string is suitable for passing to L{__init__} to
re-create the key object later.
@return: string representation of an SSH key message.
@rtype: str
"""
return ''
# noinspection PyUnresolvedReferences
def __cmp__(self, other): def __cmp__(self, other):
""" """
Compare this key to another. Returns 0 if this key is equivalent to Compare this key to another. Returns 0 if this key is equivalent to
@ -80,24 +82,24 @@ class PKey (object):
of the key are compared, so a public key will compare equal to its of the key are compared, so a public key will compare equal to its
corresponding private key. corresponding private key.
:param .Pkey other: key to compare to. @param other: key to compare to.
@type other: L{PKey}
@return: 0 if the two keys are equivalent, non-0 otherwise.
@rtype: int
""" """
hs = hash(self) hs = hash(self)
ho = hash(other) ho = hash(other)
if hs != ho: if hs != ho:
return cmp(hs, ho) return cmp(hs, ho)
return cmp(self.asbytes(), other.asbytes()) return cmp(str(self), str(other))
def __eq__(self, other):
return hash(self) == hash(other)
def get_name(self): def get_name(self):
""" """
Return the name of this private key implementation. Return the name of this private key implementation.
:return: @return: name of this private key type, in SSH terminology (for
name of this private key type, in SSH terminology, as a `str` (for example, C{"ssh-rsa"}).
example, ``"ssh-rsa"``). @rtype: str
""" """
return '' return ''
@ -106,14 +108,18 @@ class PKey (object):
Return the number of significant bits in this key. This is useful Return the number of significant bits in this key. This is useful
for judging the relative security of a key. for judging the relative security of a key.
:return: bits in the key (as an `int`) @return: bits in the key.
@rtype: int
""" """
return 0 return 0
def can_sign(self): def can_sign(self):
""" """
Return ``True`` if this key has the private part necessary for signing Return C{True} if this key has the private part necessary for signing
data. data.
@return: C{True} if this is a private key.
@rtype: bool
""" """
return False return False
@ -122,11 +128,11 @@ class PKey (object):
Return an MD5 fingerprint of the public part of this key. Nothing Return an MD5 fingerprint of the public part of this key. Nothing
secret is revealed. secret is revealed.
:return: @return: a 16-byte string (binary) of the MD5 fingerprint, in SSH
a 16-byte `string <str>` (binary) of the MD5 fingerprint, in SSH
format. format.
@rtype: str
""" """
return md5(self.asbytes()).digest() return MD5.new(str(self)).digest()
def get_base64(self): def get_base64(self):
""" """
@ -134,50 +140,61 @@ class PKey (object):
secret is revealed. This format is compatible with that used to store secret is revealed. This format is compatible with that used to store
public key files or recognized host keys. public key files or recognized host keys.
:return: a base64 `string <str>` containing the public part of the key. @return: a base64 string containing the public part of the key.
@rtype: str
""" """
return u(encodebytes(self.asbytes())).replace('\n', '') return base64.encodestring(str(self)).replace('\n', '')
def sign_ssh_data(self, data): def sign_ssh_data(self, rng, data):
""" """
Sign a blob of data with this private key, and return a `.Message` Sign a blob of data with this private key, and return a L{Message}
representing an SSH signature message. representing an SSH signature message.
:param str data: the data to sign. @param rng: a secure random number generator.
:return: an SSH signature `message <.Message>`. @type rng: L{Crypto.Util.rng.RandomPool}
@param data: the data to sign.
@type data: str
@return: an SSH signature message.
@rtype: L{Message}
""" """
return bytes() return ''
def verify_ssh_sig(self, data, msg): def verify_ssh_sig(self, data, msg):
""" """
Given a blob of data, and an SSH message representing a signature of Given a blob of data, and an SSH message representing a signature of
that data, verify that it was signed with this key. that data, verify that it was signed with this key.
:param str data: the data that was signed. @param data: the data that was signed.
:param .Message msg: an SSH signature message @type data: str
:return: @param msg: an SSH signature message
``True`` if the signature verifies correctly; ``False`` otherwise. @type msg: L{Message}
@return: C{True} if the signature verifies correctly; C{False}
otherwise.
@rtype: boolean
""" """
return False return False
def from_private_key_file(cls, filename, password=None): def from_private_key_file(cls, filename, password=None):
""" """
Create a key object by reading a private key file. If the private Create a key object by reading a private key file. If the private
key is encrypted and ``password`` is not ``None``, the given password key is encrypted and C{password} is not C{None}, the given password
will be used to decrypt the key (otherwise `.PasswordRequiredException` will be used to decrypt the key (otherwise L{PasswordRequiredException}
is thrown). Through the magic of Python, this factory method will is thrown). Through the magic of python, this factory method will
exist in all subclasses of PKey (such as `.RSAKey` or `.DSSKey`), but exist in all subclasses of PKey (such as L{RSAKey} or L{DSSKey}), but
is useless on the abstract PKey class. is useless on the abstract PKey class.
:param str filename: name of the file to read @param filename: name of the file to read
:param str password: an optional password to use to decrypt the key file, @type filename: str
@param password: an optional password to use to decrypt the key file,
if it's encrypted if it's encrypted
:return: a new `.PKey` based on the given private key @type password: str
@return: a new key object based on the given private key
@rtype: L{PKey}
:raises IOError: if there was an error reading the file @raise IOError: if there was an error reading the file
:raises PasswordRequiredException: if the private key file is @raise PasswordRequiredException: if the private key file is
encrypted, and ``password`` is ``None`` encrypted, and C{password} is C{None}
:raises SSHException: if the key file is invalid @raise SSHException: if the key file is invalid
""" """
key = cls(filename=filename, password=password) key = cls(filename=filename, password=password)
return key return key
@ -186,19 +203,22 @@ class PKey (object):
def from_private_key(cls, file_obj, password=None): def from_private_key(cls, file_obj, password=None):
""" """
Create a key object by reading a private key from a file (or file-like) Create a key object by reading a private key from a file (or file-like)
object. If the private key is encrypted and ``password`` is not ``None``, object. If the private key is encrypted and C{password} is not C{None},
the given password will be used to decrypt the key (otherwise the given password will be used to decrypt the key (otherwise
`.PasswordRequiredException` is thrown). L{PasswordRequiredException} is thrown).
:param file file_obj: the file to read from @param file_obj: the file to read from
:param str password: @type file_obj: file
an optional password to use to decrypt the key, if it's encrypted @param password: an optional password to use to decrypt the key, if it's
:return: a new `.PKey` based on the given private key encrypted
@type password: str
@return: a new key object based on the given private key
@rtype: L{PKey}
:raises IOError: if there was an error reading the key @raise IOError: if there was an error reading the key
:raises PasswordRequiredException: if the private key file is encrypted, @raise PasswordRequiredException: if the private key file is encrypted,
and ``password`` is ``None`` and C{password} is C{None}
:raises SSHException: if the key file is invalid @raise SSHException: if the key file is invalid
""" """
key = cls(file_obj=file_obj, password=password) key = cls(file_obj=file_obj, password=password)
return key return key
@ -207,52 +227,59 @@ class PKey (object):
def write_private_key_file(self, filename, password=None): def write_private_key_file(self, filename, password=None):
""" """
Write private key contents into a file. If the password is not Write private key contents into a file. If the password is not
``None``, the key is encrypted before writing. C{None}, the key is encrypted before writing.
:param str filename: name of the file to write @param filename: name of the file to write
:param str password: @type filename: str
an optional password to use to encrypt the key file @param password: an optional password to use to encrypt the key file
@type password: str
:raises IOError: if there was an error writing the file @raise IOError: if there was an error writing the file
:raises SSHException: if the key is invalid @raise SSHException: if the key is invalid
""" """
raise Exception('Not implemented in PKey') raise Exception('Not implemented in PKey')
def write_private_key(self, file_obj, password=None): def write_private_key(self, file_obj, password=None):
""" """
Write private key contents into a file (or file-like) object. If the Write private key contents into a file (or file-like) object. If the
password is not ``None``, the key is encrypted before writing. password is not C{None}, the key is encrypted before writing.
:param file file_obj: the file object to write into @param file_obj: the file object to write into
:param str password: an optional password to use to encrypt the key @type file_obj: file
@param password: an optional password to use to encrypt the key
@type password: str
:raises IOError: if there was an error writing to the file @raise IOError: if there was an error writing to the file
:raises SSHException: if the key is invalid @raise SSHException: if the key is invalid
""" """
raise Exception('Not implemented in PKey') raise Exception('Not implemented in PKey')
def _read_private_key_file(self, tag, filename, password=None): def _read_private_key_file(self, tag, filename, password=None):
""" """
Read an SSH2-format private key file, looking for a string of the type Read an SSH2-format private key file, looking for a string of the type
``"BEGIN xxx PRIVATE KEY"`` for some ``xxx``, base64-decode the text we C{"BEGIN xxx PRIVATE KEY"} for some C{xxx}, base64-decode the text we
find, and return it as a string. If the private key is encrypted and find, and return it as a string. If the private key is encrypted and
``password`` is not ``None``, the given password will be used to decrypt C{password} is not C{None}, the given password will be used to decrypt
the key (otherwise `.PasswordRequiredException` is thrown). the key (otherwise L{PasswordRequiredException} is thrown).
:param str tag: ``"RSA"`` or ``"DSA"``, the tag used to mark the data block. @param tag: C{"RSA"} or C{"DSA"}, the tag used to mark the data block.
:param str filename: name of the file to read. @type tag: str
:param str password: @param filename: name of the file to read.
an optional password to use to decrypt the key file, if it's @type filename: str
encrypted. @param password: an optional password to use to decrypt the key file,
:return: data blob (`str`) that makes up the private key. if it's encrypted.
@type password: str
@return: data blob that makes up the private key.
@rtype: str
:raises IOError: if there was an error reading the file. @raise IOError: if there was an error reading the file.
:raises PasswordRequiredException: if the private key file is @raise PasswordRequiredException: if the private key file is
encrypted, and ``password`` is ``None``. encrypted, and C{password} is C{None}.
:raises SSHException: if the key file is invalid. @raise SSHException: if the key file is invalid.
""" """
with open(filename, 'r') as f: f = open(filename, 'r')
data = self._read_private_key(tag, f, password) data = self._read_private_key(tag, f, password)
f.close()
return data return data
def _read_private_key(self, tag, f, password=None): def _read_private_key(self, tag, f, password=None):
@ -277,7 +304,7 @@ class PKey (object):
end += 1 end += 1
# if we trudged to the end of the file, just try to cope. # if we trudged to the end of the file, just try to cope.
try: try:
data = decodebytes(b(''.join(lines[start:end]))) data = base64.decodestring(six.b(''.join(lines[start:end])))
except base64.binascii.Error as e: except base64.binascii.Error as e:
raise SSHException('base64 decoding error: ' + str(e)) raise SSHException('base64 decoding error: ' + str(e))
if 'proc-type' not in headers: if 'proc-type' not in headers:
@ -289,7 +316,7 @@ class PKey (object):
try: try:
encryption_type, saltstr = headers['dek-info'].split(',') encryption_type, saltstr = headers['dek-info'].split(',')
except: except:
raise SSHException("Can't parse DEK-info in private key file") raise SSHException('Can\'t parse DEK-info in private key file')
if encryption_type not in self._CIPHER_TABLE: if encryption_type not in self._CIPHER_TABLE:
raise SSHException('Unknown private key cipher "%s"' % encryption_type) raise SSHException('Unknown private key cipher "%s"' % encryption_type)
# if no password was passed in, raise an exception pointing out that we need one # if no password was passed in, raise an exception pointing out that we need one
@ -298,8 +325,8 @@ class PKey (object):
cipher = self._CIPHER_TABLE[encryption_type]['cipher'] cipher = self._CIPHER_TABLE[encryption_type]['cipher']
keysize = self._CIPHER_TABLE[encryption_type]['keysize'] keysize = self._CIPHER_TABLE[encryption_type]['keysize']
mode = self._CIPHER_TABLE[encryption_type]['mode'] mode = self._CIPHER_TABLE[encryption_type]['mode']
salt = unhexlify(b(saltstr)) salt = unhexlify(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) return cipher.new(key, mode, salt).decrypt(data)
def _write_private_key_file(self, tag, filename, data, password=None): def _write_private_key_file(self, tag, filename, data, password=None):
@ -309,42 +336,47 @@ class PKey (object):
a trivially-encoded format (base64) which is completely insecure. If a trivially-encoded format (base64) which is completely insecure. If
a password is given, DES-EDE3-CBC is used. a password is given, DES-EDE3-CBC is used.
:param str tag: ``"RSA"`` or ``"DSA"``, the tag used to mark the data block. @param tag: C{"RSA"} or C{"DSA"}, the tag used to mark the data block.
:param file filename: name of the file to write. @type tag: str
:param str data: data blob that makes up the private key. @param filename: name of the file to write.
:param str password: an optional password to use to encrypt the file. @type filename: str
@param data: data blob that makes up the private key.
@type data: str
@param password: an optional password to use to encrypt the file.
@type password: str
:raises IOError: if there was an error writing the file. @raise IOError: if there was an error writing the file.
""" """
with open(filename, 'w', o600) as f: f = open(filename, 'w', 0o600)
# grrr... the mode doesn't always take hold # grrr... the mode doesn't always take hold
os.chmod(filename, o600) os.chmod(filename, 0o600)
self._write_private_key(tag, f, data, password) self._write_private_key(tag, f, data, password)
f.close()
def _write_private_key(self, tag, f, data, password=None): def _write_private_key(self, tag, f, data, password=None):
f.write('-----BEGIN %s PRIVATE KEY-----\n' % tag) f.write('-----BEGIN %s PRIVATE KEY-----\n' % tag)
if password is not None: if password is not None:
# since we only support one cipher here, use it # since we only support one cipher here, use it
cipher_name = list(self._CIPHER_TABLE.keys())[0] cipher_name = self._CIPHER_TABLE.keys()[0]
cipher = self._CIPHER_TABLE[cipher_name]['cipher'] cipher = self._CIPHER_TABLE[cipher_name]['cipher']
keysize = self._CIPHER_TABLE[cipher_name]['keysize'] keysize = self._CIPHER_TABLE[cipher_name]['keysize']
blocksize = self._CIPHER_TABLE[cipher_name]['blocksize'] blocksize = self._CIPHER_TABLE[cipher_name]['blocksize']
mode = self._CIPHER_TABLE[cipher_name]['mode'] mode = self._CIPHER_TABLE[cipher_name]['mode']
salt = os.urandom(16) salt = rng.read(8)
key = util.generate_key_bytes(md5, salt, password, keysize) key = util.generate_key_bytes(MD5, salt, password, keysize)
if len(data) % blocksize != 0: if len(data) % blocksize != 0:
n = blocksize - len(data) % blocksize n = blocksize - len(data) % blocksize
#data += os.urandom(n) #data += rng.read(n)
# that would make more sense ^, but it confuses openssh. # that would make more sense ^, but it confuses openssh.
data += zero_byte * n data += '\0' * n
data = cipher.new(key, mode, salt).encrypt(data) data = cipher.new(key, mode, salt).encrypt(data)
f.write('Proc-Type: 4,ENCRYPTED\n') f.write('Proc-Type: 4,ENCRYPTED\n')
f.write('DEK-Info: %s,%s\n' % (cipher_name, u(hexlify(salt)).upper())) f.write('DEK-Info: %s,%s\n' % (cipher_name, hexlify(salt).upper()))
f.write('\n') f.write('\n')
s = u(encodebytes(data)) s = base64.encodestring(data)
# re-wrap to 64-char lines # re-wrap to 64-char lines
s = ''.join(s.split('\n')) s = ''.join(s.split('\n'))
s = '\n'.join([s[i: i + 64] for i in range(0, len(s), 64)]) s = '\n'.join([s[i : i+64] for i in range(0, len(s), 64)])
f.write(s) f.write(s)
f.write('\n') f.write('\n')
f.write('-----END %s PRIVATE KEY-----\n' % tag) f.write('-----END %s PRIVATE KEY-----\n' % tag)

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -20,17 +20,36 @@
Utility functions for dealing with primes. Utility functions for dealing with primes.
""" """
import os import six
if six.PY3:
long = lambda x: int(x)
from Crypto.Util import number
from paramiko import util from paramiko import util
from paramiko.py3compat import byte_mask, long
from paramiko.ssh_exception import SSHException from paramiko.ssh_exception import SSHException
def _roll_random(n): def _generate_prime(bits, rng):
"""returns a random # from 0 to N-1""" "primtive attempt at prime generation"
bits = util.bit_length(n - 1) hbyte_mask = pow(2, bits % 8) - 1
byte_count = (bits + 7) // 8 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 = chr(ord(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):
"returns a random # from 0 to N-1"
bits = util.bit_length(n-1)
bytes = (bits + 7) // 8
hbyte_mask = pow(2, bits % 8) - 1 hbyte_mask = pow(2, bits % 8) - 1
# so here's the plan: # so here's the plan:
@ -40,9 +59,9 @@ def _roll_random(n):
# fits, so i can't guarantee that this loop will ever finish, but the odds # fits, so i can't guarantee that this loop will ever finish, but the odds
# of it looping forever should be infinitesimal. # of it looping forever should be infinitesimal.
while True: while True:
x = os.urandom(byte_count) x = rng.read(bytes)
if hbyte_mask > 0: if hbyte_mask > 0:
x = byte_mask(x[0], hbyte_mask) + x[1:] x = chr(ord(x[0]) & hbyte_mask) + x[1:]
num = util.inflate_long(x, 1) num = util.inflate_long(x, 1)
if num < n: if num < n:
break break
@ -55,10 +74,11 @@ class ModulusPack (object):
on systems that have such a file. on systems that have such a file.
""" """
def __init__(self): def __init__(self, rpool):
# pack is a hash of: bits -> [ (generator, modulus) ... ] # pack is a hash of: bits -> [ (generator, modulus) ... ]
self.pack = {} self.pack = {}
self.discarded = [] self.discarded = []
self.rng = rpool
def _parse_modulus(self, line): def _parse_modulus(self, line):
timestamp, mod_type, tests, tries, size, generator, modulus = line.split() timestamp, mod_type, tests, tries, size, generator, modulus = line.split()
@ -92,27 +112,29 @@ class ModulusPack (object):
def read_file(self, filename): def read_file(self, filename):
""" """
:raises IOError: passed from any file operations that fail. @raise IOError: passed from any file operations that fail.
""" """
self.pack = {} self.pack = {}
with open(filename, 'r') as f: f = open(filename, 'r')
for line in f: for line in f:
line = line.strip() line = line.strip()
if (len(line) == 0) or (line[0] == '#'): if (len(line) == 0) or (line[0] == '#'):
continue continue
try: try:
self._parse_modulus(line) self._parse_modulus(line)
except: except:
continue continue
f.close()
def get_modulus(self, min, prefer, max): def get_modulus(self, min, prefer, max):
bitsizes = sorted(self.pack.keys()) bitsizes = self.pack.keys()
bitsizes.sort()
if len(bitsizes) == 0: if len(bitsizes) == 0:
raise SSHException('no moduli available') raise SSHException('no moduli available')
good = -1 good = -1
# find nearest bitsize >= preferred # find nearest bitsize >= preferred
for b in bitsizes: for b in bitsizes:
if (b >= prefer) and (b < max) and (b < good or good == -1): if (b >= prefer) and (b < max) and ((b < good) or (good == -1)):
good = b good = b
# if that failed, find greatest bitsize >= min # if that failed, find greatest bitsize >= min
if good == -1: if good == -1:
@ -128,5 +150,5 @@ class ModulusPack (object):
if min > good: if min > good:
good = bitsizes[-1] good = bitsizes[-1]
# now pick a random modulus of this bitsize # now pick a random modulus of this bitsize
n = _roll_random(len(self.pack[good])) n = _roll_random(self.rng, len(self.pack[good]))
return self.pack[good][n] return self.pack[good][n]

View File

@ -16,14 +16,14 @@
# along with Paramiko; if not, write to the Free Software Foundation, Inc., # along with Paramiko; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
"""
L{ProxyCommand}.
"""
from datetime import datetime
import os import os
from shlex import split as shlsplit from shlex import split as shlsplit
import signal import signal
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
from select import select
import socket
from paramiko.ssh_exception import ProxyCommandFailure from paramiko.ssh_exception import ProxyCommandFailure
@ -33,29 +33,29 @@ class ProxyCommand(object):
Wraps a subprocess running ProxyCommand-driven programs. Wraps a subprocess running ProxyCommand-driven programs.
This class implements a the socket-like interface needed by the This class implements a the socket-like interface needed by the
`.Transport` and `.Packetizer` classes. Using this class instead of a L{Transport} and L{Packetizer} classes. Using this class instead of a
regular socket makes it possible to talk with a Popen'd command that will regular socket makes it possible to talk with a Popen'd command that will
proxy traffic between the client and a server hosted in another machine. proxy traffic between the client and a server hosted in another machine.
""" """
def __init__(self, command_line): def __init__(self, command_line):
""" """
Create a new CommandProxy instance. The instance created by this Create a new CommandProxy instance. The instance created by this
class can be passed as an argument to the `.Transport` class. class can be passed as an argument to the L{Transport} class.
:param str command_line: @param command_line: the command that should be executed and
the command that should be executed and used as the proxy. used as the proxy.
@type command_line: str
""" """
self.cmd = shlsplit(command_line) self.cmd = shlsplit(command_line)
self.process = Popen(self.cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) self.process = Popen(self.cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
self.timeout = None
self.buffer = []
def send(self, content): def send(self, content):
""" """
Write the content received from the SSH client to the standard Write the content received from the SSH client to the standard
input of the forked command. input of the forked command.
:param str content: string to be sent to the forked command @param content: string to be sent to the forked command
@type content: str
""" """
try: try:
self.process.stdin.write(content) self.process.stdin.write(content)
@ -64,42 +64,28 @@ class ProxyCommand(object):
# died and we can't proceed. The best option here is to # died and we can't proceed. The best option here is to
# raise an exception informing the user that the informed # raise an exception informing the user that the informed
# ProxyCommand is not working. # ProxyCommand is not working.
raise ProxyCommandFailure(' '.join(self.cmd), e.strerror) raise BadProxyCommand(' '.join(self.cmd), e.strerror)
return len(content) return len(content)
def recv(self, size): def recv(self, size):
""" """
Read from the standard output of the forked program. Read from the standard output of the forked program.
:param int size: how many chars should be read @param size: how many chars should be read
@type size: int
:return: the length of the read content, as an `int` @return: the length of the read content
@rtype: int
""" """
try: try:
start = datetime.now() return os.read(self.process.stdout.fileno(), size)
while len(self.buffer) < size:
if self.timeout is not None:
elapsed = (datetime.now() - start).microseconds
timeout = self.timeout * 1000 * 1000 # to microseconds
if elapsed >= timeout:
raise socket.timeout()
r, w, x = select([self.process.stdout], [], [], 0.0)
if r and r[0] == self.process.stdout:
b = os.read(self.process.stdout.fileno(), 1)
# Store in class-level buffer for persistence across
# timeouts; this makes us act more like a real socket
# (where timeouts don't actually drop data.)
self.buffer.append(b)
result = ''.join(self.buffer)
self.buffer = []
return result
except socket.timeout:
raise # socket.timeout is a subclass of IOError
except IOError as e: except IOError as e:
raise ProxyCommandFailure(' '.join(self.cmd), e.strerror) raise BadProxyCommand(' '.join(self.cmd), e.strerror)
def close(self): def close(self):
os.kill(self.process.pid, signal.SIGTERM) os.kill(self.process.pid, signal.SIGTERM)
def settimeout(self, timeout): def settimeout(self, timeout):
self.timeout = timeout # Timeouts are meaningless for this implementation, but are part of the
# spec, so must be present.
pass

View File

@ -1,162 +0,0 @@
import sys
import base64
__all__ = ['PY2', 'string_types', 'integer_types', 'text_type', 'bytes_types', 'bytes', 'long', 'input',
'decodebytes', 'encodebytes', 'bytestring', 'byte_ord', 'byte_chr', 'byte_mask',
'b', 'u', 'b2s', 'StringIO', 'BytesIO', 'is_callable', 'MAXSIZE', 'next']
PY2 = sys.version_info[0] < 3
if PY2:
string_types = basestring
text_type = unicode
bytes_types = str
bytes = str
integer_types = (int, long)
long = long
input = raw_input
decodebytes = base64.decodestring
encodebytes = base64.encodestring
def bytestring(s): # NOQA
if isinstance(s, unicode):
return s.encode('utf-8')
return s
byte_ord = ord # NOQA
byte_chr = chr # NOQA
def byte_mask(c, mask):
return chr(ord(c) & mask)
def b(s, encoding='utf8'): # NOQA
"""cast unicode or bytes to bytes"""
if isinstance(s, str):
return s
elif isinstance(s, unicode):
return s.encode(encoding)
else:
raise TypeError("Expected unicode or bytes, got %r" % s)
def u(s, encoding='utf8'): # NOQA
"""cast bytes or unicode to unicode"""
if isinstance(s, str):
return s.decode(encoding)
elif isinstance(s, unicode):
return s
else:
raise TypeError("Expected unicode or bytes, got %r" % s)
def b2s(s):
return s
try:
import cStringIO
StringIO = cStringIO.StringIO # NOQA
except ImportError:
import StringIO
StringIO = StringIO.StringIO # NOQA
BytesIO = StringIO
def is_callable(c): # NOQA
return callable(c)
def get_next(c): # NOQA
return c.next
def next(c):
return c.next()
# It's possible to have sizeof(long) != sizeof(Py_ssize_t).
class X(object):
def __len__(self):
return 1 << 31
try:
len(X())
except OverflowError:
# 32-bit
MAXSIZE = int((1 << 31) - 1) # NOQA
else:
# 64-bit
MAXSIZE = int((1 << 63) - 1) # NOQA
del X
else:
import collections
import struct
string_types = str
text_type = str
bytes = bytes
bytes_types = bytes
integer_types = int
class long(int):
pass
input = input
decodebytes = base64.decodebytes
encodebytes = base64.encodebytes
def bytestring(s):
return s
def byte_ord(c):
# In case we're handed a string instead of an int.
if not isinstance(c, int):
c = ord(c)
return c
def byte_chr(c):
assert isinstance(c, int)
return struct.pack('B', c)
def byte_mask(c, mask):
assert isinstance(c, int)
return struct.pack('B', c & mask)
def b(s, encoding='utf8'):
"""cast unicode or bytes to bytes"""
if isinstance(s, bytes):
return s
elif isinstance(s, str):
return s.encode(encoding)
else:
raise TypeError("Expected unicode or bytes, got %r" % s)
def u(s, encoding='utf8'):
"""cast bytes or unicode to unicode"""
if isinstance(s, bytes):
return s.decode(encoding)
elif isinstance(s, str):
return s
else:
raise TypeError("Expected unicode or bytes, got %r" % s)
def b2s(s):
return s.decode() if isinstance(s, bytes) else s
import io
StringIO = io.StringIO # NOQA
BytesIO = io.BytesIO # NOQA
def is_callable(c):
return isinstance(c, collections.Callable)
def get_next(c):
return c.__next__
next = next
MAXSIZE = sys.maxsize # NOQA

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -28,13 +28,13 @@ class ResourceManager (object):
A registry of objects and resources that should be closed when those A registry of objects and resources that should be closed when those
objects are deleted. objects are deleted.
This is meant to be a safer alternative to Python's ``__del__`` method, This is meant to be a safer alternative to python's C{__del__} method,
which can cause reference cycles to never be collected. Objects registered which can cause reference cycles to never be collected. Objects registered
with the ResourceManager can be collected but still free resources when with the ResourceManager can be collected but still free resources when
they die. they die.
Resources are registered using `register`, and when an object is garbage Resources are registered using L{register}, and when an object is garbage
collected, each registered resource is closed by having its ``close()`` collected, each registered resource is closed by having its C{close()}
method called. Multiple resources may be registered per object, but a method called. Multiple resources may be registered per object, but a
resource will only be closed once, even if multiple objects register it. resource will only be closed once, even if multiple objects register it.
(The last object to register it wins.) (The last object to register it wins.)
@ -47,13 +47,14 @@ class ResourceManager (object):
""" """
Register a resource to be closed with an object is collected. Register a resource to be closed with an object is collected.
When the given ``obj`` is garbage-collected by the Python interpreter, When the given C{obj} is garbage-collected by the python interpreter,
the ``resource`` will be closed by having its ``close()`` method called. the C{resource} will be closed by having its C{close()} method called.
Any exceptions are ignored. Any exceptions are ignored.
:param object obj: the object to track @param obj: the object to track
:param object resource: @type obj: object
the resource to close when the object is collected @param resource: the resource to close when the object is collected
@type resource: object
""" """
def callback(ref): def callback(ref):
try: try:

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -17,24 +17,24 @@
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
""" """
RSA keys. L{RSAKey}
""" """
import os import six
from hashlib import sha1 if six.PY3:
long = lambda x: int(x)
from Crypto.PublicKey import RSA from Crypto.PublicKey import RSA
from Crypto.Hash import SHA, MD5
from Crypto.Cipher import DES3
from paramiko.common import *
from paramiko import util from paramiko import util
from paramiko.common import max_byte, zero_byte, one_byte
from paramiko.message import Message from paramiko.message import Message
from paramiko.ber import BER, BERException from paramiko.ber import BER, BERException
from paramiko.pkey import PKey from paramiko.pkey import PKey
from paramiko.py3compat import long
from paramiko.ssh_exception import SSHException from paramiko.ssh_exception import SSHException
SHA1_DIGESTINFO = b'\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14'
class RSAKey (PKey): class RSAKey (PKey):
""" """
@ -61,21 +61,18 @@ class RSAKey (PKey):
else: else:
if msg is None: if msg is None:
raise SSHException('Key object may not be empty') raise SSHException('Key object may not be empty')
if msg.get_text() != 'ssh-rsa': if msg.get_string() != 'ssh-rsa':
raise SSHException('Invalid key') raise SSHException('Invalid key')
self.e = msg.get_mpint() self.e = msg.get_mpint()
self.n = msg.get_mpint() self.n = msg.get_mpint()
self.size = util.bit_length(self.n) self.size = util.bit_length(self.n)
def asbytes(self): def __str__(self):
m = Message() m = Message()
m.add_string('ssh-rsa') m.add_string('ssh-rsa')
m.add_mpint(self.e) m.add_mpint(self.e)
m.add_mpint(self.n) m.add_mpint(self.n)
return m.asbytes() return str(m)
def __str__(self):
return self.asbytes()
def __hash__(self): def __hash__(self):
h = hash(self.get_name()) h = hash(self.get_name())
@ -92,42 +89,42 @@ class RSAKey (PKey):
def can_sign(self): def can_sign(self):
return self.d is not None return self.d is not None
def sign_ssh_data(self, data): def sign_ssh_data(self, rpool, data):
digest = sha1(data).digest() digest = SHA.new(data).digest()
rsa = RSA.construct((long(self.n), long(self.e), long(self.d))) rsa = RSA.construct((long(self.n), long(self.e), long(self.d)))
sig = util.deflate_long(rsa.sign(self._pkcs1imify(digest), bytes())[0], 0) sig = util.deflate_long(rsa.sign(self._pkcs1imify(digest), '')[0], 0)
m = Message() m = Message()
m.add_string('ssh-rsa') m.add_string('ssh-rsa')
m.add_string(sig) m.add_string(sig)
return m return m
def verify_ssh_sig(self, data, msg): def verify_ssh_sig(self, data, msg):
if msg.get_text() != 'ssh-rsa': if msg.get_string() != 'ssh-rsa':
return False return False
sig = util.inflate_long(msg.get_binary(), True) sig = util.inflate_long(msg.get_string(), True)
# verify the signature by SHA'ing the data and encrypting it using the # verify the signature by SHA'ing the data and encrypting it using the
# public key. some wackiness ensues where we "pkcs1imify" the 20-byte # public key. some wackiness ensues where we "pkcs1imify" the 20-byte
# hash into a string as long as the RSA key. # hash into a string as long as the RSA key.
hash_obj = util.inflate_long(self._pkcs1imify(sha1(data).digest()), True) hash_obj = util.inflate_long(self._pkcs1imify(SHA.new(data).digest()), True)
rsa = RSA.construct((long(self.n), long(self.e))) rsa = RSA.construct((long(self.n), long(self.e)))
return rsa.verify(hash_obj, (sig,)) return rsa.verify(hash_obj, (sig,))
def _encode_key(self): def _encode_key(self):
if (self.p is None) or (self.q is None): if (self.p is None) or (self.q is None):
raise SSHException('Not enough key info to write private key file') raise SSHException('Not enough key info to write private key file')
keylist = [0, self.n, self.e, self.d, self.p, self.q, keylist = [ 0, self.n, self.e, self.d, self.p, self.q,
self.d % (self.p - 1), self.d % (self.q - 1), self.d % (self.p - 1), self.d % (self.q - 1),
util.mod_inverse(self.q, self.p)] util.mod_inverse(self.q, self.p) ]
try: try:
b = BER() b = BER()
b.encode(keylist) b.encode(keylist)
except BERException: except BERException:
raise SSHException('Unable to create ber encoding of key') raise SSHException('Unable to create ber encoding of key')
return b.asbytes() return str(b)
def write_private_key_file(self, filename, password=None): def write_private_key_file(self, filename, password=None):
self._write_private_key_file('RSA', filename, self._encode_key(), password) self._write_private_key_file('RSA', filename, self._encode_key(), password)
def write_private_key(self, file_obj, password=None): def write_private_key(self, file_obj, password=None):
self._write_private_key('RSA', file_obj, self._encode_key(), password) self._write_private_key('RSA', file_obj, self._encode_key(), password)
@ -136,13 +133,15 @@ class RSAKey (PKey):
Generate a new private RSA key. This factory function can be used to Generate a new private RSA key. This factory function can be used to
generate a new host key or authentication key. generate a new host key or authentication key.
:param int bits: number of bits the generated key should be. @param bits: number of bits the generated key should be.
:param function progress_func: @type bits: int
an optional function to call at key points in key generation (used @param progress_func: an optional function to call at key points in
by ``pyCrypto.PublicKey``). key generation (used by C{pyCrypto.PublicKey}).
:return: new `.RSAKey` private key @type progress_func: function
@return: new private key
@rtype: L{RSAKey}
""" """
rsa = RSA.generate(bits, os.urandom, progress_func) rsa = RSA.generate(bits, rng.read, progress_func)
key = RSAKey(vals=(rsa.e, rsa.n)) key = RSAKey(vals=(rsa.e, rsa.n))
key.d = rsa.d key.d = rsa.d
key.p = rsa.p key.p = rsa.p
@ -150,25 +149,28 @@ class RSAKey (PKey):
return key return key
generate = staticmethod(generate) generate = staticmethod(generate)
### internals... ### internals...
def _pkcs1imify(self, data): def _pkcs1imify(self, data):
""" """
turn a 20-byte SHA1 hash into a blob of data as large as the key's N, turn a 20-byte SHA1 hash into a blob of data as large as the key's N,
using PKCS1's \"emsa-pkcs1-v1_5\" encoding. totally bizarre. using PKCS1's \"emsa-pkcs1-v1_5\" encoding. totally bizarre.
""" """
SHA1_DIGESTINFO = '\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14'
size = len(util.deflate_long(self.n, 0)) size = len(util.deflate_long(self.n, 0))
filler = max_byte * (size - len(SHA1_DIGESTINFO) - len(data) - 3) filler = '\xff' * (size - len(SHA1_DIGESTINFO) - len(data) - 3)
return zero_byte + one_byte + filler + zero_byte + SHA1_DIGESTINFO + data return '\x00\x01' + filler + '\x00' + SHA1_DIGESTINFO + data
def _from_private_key_file(self, filename, password): def _from_private_key_file(self, filename, password):
data = self._read_private_key_file('RSA', filename, password) data = self._read_private_key_file('RSA', filename, password)
self._decode_key(data) self._decode_key(data)
def _from_private_key(self, file_obj, password): def _from_private_key(self, file_obj, password):
data = self._read_private_key('RSA', file_obj, password) data = self._read_private_key('RSA', file_obj, password)
self._decode_key(data) self._decode_key(data)
def _decode_key(self, data): def _decode_key(self, data):
# private key file contains: # private key file contains:
# RSAPrivateKey = { version = 0, n, e, d, p, q, d mod p-1, d mod q-1, q**-1 mod p } # RSAPrivateKey = { version = 0, n, e, d, p, q, d mod p-1, d mod q-1, q**-1 mod p }

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -17,21 +17,64 @@
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
""" """
`.ServerInterface` is an interface to override for server support. L{ServerInterface} is an interface to override for server support.
""" """
import threading import threading
import six
from paramiko.common import *
from paramiko import util from paramiko import util
from paramiko.common import DEBUG, ERROR, OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED, AUTH_FAILED
from paramiko.py3compat import string_types
class InteractiveQuery (object):
"""
A query (set of prompts) for a user during interactive authentication.
"""
def __init__(self, name='', instructions='', *prompts):
"""
Create a new interactive query to send to the client. The name and
instructions are optional, but are generally displayed to the end
user. A list of prompts may be included, or they may be added via
the L{add_prompt} method.
@param name: name of this query
@type name: str
@param instructions: user instructions (usually short) about this query
@type instructions: str
@param prompts: one or more authentication prompts
@type prompts: str
"""
self.name = name
self.instructions = instructions
self.prompts = []
for x in prompts:
if isinstance(x, (six.binary_type, six.text_type)):
self.add_prompt(x)
else:
self.add_prompt(x[0], x[1])
def add_prompt(self, prompt, echo=True):
"""
Add a prompt to this query. The prompt should be a (reasonably short)
string. Multiple prompts can be added to the same query.
@param prompt: the user prompt
@type prompt: str
@param echo: C{True} (default) if the user's response should be echoed;
C{False} if not (for a password or similar)
@type echo: bool
"""
self.prompts.append((prompt, echo))
class ServerInterface (object): class ServerInterface (object):
""" """
This class defines an interface for controlling the behavior of Paramiko This class defines an interface for controlling the behavior of paramiko
in server mode. in server mode.
Methods on this class are called from Paramiko's primary thread, so you Methods on this class are called from paramiko's primary thread, so you
shouldn't do too much work in them. (Certainly nothing that blocks or shouldn't do too much work in them. (Certainly nothing that blocks or
sleeps.) sleeps.)
""" """
@ -39,7 +82,7 @@ class ServerInterface (object):
def check_channel_request(self, kind, chanid): def check_channel_request(self, kind, chanid):
""" """
Determine if a channel request of a given type will be granted, and Determine if a channel request of a given type will be granted, and
return ``OPEN_SUCCEEDED`` or an error code. This method is return C{OPEN_SUCCEEDED} or an error code. This method is
called in server mode when the client requests a channel, after called in server mode when the client requests a channel, after
authentication is complete. authentication is complete.
@ -47,37 +90,37 @@ class ServerInterface (object):
useless), you should also override some of the channel request methods useless), you should also override some of the channel request methods
below, which are used to determine which services will be allowed on below, which are used to determine which services will be allowed on
a given channel: a given channel:
- L{check_channel_pty_request}
- L{check_channel_shell_request}
- L{check_channel_subsystem_request}
- L{check_channel_window_change_request}
- L{check_channel_x11_request}
- L{check_channel_forward_agent_request}
- `check_channel_pty_request` The C{chanid} parameter is a small number that uniquely identifies the
- `check_channel_shell_request` channel within a L{Transport}. A L{Channel} object is not created
- `check_channel_subsystem_request` unless this method returns C{OPEN_SUCCEEDED} -- once a
- `check_channel_window_change_request` L{Channel} object is created, you can call L{Channel.get_id} to
- `check_channel_x11_request`
- `check_channel_forward_agent_request`
The ``chanid`` parameter is a small number that uniquely identifies the
channel within a `.Transport`. A `.Channel` object is not created
unless this method returns ``OPEN_SUCCEEDED`` -- once a
`.Channel` object is created, you can call `.Channel.get_id` to
retrieve the channel ID. retrieve the channel ID.
The return value should either be ``OPEN_SUCCEEDED`` (or The return value should either be C{OPEN_SUCCEEDED} (or
``0``) to allow the channel request, or one of the following error C{0}) to allow the channel request, or one of the following error
codes to reject it: codes to reject it:
- C{OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED}
- ``OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED`` - C{OPEN_FAILED_CONNECT_FAILED}
- ``OPEN_FAILED_CONNECT_FAILED`` - C{OPEN_FAILED_UNKNOWN_CHANNEL_TYPE}
- ``OPEN_FAILED_UNKNOWN_CHANNEL_TYPE`` - C{OPEN_FAILED_RESOURCE_SHORTAGE}
- ``OPEN_FAILED_RESOURCE_SHORTAGE``
The default implementation always returns The default implementation always returns
``OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED``. C{OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED}.
:param str kind: @param kind: the kind of channel the client would like to open
the kind of channel the client would like to open (usually (usually C{"session"}).
``"session"``). @type kind: str
:param int chanid: ID of the channel @param chanid: ID of the channel
:return: an `int` success or failure code (listed above) @type chanid: int
@return: a success or failure code (listed above)
@rtype: int
""" """
return OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED return OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
@ -88,13 +131,15 @@ class ServerInterface (object):
of authentication methods that might be successful. of authentication methods that might be successful.
The "list" is actually a string of comma-separated names of types of The "list" is actually a string of comma-separated names of types of
authentication. Possible values are ``"password"``, ``"publickey"``, authentication. Possible values are C{"password"}, C{"publickey"},
and ``"none"``. and C{"none"}.
The default implementation always returns ``"password"``. The default implementation always returns C{"password"}.
:param str username: the username requesting authentication. @param username: the username requesting authentication.
:return: a comma-separated `str` of authentication types @type username: str
@return: a comma-separated list of authentication types
@rtype: str
""" """
return 'password' return 'password'
@ -103,17 +148,17 @@ class ServerInterface (object):
Determine if a client may open channels with no (further) Determine if a client may open channels with no (further)
authentication. authentication.
Return `.AUTH_FAILED` if the client must authenticate, or Return L{AUTH_FAILED} if the client must authenticate, or
`.AUTH_SUCCESSFUL` if it's okay for the client to not L{AUTH_SUCCESSFUL} if it's okay for the client to not
authenticate. authenticate.
The default implementation always returns `.AUTH_FAILED`. The default implementation always returns L{AUTH_FAILED}.
:param str username: the username of the client. @param username: the username of the client.
:return: @type username: str
`.AUTH_FAILED` if the authentication fails; `.AUTH_SUCCESSFUL` if @return: L{AUTH_FAILED} if the authentication fails;
it succeeds. L{AUTH_SUCCESSFUL} if it succeeds.
:rtype: int @rtype: int
""" """
return AUTH_FAILED return AUTH_FAILED
@ -122,23 +167,25 @@ class ServerInterface (object):
Determine if a given username and password supplied by the client is Determine if a given username and password supplied by the client is
acceptable for use in authentication. acceptable for use in authentication.
Return `.AUTH_FAILED` if the password is not accepted, Return L{AUTH_FAILED} if the password is not accepted,
`.AUTH_SUCCESSFUL` if the password is accepted and completes L{AUTH_SUCCESSFUL} if the password is accepted and completes
the authentication, or `.AUTH_PARTIALLY_SUCCESSFUL` if your the authentication, or L{AUTH_PARTIALLY_SUCCESSFUL} if your
authentication is stateful, and this key is accepted for authentication is stateful, and this key is accepted for
authentication, but more authentication is required. (In this latter authentication, but more authentication is required. (In this latter
case, `get_allowed_auths` will be called to report to the client what case, L{get_allowed_auths} will be called to report to the client what
options it has for continuing the authentication.) options it has for continuing the authentication.)
The default implementation always returns `.AUTH_FAILED`. The default implementation always returns L{AUTH_FAILED}.
:param str username: the username of the authenticating client. @param username: the username of the authenticating client.
:param str password: the password given by the client. @type username: str
:return: @param password: the password given by the client.
`.AUTH_FAILED` if the authentication fails; `.AUTH_SUCCESSFUL` if @type password: str
it succeeds; `.AUTH_PARTIALLY_SUCCESSFUL` if the password auth is @return: L{AUTH_FAILED} if the authentication fails;
L{AUTH_SUCCESSFUL} if it succeeds;
L{AUTH_PARTIALLY_SUCCESSFUL} if the password auth is
successful, but authentication must continue. successful, but authentication must continue.
:rtype: int @rtype: int
""" """
return AUTH_FAILED return AUTH_FAILED
@ -149,28 +196,29 @@ class ServerInterface (object):
check the username and key and decide if you would accept a signature check the username and key and decide if you would accept a signature
made using this key. made using this key.
Return `.AUTH_FAILED` if the key is not accepted, Return L{AUTH_FAILED} if the key is not accepted,
`.AUTH_SUCCESSFUL` if the key is accepted and completes the L{AUTH_SUCCESSFUL} if the key is accepted and completes the
authentication, or `.AUTH_PARTIALLY_SUCCESSFUL` if your authentication, or L{AUTH_PARTIALLY_SUCCESSFUL} if your
authentication is stateful, and this password is accepted for authentication is stateful, and this password is accepted for
authentication, but more authentication is required. (In this latter authentication, but more authentication is required. (In this latter
case, `get_allowed_auths` will be called to report to the client what case, L{get_allowed_auths} will be called to report to the client what
options it has for continuing the authentication.) options it has for continuing the authentication.)
Note that you don't have to actually verify any key signtature here. Note that you don't have to actually verify any key signtature here.
If you're willing to accept the key, Paramiko will do the work of If you're willing to accept the key, paramiko will do the work of
verifying the client's signature. verifying the client's signature.
The default implementation always returns `.AUTH_FAILED`. The default implementation always returns L{AUTH_FAILED}.
:param str username: the username of the authenticating client @param username: the username of the authenticating client
:param .PKey key: the key object provided by the client @type username: str
:return: @param key: the key object provided by the client
`.AUTH_FAILED` if the client can't authenticate with this key; @type key: L{PKey <pkey.PKey>}
`.AUTH_SUCCESSFUL` if it can; `.AUTH_PARTIALLY_SUCCESSFUL` if it @return: L{AUTH_FAILED} if the client can't authenticate
can authenticate with this key but must continue with with this key; L{AUTH_SUCCESSFUL} if it can;
authentication L{AUTH_PARTIALLY_SUCCESSFUL} if it can authenticate with
:rtype: int this key but must continue with authentication
@rtype: int
""" """
return AUTH_FAILED return AUTH_FAILED
@ -178,24 +226,24 @@ class ServerInterface (object):
""" """
Begin an interactive authentication challenge, if supported. You Begin an interactive authentication challenge, if supported. You
should override this method in server mode if you want to support the should override this method in server mode if you want to support the
``"keyboard-interactive"`` auth type, which requires you to send a C{"keyboard-interactive"} auth type, which requires you to send a
series of questions for the client to answer. series of questions for the client to answer.
Return `.AUTH_FAILED` if this auth method isn't supported. Otherwise, Return L{AUTH_FAILED} if this auth method isn't supported. Otherwise,
you should return an `.InteractiveQuery` object containing the prompts you should return an L{InteractiveQuery} object containing the prompts
and instructions for the user. The response will be sent via a call and instructions for the user. The response will be sent via a call
to `check_auth_interactive_response`. to L{check_auth_interactive_response}.
The default implementation always returns `.AUTH_FAILED`. The default implementation always returns L{AUTH_FAILED}.
:param str username: the username of the authenticating client @param username: the username of the authenticating client
:param str submethods: @type username: str
a comma-separated list of methods preferred by the client (usually @param submethods: a comma-separated list of methods preferred by the
empty) client (usually empty)
:return: @type submethods: str
`.AUTH_FAILED` if this auth method isn't supported; otherwise an @return: L{AUTH_FAILED} if this auth method isn't supported; otherwise
object containing queries for the user an object containing queries for the user
:rtype: int or `.InteractiveQuery` @rtype: int or L{InteractiveQuery}
""" """
return AUTH_FAILED return AUTH_FAILED
@ -203,30 +251,31 @@ class ServerInterface (object):
""" """
Continue or finish an interactive authentication challenge, if Continue or finish an interactive authentication challenge, if
supported. You should override this method in server mode if you want supported. You should override this method in server mode if you want
to support the ``"keyboard-interactive"`` auth type. to support the C{"keyboard-interactive"} auth type.
Return `.AUTH_FAILED` if the responses are not accepted, Return L{AUTH_FAILED} if the responses are not accepted,
`.AUTH_SUCCESSFUL` if the responses are accepted and complete L{AUTH_SUCCESSFUL} if the responses are accepted and complete
the authentication, or `.AUTH_PARTIALLY_SUCCESSFUL` if your the authentication, or L{AUTH_PARTIALLY_SUCCESSFUL} if your
authentication is stateful, and this set of responses is accepted for authentication is stateful, and this set of responses is accepted for
authentication, but more authentication is required. (In this latter authentication, but more authentication is required. (In this latter
case, `get_allowed_auths` will be called to report to the client what case, L{get_allowed_auths} will be called to report to the client what
options it has for continuing the authentication.) options it has for continuing the authentication.)
If you wish to continue interactive authentication with more questions, If you wish to continue interactive authentication with more questions,
you may return an `.InteractiveQuery` object, which should cause the you may return an L{InteractiveQuery} object, which should cause the
client to respond with more answers, calling this method again. This client to respond with more answers, calling this method again. This
cycle can continue indefinitely. cycle can continue indefinitely.
The default implementation always returns `.AUTH_FAILED`. The default implementation always returns L{AUTH_FAILED}.
:param list responses: list of `str` responses from the client @param responses: list of responses from the client
:return: @type responses: list(str)
`.AUTH_FAILED` if the authentication fails; `.AUTH_SUCCESSFUL` if @return: L{AUTH_FAILED} if the authentication fails;
it succeeds; `.AUTH_PARTIALLY_SUCCESSFUL` if the interactive auth L{AUTH_SUCCESSFUL} if it succeeds;
is successful, but authentication must continue; otherwise an L{AUTH_PARTIALLY_SUCCESSFUL} if the interactive auth is
object containing queries for the user successful, but authentication must continue; otherwise an object
:rtype: int or `.InteractiveQuery` containing queries for the user
@rtype: int or L{InteractiveQuery}
""" """
return AUTH_FAILED return AUTH_FAILED
@ -234,20 +283,22 @@ class ServerInterface (object):
""" """
Handle a request for port forwarding. The client is asking that Handle a request for port forwarding. The client is asking that
connections to the given address and port be forwarded back across connections to the given address and port be forwarded back across
this ssh connection. An address of ``"0.0.0.0"`` indicates a global this ssh connection. An address of C{"0.0.0.0"} indicates a global
address (any address associated with this server) and a port of ``0`` address (any address associated with this server) and a port of C{0}
indicates that no specific port is requested (usually the OS will pick indicates that no specific port is requested (usually the OS will pick
a port). a port).
The default implementation always returns ``False``, rejecting the The default implementation always returns C{False}, rejecting the
port forwarding request. If the request is accepted, you should return port forwarding request. If the request is accepted, you should return
the port opened for listening. the port opened for listening.
:param str address: the requested address @param address: the requested address
:param int port: the requested port @type address: str
:return: @param port: the requested port
the port number (`int`) that was opened for listening, or ``False`` @type port: int
to reject @return: the port number that was opened for listening, or C{False} to
reject
@rtype: int
""" """
return False return False
@ -257,17 +308,19 @@ class ServerInterface (object):
If the given address and port is being forwarded across this ssh If the given address and port is being forwarded across this ssh
connection, the port should be closed. connection, the port should be closed.
:param str address: the forwarded address @param address: the forwarded address
:param int port: the forwarded port @type address: str
@param port: the forwarded port
@type port: int
""" """
pass pass
def check_global_request(self, kind, msg): def check_global_request(self, kind, msg):
""" """
Handle a global request of the given ``kind``. This method is called Handle a global request of the given C{kind}. This method is called
in server mode and client mode, whenever the remote host makes a global in server mode and client mode, whenever the remote host makes a global
request. If there are any arguments to the request, they will be in request. If there are any arguments to the request, they will be in
``msg``. C{msg}.
There aren't any useful global requests defined, aside from port There aren't any useful global requests defined, aside from port
forwarding, so usually this type of request is an extension to the forwarding, so usually this type of request is an extension to the
@ -278,100 +331,115 @@ class ServerInterface (object):
sent back with the successful result. (Note that the items in the sent back with the successful result. (Note that the items in the
tuple can only be strings, ints, longs, or bools.) tuple can only be strings, ints, longs, or bools.)
The default implementation always returns ``False``, indicating that it The default implementation always returns C{False}, indicating that it
does not support any global requests. does not support any global requests.
.. note:: Port forwarding requests are handled separately, in @note: Port forwarding requests are handled separately, in
`check_port_forward_request`. L{check_port_forward_request}.
:param str kind: the kind of global request being made. @param kind: the kind of global request being made.
:param .Message msg: any extra arguments to the request. @type kind: str
:return: @param msg: any extra arguments to the request.
``True`` or a `tuple` of data if the request was granted; ``False`` @type msg: L{Message}
otherwise. @return: C{True} or a tuple of data if the request was granted;
C{False} otherwise.
@rtype: bool
""" """
return False return False
### Channel requests ### Channel requests
def check_channel_pty_request(self, channel, term, width, height, pixelwidth, pixelheight, def check_channel_pty_request(self, channel, term, width, height, pixelwidth, pixelheight,
modes): modes):
""" """
Determine if a pseudo-terminal of the given dimensions (usually Determine if a pseudo-terminal of the given dimensions (usually
requested for shell access) can be provided on the given channel. requested for shell access) can be provided on the given channel.
The default implementation always returns ``False``. The default implementation always returns C{False}.
:param .Channel channel: the `.Channel` the pty request arrived on. @param channel: the L{Channel} the pty request arrived on.
:param str term: type of terminal requested (for example, ``"vt100"``). @type channel: L{Channel}
:param int width: width of screen in characters. @param term: type of terminal requested (for example, C{"vt100"}).
:param int height: height of screen in characters. @type term: str
:param int pixelwidth: @param width: width of screen in characters.
width of screen in pixels, if known (may be ``0`` if unknown). @type width: int
:param int pixelheight: @param height: height of screen in characters.
height of screen in pixels, if known (may be ``0`` if unknown). @type height: int
:return: @param pixelwidth: width of screen in pixels, if known (may be C{0} if
``True`` if the psuedo-terminal has been allocated; ``False`` unknown).
@type pixelwidth: int
@param pixelheight: height of screen in pixels, if known (may be C{0}
if unknown).
@type pixelheight: int
@return: C{True} if the psuedo-terminal has been allocated; C{False}
otherwise. otherwise.
@rtype: bool
""" """
return False return False
def check_channel_shell_request(self, channel): def check_channel_shell_request(self, channel):
""" """
Determine if a shell will be provided to the client on the given Determine if a shell will be provided to the client on the given
channel. If this method returns ``True``, the channel should be channel. If this method returns C{True}, the channel should be
connected to the stdin/stdout of a shell (or something that acts like connected to the stdin/stdout of a shell (or something that acts like
a shell). a shell).
The default implementation always returns ``False``. The default implementation always returns C{False}.
:param .Channel channel: the `.Channel` the request arrived on. @param channel: the L{Channel} the request arrived on.
:return: @type channel: L{Channel}
``True`` if this channel is now hooked up to a shell; ``False`` if @return: C{True} if this channel is now hooked up to a shell; C{False}
a shell can't or won't be provided. if a shell can't or won't be provided.
@rtype: bool
""" """
return False return False
def check_channel_exec_request(self, channel, command): def check_channel_exec_request(self, channel, command):
""" """
Determine if a shell command will be executed for the client. If this Determine if a shell command will be executed for the client. If this
method returns ``True``, the channel should be connected to the stdin, method returns C{True}, the channel should be connected to the stdin,
stdout, and stderr of the shell command. stdout, and stderr of the shell command.
The default implementation always returns ``False``. The default implementation always returns C{False}.
:param .Channel channel: the `.Channel` the request arrived on. @param channel: the L{Channel} the request arrived on.
:param str command: the command to execute. @type channel: L{Channel}
:return: @param command: the command to execute.
``True`` if this channel is now hooked up to the stdin, stdout, and @type command: str
stderr of the executing command; ``False`` if the command will not @return: C{True} if this channel is now hooked up to the stdin,
be executed. stdout, and stderr of the executing command; C{False} if the
command will not be executed.
@rtype: bool
.. versionadded:: 1.1 @since: 1.1
""" """
return False return False
def check_channel_subsystem_request(self, channel, name): def check_channel_subsystem_request(self, channel, name):
""" """
Determine if a requested subsystem will be provided to the client on Determine if a requested subsystem will be provided to the client on
the given channel. If this method returns ``True``, all future I/O the given channel. If this method returns C{True}, all future I/O
through this channel will be assumed to be connected to the requested through this channel will be assumed to be connected to the requested
subsystem. An example of a subsystem is ``sftp``. subsystem. An example of a subsystem is C{sftp}.
The default implementation checks for a subsystem handler assigned via The default implementation checks for a subsystem handler assigned via
`.Transport.set_subsystem_handler`. L{Transport.set_subsystem_handler}.
If one has been set, the handler is invoked and this method returns If one has been set, the handler is invoked and this method returns
``True``. Otherwise it returns ``False``. C{True}. Otherwise it returns C{False}.
.. note:: Because the default implementation uses the `.Transport` to @note: Because the default implementation uses the L{Transport} to
identify valid subsystems, you probably won't need to override this identify valid subsystems, you probably won't need to override this
method. method.
:param .Channel channel: the `.Channel` the pty request arrived on. @param channel: the L{Channel} the pty request arrived on.
:param str name: name of the requested subsystem. @type channel: L{Channel}
:return: @param name: name of the requested subsystem.
``True`` if this channel is now hooked up to the requested @type name: str
subsystem; ``False`` if that subsystem can't or won't be provided. @return: C{True} if this channel is now hooked up to the requested
subsystem; C{False} if that subsystem can't or won't be provided.
@rtype: bool
""" """
handler_class, larg, kwarg = channel.get_transport()._get_subsystem_handler(name) handler_class, larg, kwarg = channel.get_transport()._get_subsystem_handler(name)
if handler_class is None: if handler_class is None:
@ -385,178 +453,137 @@ class ServerInterface (object):
Determine if the pseudo-terminal on the given channel can be resized. Determine if the pseudo-terminal on the given channel can be resized.
This only makes sense if a pty was previously allocated on it. This only makes sense if a pty was previously allocated on it.
The default implementation always returns ``False``. The default implementation always returns C{False}.
:param .Channel channel: the `.Channel` the pty request arrived on. @param channel: the L{Channel} the pty request arrived on.
:param int width: width of screen in characters. @type channel: L{Channel}
:param int height: height of screen in characters. @param width: width of screen in characters.
:param int pixelwidth: @type width: int
width of screen in pixels, if known (may be ``0`` if unknown). @param height: height of screen in characters.
:param int pixelheight: @type height: int
height of screen in pixels, if known (may be ``0`` if unknown). @param pixelwidth: width of screen in pixels, if known (may be C{0} if
:return: ``True`` if the terminal was resized; ``False`` if not. unknown).
@type pixelwidth: int
@param pixelheight: height of screen in pixels, if known (may be C{0}
if unknown).
@type pixelheight: int
@return: C{True} if the terminal was resized; C{False} if not.
@rtype: bool
""" """
return False return False
def check_channel_x11_request(self, channel, single_connection, auth_protocol, auth_cookie, screen_number): def check_channel_x11_request(self, channel, single_connection, auth_protocol, auth_cookie, screen_number):
""" """
Determine if the client will be provided with an X11 session. If this Determine if the client will be provided with an X11 session. If this
method returns ``True``, X11 applications should be routed through new method returns C{True}, X11 applications should be routed through new
SSH channels, using `.Transport.open_x11_channel`. SSH channels, using L{Transport.open_x11_channel}.
The default implementation always returns ``False``. The default implementation always returns C{False}.
:param .Channel channel: the `.Channel` the X11 request arrived on @param channel: the L{Channel} the X11 request arrived on
:param bool single_connection: @type channel: L{Channel}
``True`` if only a single X11 channel should be opened, else @param single_connection: C{True} if only a single X11 channel should
``False``. be opened
:param str auth_protocol: the protocol used for X11 authentication @type single_connection: bool
:param str auth_cookie: the cookie used to authenticate to X11 @param auth_protocol: the protocol used for X11 authentication
:param int screen_number: the number of the X11 screen to connect to @type auth_protocol: str
:return: ``True`` if the X11 session was opened; ``False`` if not @param auth_cookie: the cookie used to authenticate to X11
@type auth_cookie: str
@param screen_number: the number of the X11 screen to connect to
@type screen_number: int
@return: C{True} if the X11 session was opened; C{False} if not
@rtype: bool
""" """
return False return False
def check_channel_forward_agent_request(self, channel): def check_channel_forward_agent_request(self, channel):
""" """
Determine if the client will be provided with an forward agent session. Determine if the client will be provided with an forward agent session.
If this method returns ``True``, the server will allow SSH Agent If this method returns C{True}, the server will allow SSH Agent
forwarding. forwarding.
The default implementation always returns ``False``. The default implementation always returns C{False}.
:param .Channel channel: the `.Channel` the request arrived on @param channel: the L{Channel} the request arrived on
:return: ``True`` if the AgentForward was loaded; ``False`` if not @type channel: L{Channel}
@return: C{True} if the AgentForward was loaded; C{False} if not
@rtype: bool
""" """
return False return False
def check_channel_direct_tcpip_request(self, chanid, origin, destination): def check_channel_direct_tcpip_request(self, chanid, origin, destination):
""" """
Determine if a local port forwarding channel will be granted, and Determine if a local port forwarding channel will be granted, and
return ``OPEN_SUCCEEDED`` or an error code. This method is return C{OPEN_SUCCEEDED} or an error code. This method is
called in server mode when the client requests a channel, after called in server mode when the client requests a channel, after
authentication is complete. authentication is complete.
The ``chanid`` parameter is a small number that uniquely identifies the The C{chanid} parameter is a small number that uniquely identifies the
channel within a `.Transport`. A `.Channel` object is not created channel within a L{Transport}. A L{Channel} object is not created
unless this method returns ``OPEN_SUCCEEDED`` -- once a unless this method returns C{OPEN_SUCCEEDED} -- once a
`.Channel` object is created, you can call `.Channel.get_id` to L{Channel} object is created, you can call L{Channel.get_id} to
retrieve the channel ID. retrieve the channel ID.
The origin and destination parameters are (ip_address, port) tuples The origin and destination parameters are (ip_address, port) tuples
that correspond to both ends of the TCP connection in the forwarding that correspond to both ends of the TCP connection in the forwarding
tunnel. tunnel.
The return value should either be ``OPEN_SUCCEEDED`` (or The return value should either be C{OPEN_SUCCEEDED} (or
``0``) to allow the channel request, or one of the following error C{0}) to allow the channel request, or one of the following error
codes to reject it: codes to reject it:
- C{OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED}
- ``OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED`` - C{OPEN_FAILED_CONNECT_FAILED}
- ``OPEN_FAILED_CONNECT_FAILED`` - C{OPEN_FAILED_UNKNOWN_CHANNEL_TYPE}
- ``OPEN_FAILED_UNKNOWN_CHANNEL_TYPE`` - C{OPEN_FAILED_RESOURCE_SHORTAGE}
- ``OPEN_FAILED_RESOURCE_SHORTAGE``
The default implementation always returns The default implementation always returns
``OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED``. C{OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED}.
:param int chanid: ID of the channel @param chanid: ID of the channel
:param tuple origin: @type chanid: int
2-tuple containing the IP address and port of the originator @param origin: 2-tuple containing the IP address and port of the
(client side) originator (client side)
:param tuple destination: @type origin: tuple
2-tuple containing the IP address and port of the destination @param destination: 2-tuple containing the IP address and port of the
(server side) destination (server side)
:return: an `int` success or failure code (listed above) @type destination: tuple
@return: a success or failure code (listed above)
@rtype: int
""" """
return OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED return OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
def check_channel_env_request(self, channel, name, value):
"""
Check whether a given environment variable can be specified for the
given channel. This method should return C{True} if the server
is willing to set the specified environment variable. Note that
some environment variables (e.g., PATH) can be exceedingly
dangerous, so blindly allowing the client to set the environment
is almost certainly not a good idea.
The default implementation always returns C{False}.
@param channel: the L{Channel} the env request arrived on
@type channel: L{Channel}
@param name: foo bar baz
@type name: str
@param value: flklj
@type value: str
@rtype: bool
"""
return False
class InteractiveQuery (object):
"""
A query (set of prompts) for a user during interactive authentication.
"""
def __init__(self, name='', instructions='', *prompts):
"""
Create a new interactive query to send to the client. The name and
instructions are optional, but are generally displayed to the end
user. A list of prompts may be included, or they may be added via
the `add_prompt` method.
:param str name: name of this query
:param str instructions:
user instructions (usually short) about this query
:param str prompts: one or more authentication prompts
"""
self.name = name
self.instructions = instructions
self.prompts = []
for x in prompts:
if isinstance(x, string_types):
self.add_prompt(x)
else:
self.add_prompt(x[0], x[1])
def add_prompt(self, prompt, echo=True):
"""
Add a prompt to this query. The prompt should be a (reasonably short)
string. Multiple prompts can be added to the same query.
:param str prompt: the user prompt
:param bool echo:
``True`` (default) if the user's response should be echoed;
``False`` if not (for a password or similar)
"""
self.prompts.append((prompt, echo))
class SubsystemHandler (threading.Thread): class SubsystemHandler (threading.Thread):
""" """
Handler for a subsytem in server mode. If you create a subclass of this Handler for a subsytem in server mode. If you create a subclass of this
class and pass it to `.Transport.set_subsystem_handler`, an object of this class and pass it to
L{Transport.set_subsystem_handler},
an object of this
class will be created for each request for this subsystem. Each new object class will be created for each request for this subsystem. Each new object
will be executed within its own new thread by calling `start_subsystem`. will be executed within its own new thread by calling L{start_subsystem}.
When that method completes, the channel is closed. When that method completes, the channel is closed.
For example, if you made a subclass ``MP3Handler`` and registered it as the For example, if you made a subclass C{MP3Handler} and registered it as the
handler for subsystem ``"mp3"``, then whenever a client has successfully handler for subsystem C{"mp3"}, then whenever a client has successfully
authenticated and requests subsytem ``"mp3"``, an object of class authenticated and requests subsytem C{"mp3"}, an object of class
``MP3Handler`` will be created, and `start_subsystem` will be called on C{MP3Handler} will be created, and L{start_subsystem} will be called on
it from a new thread. it from a new thread.
""" """
def __init__(self, channel, name, server): def __init__(self, channel, name, server):
""" """
Create a new handler for a channel. This is used by `.ServerInterface` Create a new handler for a channel. This is used by L{ServerInterface}
to start up a new handler when a channel requests this subsystem. You to start up a new handler when a channel requests this subsystem. You
don't need to override this method, but if you do, be sure to pass the don't need to override this method, but if you do, be sure to pass the
``channel`` and ``name`` parameters through to the original ``__init__`` C{channel} and C{name} parameters through to the original C{__init__}
method here. method here.
:param .Channel channel: the channel associated with this subsystem request. @param channel: the channel associated with this subsystem request.
:param str name: name of the requested subsystem. @type channel: L{Channel}
:param .ServerInterface server: @param name: name of the requested subsystem.
the server object for the session that started this subsystem @type name: str
@param server: the server object for the session that started this
subsystem
@type server: L{ServerInterface}
""" """
threading.Thread.__init__(self, target=self._run) threading.Thread.__init__(self, target=self._run)
self.__channel = channel self.__channel = channel
@ -566,8 +593,10 @@ class SubsystemHandler (threading.Thread):
def get_server(self): def get_server(self):
""" """
Return the `.ServerInterface` object associated with this channel and Return the L{ServerInterface} object associated with this channel and
subsystem. subsystem.
@rtype: L{ServerInterface}
""" """
return self.__server return self.__server
@ -592,20 +621,22 @@ class SubsystemHandler (threading.Thread):
subsystem is finished, this method will return. After this method subsystem is finished, this method will return. After this method
returns, the channel is closed. returns, the channel is closed.
The combination of ``transport`` and ``channel`` are unique; this handler The combination of C{transport} and C{channel} are unique; this handler
corresponds to exactly one `.Channel` on one `.Transport`. corresponds to exactly one L{Channel} on one L{Transport}.
.. note:: @note: It is the responsibility of this method to exit if the
It is the responsibility of this method to exit if the underlying underlying L{Transport} is closed. This can be done by checking
`.Transport` is closed. This can be done by checking L{Transport.is_active} or noticing an EOF
`.Transport.is_active` or noticing an EOF on the `.Channel`. If on the L{Channel}. If this method loops forever without checking
this method loops forever without checking for this case, your for this case, your python interpreter may refuse to exit because
Python interpreter may refuse to exit because this thread will this thread will still be running.
still be running.
:param str name: name of the requested subsystem. @param name: name of the requested subsystem.
:param .Transport transport: the server-mode `.Transport`. @type name: str
:param .Channel channel: the channel associated with this subsystem request. @param transport: the server-mode L{Transport}.
@type transport: L{Transport}
@param channel: the channel associated with this subsystem request.
@type channel: L{Channel}
""" """
pass pass
@ -614,6 +645,6 @@ class SubsystemHandler (threading.Thread):
Perform any cleanup at the end of a subsystem. The default Perform any cleanup at the end of a subsystem. The default
implementation just closes the channel. implementation just closes the channel.
.. versionadded:: 1.1 @since: 1.1
""" """
self.__channel.close() self.__channel.close()

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -20,31 +20,32 @@ import select
import socket import socket
import struct import struct
from paramiko.common import *
from paramiko import util from paramiko import util
from paramiko.common import asbytes, DEBUG from paramiko.channel import Channel
from paramiko.message import Message from paramiko.message import Message
from paramiko.py3compat import byte_chr, byte_ord
CMD_INIT, CMD_VERSION, CMD_OPEN, CMD_CLOSE, CMD_READ, CMD_WRITE, CMD_LSTAT, CMD_FSTAT, \ CMD_INIT, CMD_VERSION, CMD_OPEN, CMD_CLOSE, CMD_READ, CMD_WRITE, CMD_LSTAT, CMD_FSTAT, \
CMD_SETSTAT, CMD_FSETSTAT, CMD_OPENDIR, CMD_READDIR, CMD_REMOVE, CMD_MKDIR, \ CMD_SETSTAT, CMD_FSETSTAT, CMD_OPENDIR, CMD_READDIR, CMD_REMOVE, CMD_MKDIR, \
CMD_RMDIR, CMD_REALPATH, CMD_STAT, CMD_RENAME, CMD_READLINK, CMD_SYMLINK = range(1, 21) CMD_RMDIR, CMD_REALPATH, CMD_STAT, CMD_RENAME, CMD_READLINK, CMD_SYMLINK \
= range(1, 21)
CMD_STATUS, CMD_HANDLE, CMD_DATA, CMD_NAME, CMD_ATTRS = range(101, 106) CMD_STATUS, CMD_HANDLE, CMD_DATA, CMD_NAME, CMD_ATTRS = range(101, 106)
CMD_EXTENDED, CMD_EXTENDED_REPLY = range(200, 202) CMD_EXTENDED, CMD_EXTENDED_REPLY = range(200, 202)
SFTP_OK = 0 SFTP_OK = 0
SFTP_EOF, SFTP_NO_SUCH_FILE, SFTP_PERMISSION_DENIED, SFTP_FAILURE, SFTP_BAD_MESSAGE, \ SFTP_EOF, SFTP_NO_SUCH_FILE, SFTP_PERMISSION_DENIED, SFTP_FAILURE, SFTP_BAD_MESSAGE, \
SFTP_NO_CONNECTION, SFTP_CONNECTION_LOST, SFTP_OP_UNSUPPORTED = range(1, 9) SFTP_NO_CONNECTION, SFTP_CONNECTION_LOST, SFTP_OP_UNSUPPORTED = range(1, 9)
SFTP_DESC = ['Success', SFTP_DESC = [ 'Success',
'End of file', 'End of file',
'No such file', 'No such file',
'Permission denied', 'Permission denied',
'Failure', 'Failure',
'Bad message', 'Bad message',
'No connection', 'No connection',
'Connection lost', 'Connection lost',
'Operation unsupported'] 'Operation unsupported' ]
SFTP_FLAG_READ = 0x1 SFTP_FLAG_READ = 0x1
SFTP_FLAG_WRITE = 0x2 SFTP_FLAG_WRITE = 0x2
@ -85,7 +86,7 @@ CMD_NAMES = {
CMD_ATTRS: 'attrs', CMD_ATTRS: 'attrs',
CMD_EXTENDED: 'extended', CMD_EXTENDED: 'extended',
CMD_EXTENDED_REPLY: 'extended_reply' CMD_EXTENDED_REPLY: 'extended_reply'
} }
class SFTPError (Exception): class SFTPError (Exception):
@ -98,8 +99,10 @@ class BaseSFTP (object):
self.sock = None self.sock = None
self.ultra_debug = False self.ultra_debug = False
### internals... ### internals...
def _send_version(self): def _send_version(self):
self._send_packet(CMD_INIT, struct.pack('>I', _VERSION)) self._send_packet(CMD_INIT, struct.pack('>I', _VERSION))
t, data = self._read_packet() t, data = self._read_packet()
@ -118,11 +121,11 @@ class BaseSFTP (object):
raise SFTPError('Incompatible sftp protocol') raise SFTPError('Incompatible sftp protocol')
version = struct.unpack('>I', data[:4])[0] version = struct.unpack('>I', data[:4])[0]
# advertise that we support "check-file" # advertise that we support "check-file"
extension_pairs = ['check-file', 'md5,sha1'] extension_pairs = [ 'check-file', 'md5,sha1' ]
msg = Message() msg = Message()
msg.add_int(_VERSION) msg.add_int(_VERSION)
msg.add(*extension_pairs) msg.add(*extension_pairs)
self._send_packet(CMD_VERSION, msg) self._send_packet(CMD_VERSION, str(msg))
return version return version
def _log(self, level, msg, *args): def _log(self, level, msg, *args):
@ -139,7 +142,7 @@ class BaseSFTP (object):
return return
def _read_all(self, n): def _read_all(self, n):
out = bytes() out = ''
while n > 0: while n > 0:
if isinstance(self.sock, socket.socket): if isinstance(self.sock, socket.socket):
# sometimes sftp is used directly over a socket instead of # sometimes sftp is used directly over a socket instead of
@ -148,7 +151,7 @@ class BaseSFTP (object):
# return or raise an exception, but calling select on a closed # return or raise an exception, but calling select on a closed
# socket will.) # socket will.)
while True: while True:
read, write, err = select.select([self.sock], [], [], 0.1) read, write, err = select.select([ self.sock ], [], [], 0.1)
if len(read) > 0: if len(read) > 0:
x = self.sock.recv(n) x = self.sock.recv(n)
break break
@ -163,8 +166,7 @@ class BaseSFTP (object):
def _send_packet(self, t, packet): def _send_packet(self, t, packet):
#self._log(DEBUG2, 'write: %s (len=%d)' % (CMD_NAMES.get(t, '0x%02x' % t), len(packet))) #self._log(DEBUG2, 'write: %s (len=%d)' % (CMD_NAMES.get(t, '0x%02x' % t), len(packet)))
packet = asbytes(packet) out = struct.pack('>I', len(packet) + 1) + chr(t) + packet
out = struct.pack('>I', len(packet) + 1) + byte_chr(t) + packet
if self.ultra_debug: if self.ultra_debug:
self._log(DEBUG, util.format_binary(out, 'OUT: ')) self._log(DEBUG, util.format_binary(out, 'OUT: '))
self._write_all(out) self._write_all(out)
@ -173,14 +175,14 @@ class BaseSFTP (object):
x = self._read_all(4) x = self._read_all(4)
# most sftp servers won't accept packets larger than about 32k, so # most sftp servers won't accept packets larger than about 32k, so
# anything with the high byte set (> 16MB) is just garbage. # anything with the high byte set (> 16MB) is just garbage.
if byte_ord(x[0]): if x[0] != '\x00':
raise SFTPError('Garbage packet received') raise SFTPError('Garbage packet received')
size = struct.unpack('>I', x)[0] size = struct.unpack('>I', x)[0]
data = self._read_all(size) data = self._read_all(size)
if self.ultra_debug: if self.ultra_debug:
self._log(DEBUG, util.format_binary(data, 'IN: ')) self._log(DEBUG, util.format_binary(data, 'IN: '))
if size > 0: if size > 0:
t = byte_ord(data[0]) t = ord(data[0])
#self._log(DEBUG2, 'read: %s (len=%d)' % (CMD_NAMES.get(t), '0x%02x' % t, len(data)-1)) #self._log(DEBUG2, 'read: %s (len=%d)' % (CMD_NAMES.get(t), '0x%02x' % t, len(data)-1))
return t, data[1:] return t, data[1:]
return 0, bytes() return 0, ''

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -16,36 +16,39 @@
# along with Paramiko; if not, write to the Free Software Foundation, Inc., # along with Paramiko; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
import six
if six.PY3:
long = lambda x: int(x)
import stat import stat
import time import time
from paramiko.common import x80000000, o700, o70, xffffffff from paramiko.common import *
from paramiko.py3compat import long, b from paramiko.sftp import *
class SFTPAttributes (object): class SFTPAttributes (object):
""" """
Representation of the attributes of a file (or proxied file) for SFTP in Representation of the attributes of a file (or proxied file) for SFTP in
client or server mode. It attemps to mirror the object returned by client or server mode. It attemps to mirror the object returned by
`os.stat` as closely as possible, so it may have the following fields, C{os.stat} as closely as possible, so it may have the following fields,
with the same meanings as those returned by an `os.stat` object: with the same meanings as those returned by an C{os.stat} object:
- st_size
- ``st_size`` - st_uid
- ``st_uid`` - st_gid
- ``st_gid`` - st_mode
- ``st_mode`` - st_atime
- ``st_atime`` - st_mtime
- ``st_mtime``
Because SFTP allows flags to have other arbitrary named attributes, these Because SFTP allows flags to have other arbitrary named attributes, these
are stored in a dict named ``attr``. Occasionally, the filename is also are stored in a dict named C{attr}. Occasionally, the filename is also
stored, in ``filename``. stored, in C{filename}.
""" """
FLAG_SIZE = 1 FLAG_SIZE = 1
FLAG_UIDGID = 2 FLAG_UIDGID = 2
FLAG_PERMISSIONS = 4 FLAG_PERMISSIONS = 4
FLAG_AMTIME = 8 FLAG_AMTIME = 8
FLAG_EXTENDED = x80000000 FLAG_EXTENDED = long(0x80000000)
def __init__(self): def __init__(self):
""" """
@ -62,12 +65,15 @@ class SFTPAttributes (object):
def from_stat(cls, obj, filename=None): def from_stat(cls, obj, filename=None):
""" """
Create an `.SFTPAttributes` object from an existing ``stat`` object (an Create an SFTPAttributes object from an existing C{stat} object (an
object returned by `os.stat`). object returned by C{os.stat}).
:param object obj: an object returned by `os.stat` (or equivalent). @param obj: an object returned by C{os.stat} (or equivalent).
:param str filename: the filename associated with this file. @type obj: object
:return: new `.SFTPAttributes` object with the same attribute fields. @param filename: the filename associated with this file.
@type filename: str
@return: new L{SFTPAttributes} object with the same attribute fields.
@rtype: L{SFTPAttributes}
""" """
attr = cls() attr = cls()
attr.st_size = obj.st_size attr.st_size = obj.st_size
@ -84,8 +90,10 @@ class SFTPAttributes (object):
def __repr__(self): def __repr__(self):
return '<SFTPAttributes: %s>' % self._debug_str() return '<SFTPAttributes: %s>' % self._debug_str()
### internals... ### internals...
def _from_msg(cls, msg, filename=None, longname=None): def _from_msg(cls, msg, filename=None, longname=None):
attr = cls() attr = cls()
attr._unpack(msg) attr._unpack(msg)
@ -139,7 +147,7 @@ class SFTPAttributes (object):
msg.add_int(long(self.st_mtime)) msg.add_int(long(self.st_mtime))
if self._flags & self.FLAG_EXTENDED: if self._flags & self.FLAG_EXTENDED:
msg.add_int(len(self.attr)) msg.add_int(len(self.attr))
for key, val in self.attr.items(): for key, val in self.attr.iteritems():
msg.add_string(key) msg.add_string(key)
msg.add_string(val) msg.add_string(val)
return return
@ -154,7 +162,7 @@ class SFTPAttributes (object):
out += 'mode=' + oct(self.st_mode) + ' ' out += 'mode=' + oct(self.st_mode) + ' '
if (self.st_atime is not None) and (self.st_mtime is not None): if (self.st_atime is not None) and (self.st_mtime is not None):
out += 'atime=%d mtime=%d ' % (self.st_atime, self.st_mtime) out += 'atime=%d mtime=%d ' % (self.st_atime, self.st_mtime)
for k, v in self.attr.items(): for k, v in self.attr.iteritems():
out += '"%s"=%r ' % (str(k), v) out += '"%s"=%r ' % (str(k), v)
out += ']' out += ']'
return out return out
@ -171,7 +179,7 @@ class SFTPAttributes (object):
_rwx = staticmethod(_rwx) _rwx = staticmethod(_rwx)
def __str__(self): def __str__(self):
"""create a unix-style long description of the file (like ls -l)""" "create a unix-style long description of the file (like ls -l)"
if self.st_mode is not None: if self.st_mode is not None:
kind = stat.S_IFMT(self.st_mode) kind = stat.S_IFMT(self.st_mode)
if kind == stat.S_IFIFO: if kind == stat.S_IFIFO:
@ -190,13 +198,13 @@ class SFTPAttributes (object):
ks = 's' ks = 's'
else: else:
ks = '?' ks = '?'
ks += self._rwx((self.st_mode & o700) >> 6, self.st_mode & stat.S_ISUID) ks += self._rwx((self.st_mode & 0o700) >> 6, self.st_mode & stat.S_ISUID)
ks += self._rwx((self.st_mode & o70) >> 3, self.st_mode & stat.S_ISGID) ks += self._rwx((self.st_mode & 0o70) >> 3, self.st_mode & stat.S_ISGID)
ks += self._rwx(self.st_mode & 7, self.st_mode & stat.S_ISVTX, True) ks += self._rwx(self.st_mode & 7, self.st_mode & stat.S_ISVTX, True)
else: else:
ks = '?---------' ks = '?---------'
# compute display date # compute display date
if (self.st_mtime is None) or (self.st_mtime == xffffffff): if (self.st_mtime is None) or (self.st_mtime == long(0xffffffff)):
# shouldn't really happen # shouldn't really happen
datestr = '(unknown date)' datestr = '(unknown date)'
else: else:
@ -217,5 +225,3 @@ class SFTPAttributes (object):
return '%s 1 %-8d %-8d %8d %-12s %s' % (ks, uid, gid, self.st_size, datestr, filename) return '%s 1 %-8d %-8d %8d %-12s %s' % (ks, uid, gid, self.st_size, datestr, filename)
def asbytes(self):
return b(str(self))

View File

@ -1,13 +1,13 @@
# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> # Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com>
# #
# This file is part of Paramiko. # This file is part of paramiko.
# #
# Paramiko is free software; you can redistribute it and/or modify it under the # Paramiko is free software; you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free # terms of the GNU Lesser General Public License as published by the Free
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -16,6 +16,13 @@
# along with Paramiko; if not, write to the Free Software Foundation, Inc., # along with Paramiko; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
"""
Client-mode SFTP support.
"""
import six
if six.PY3:
long = lambda x: int(x)
from binascii import hexlify from binascii import hexlify
import errno import errno
@ -24,18 +31,8 @@ import stat
import threading import threading
import time import time
import weakref import weakref
from paramiko import util
from paramiko.channel import Channel
from paramiko.message import Message
from paramiko.common import INFO, DEBUG, o777
from paramiko.py3compat import bytestring, b, u, long, string_types, bytes_types
from paramiko.sftp import BaseSFTP, CMD_OPENDIR, CMD_HANDLE, SFTPError, CMD_READDIR, \
CMD_NAME, CMD_CLOSE, SFTP_FLAG_READ, SFTP_FLAG_WRITE, SFTP_FLAG_CREATE, \
SFTP_FLAG_TRUNC, SFTP_FLAG_APPEND, SFTP_FLAG_EXCL, CMD_OPEN, CMD_REMOVE, \
CMD_RENAME, CMD_MKDIR, CMD_RMDIR, CMD_STAT, CMD_ATTRS, CMD_LSTAT, \
CMD_SYMLINK, CMD_SETSTAT, CMD_READLINK, CMD_REALPATH, CMD_STATUS, SFTP_OK, \
SFTP_EOF, SFTP_NO_SUCH_FILE, SFTP_PERMISSION_DENIED
from paramiko.sftp import *
from paramiko.sftp_attr import SFTPAttributes from paramiko.sftp_attr import SFTPAttributes
from paramiko.ssh_exception import SSHException from paramiko.ssh_exception import SSHException
from paramiko.sftp_file import SFTPFile from paramiko.sftp_file import SFTPFile
@ -49,33 +46,31 @@ def _to_unicode(s):
""" """
try: try:
return s.encode('ascii') return s.encode('ascii')
except (UnicodeError, AttributeError): except UnicodeError:
try: try:
return s.decode('utf-8') return s.decode('utf-8')
except UnicodeError: except UnicodeError:
return s return s
b_slash = b'/'
class SFTPClient (BaseSFTP):
"""
SFTP client object. C{SFTPClient} is used to open an sftp session across
an open ssh L{Transport} and do remote file operations.
"""
class SFTPClient(BaseSFTP):
"""
SFTP client object.
Used to open an SFTP session across an open SSH `.Transport` and perform
remote file operations.
"""
def __init__(self, sock): def __init__(self, sock):
""" """
Create an SFTP client from an existing `.Channel`. The channel Create an SFTP client from an existing L{Channel}. The channel
should already have requested the ``"sftp"`` subsystem. should already have requested the C{"sftp"} subsystem.
An alternate way to create an SFTP client context is by using An alternate way to create an SFTP client context is by using
`from_transport`. L{from_transport}.
:param .Channel sock: an open `.Channel` using the ``"sftp"`` subsystem @param sock: an open L{Channel} using the C{"sftp"} subsystem
@type sock: L{Channel}
:raises SSHException: if there's an exception while negotiating @raise SSHException: if there's an exception while negotiating
sftp sftp
""" """
BaseSFTP.__init__(self) BaseSFTP.__init__(self)
@ -94,18 +89,19 @@ class SFTPClient(BaseSFTP):
self.ultra_debug = transport.get_hexdump() self.ultra_debug = transport.get_hexdump()
try: try:
server_version = self._send_version() server_version = self._send_version()
except EOFError: except EOFError as x:
raise SSHException('EOF during negotiation') raise SSHException('EOF during negotiation')
self._log(INFO, 'Opened sftp connection (server version %d)' % server_version) self._log(INFO, 'Opened sftp connection (server version %d)' % server_version)
def from_transport(cls, t): def from_transport(cls, t):
""" """
Create an SFTP client channel from an open `.Transport`. Create an SFTP client channel from an open L{Transport}.
:param .Transport t: an open `.Transport` which is already authenticated @param t: an open L{Transport} which is already authenticated
:return: @type t: L{Transport}
a new `.SFTPClient` object, referring to an sftp session (channel) @return: a new L{SFTPClient} object, referring to an sftp session
across the transport (channel) across the transport
@rtype: L{SFTPClient}
""" """
chan = t.open_session() chan = t.open_session()
if chan is None: if chan is None:
@ -117,79 +113,84 @@ class SFTPClient(BaseSFTP):
def _log(self, level, msg, *args): def _log(self, level, msg, *args):
if isinstance(msg, list): if isinstance(msg, list):
for m in msg: for m in msg:
self._log(level, m, *args) super(SFTPClient, self)._log(level, "[chan %s] " + m, *([ self.sock.get_name() ] + list(args)))
else: else:
# escape '%' in msg (they could come from file or directory names) before logging super(SFTPClient, self)._log(level, "[chan %s] " + msg, *([ self.sock.get_name() ] + list(args)))
msg = msg.replace('%','%%')
super(SFTPClient, self)._log(level, "[chan %s] " + msg, *([self.sock.get_name()] + list(args)))
def close(self): def close(self):
""" """
Close the SFTP session and its underlying channel. Close the SFTP session and its underlying channel.
.. versionadded:: 1.4 @since: 1.4
""" """
self._log(INFO, 'sftp session closed.') self._log(INFO, 'sftp session closed.')
self.sock.close() self.sock.close()
def get_channel(self): def get_channel(self):
""" """
Return the underlying `.Channel` object for this SFTP session. This Return the underlying L{Channel} object for this SFTP session. This
might be useful for doing things like setting a timeout on the channel. might be useful for doing things like setting a timeout on the channel.
.. versionadded:: 1.7.1 @return: the SSH channel
@rtype: L{Channel}
@since: 1.7.1
""" """
return self.sock return self.sock
def listdir(self, path='.'): def listdir(self, path='.'):
""" """
Return a list containing the names of the entries in the given ``path``. Return a list containing the names of the entries in the given C{path}.
The list is in arbitrary order. It does not include the special The list is in arbitrary order. It does not include the special
entries ``'.'`` and ``'..'`` even if they are present in the folder. entries C{'.'} and C{'..'} even if they are present in the folder.
This method is meant to mirror ``os.listdir`` as closely as possible. This method is meant to mirror C{os.listdir} as closely as possible.
For a list of full `.SFTPAttributes` objects, see `listdir_attr`. For a list of full L{SFTPAttributes} objects, see L{listdir_attr}.
:param str path: path to list (defaults to ``'.'``) @param path: path to list (defaults to C{'.'})
@type path: str
@return: list of filenames
@rtype: list of str
""" """
return [f.filename for f in self.listdir_attr(path)] return [f.filename for f in self.listdir_attr(path)]
def listdir_attr(self, path='.'): def listdir_attr(self, path='.'):
""" """
Return a list containing `.SFTPAttributes` objects corresponding to Return a list containing L{SFTPAttributes} objects corresponding to
files in the given ``path``. The list is in arbitrary order. It does files in the given C{path}. The list is in arbitrary order. It does
not include the special entries ``'.'`` and ``'..'`` even if they are not include the special entries C{'.'} and C{'..'} even if they are
present in the folder. present in the folder.
The returned `.SFTPAttributes` objects will each have an additional The returned L{SFTPAttributes} objects will each have an additional
field: ``longname``, which may contain a formatted string of the file's field: C{longname}, which may contain a formatted string of the file's
attributes, in unix format. The content of this string will probably attributes, in unix format. The content of this string will probably
depend on the SFTP server implementation. depend on the SFTP server implementation.
:param str path: path to list (defaults to ``'.'``) @param path: path to list (defaults to C{'.'})
:return: list of `.SFTPAttributes` objects @type path: str
@return: list of attributes
@rtype: list of L{SFTPAttributes}
.. versionadded:: 1.2 @since: 1.2
""" """
path = self._adjust_cwd(path) path = self._adjust_cwd(path)
self._log(DEBUG, 'listdir(%r)' % path) self._log(DEBUG, 'listdir(%r)' % path)
t, msg = self._request(CMD_OPENDIR, path) t, msg = self._request(CMD_OPENDIR, path)
if t != CMD_HANDLE: if t != CMD_HANDLE:
raise SFTPError('Expected handle') raise SFTPError('Expected handle')
handle = msg.get_binary() handle = msg.get_string()
filelist = [] filelist = []
while True: while True:
try: try:
t, msg = self._request(CMD_READDIR, handle) t, msg = self._request(CMD_READDIR, handle)
except EOFError: except EOFError as e:
# done with handle # done with handle
break break
if t != CMD_NAME: if t != CMD_NAME:
raise SFTPError('Expected name response') raise SFTPError('Expected name response')
count = msg.get_int() count = msg.get_int()
for i in range(count): for i in range(count):
filename = msg.get_text() filename = _to_unicode(msg.get_string())
longname = msg.get_text() longname = _to_unicode(msg.get_string())
attr = SFTPAttributes._from_msg(msg, filename, longname) attr = SFTPAttributes._from_msg(msg, filename, longname)
if (filename != '.') and (filename != '..'): if (filename != '.') and (filename != '..'):
filelist.append(attr) filelist.append(attr)
@ -199,34 +200,37 @@ class SFTPClient(BaseSFTP):
def open(self, filename, mode='r', bufsize=-1): def open(self, filename, mode='r', bufsize=-1):
""" """
Open a file on the remote server. The arguments are the same as for Open a file on the remote server. The arguments are the same as for
Python's built-in `python:file` (aka `python:open`). A file-like python's built-in C{file} (aka C{open}). A file-like object is
object is returned, which closely mimics the behavior of a normal returned, which closely mimics the behavior of a normal python file
Python file object, including the ability to be used as a context object, including the ability to be used as a context manager.
manager.
The mode indicates how the file is to be opened: ``'r'`` for reading, The mode indicates how the file is to be opened: C{'r'} for reading,
``'w'`` for writing (truncating an existing file), ``'a'`` for C{'w'} for writing (truncating an existing file), C{'a'} for appending,
appending, ``'r+'`` for reading/writing, ``'w+'`` for reading/writing C{'r+'} for reading/writing, C{'w+'} for reading/writing (truncating an
(truncating an existing file), ``'a+'`` for reading/appending. The existing file), C{'a+'} for reading/appending. The python C{'b'} flag
Python ``'b'`` flag is ignored, since SSH treats all files as binary. is ignored, since SSH treats all files as binary. The C{'U'} flag is
The ``'U'`` flag is supported in a compatible way. supported in a compatible way.
Since 1.5.2, an ``'x'`` flag indicates that the operation should only Since 1.5.2, an C{'x'} flag indicates that the operation should only
succeed if the file was created and did not previously exist. This has succeed if the file was created and did not previously exist. This has
no direct mapping to Python's file flags, but is commonly known as the no direct mapping to python's file flags, but is commonly known as the
``O_EXCL`` flag in posix. C{O_EXCL} flag in posix.
The file will be buffered in standard Python style by default, but The file will be buffered in standard python style by default, but
can be altered with the ``bufsize`` parameter. ``0`` turns off can be altered with the C{bufsize} parameter. C{0} turns off
buffering, ``1`` uses line buffering, and any number greater than 1 buffering, C{1} uses line buffering, and any number greater than 1
(``>1``) uses that specific buffer size. (C{>1}) uses that specific buffer size.
:param str filename: name of the file to open @param filename: name of the file to open
:param str mode: mode (Python-style) to open in @type filename: str
:param int bufsize: desired buffering (-1 = default buffer size) @param mode: mode (python-style) to open in
:return: an `.SFTPFile` object representing the open file @type mode: str
@param bufsize: desired buffering (-1 = default buffer size)
@type bufsize: int
@return: a file object representing the open file
@rtype: SFTPFile
:raises IOError: if the file could not be opened. @raise IOError: if the file could not be opened.
""" """
filename = self._adjust_cwd(filename) filename = self._adjust_cwd(filename)
self._log(DEBUG, 'open(%r, %r)' % (filename, mode)) self._log(DEBUG, 'open(%r, %r)' % (filename, mode))
@ -235,31 +239,32 @@ class SFTPClient(BaseSFTP):
imode |= SFTP_FLAG_READ imode |= SFTP_FLAG_READ
if ('w' in mode) or ('+' in mode) or ('a' in mode): if ('w' in mode) or ('+' in mode) or ('a' in mode):
imode |= SFTP_FLAG_WRITE imode |= SFTP_FLAG_WRITE
if 'w' in mode: if ('w' in mode):
imode |= SFTP_FLAG_CREATE | SFTP_FLAG_TRUNC imode |= SFTP_FLAG_CREATE | SFTP_FLAG_TRUNC
if 'a' in mode: if ('a' in mode):
imode |= SFTP_FLAG_CREATE | SFTP_FLAG_APPEND imode |= SFTP_FLAG_CREATE | SFTP_FLAG_APPEND
if 'x' in mode: if ('x' in mode):
imode |= SFTP_FLAG_CREATE | SFTP_FLAG_EXCL imode |= SFTP_FLAG_CREATE | SFTP_FLAG_EXCL
attrblock = SFTPAttributes() attrblock = SFTPAttributes()
t, msg = self._request(CMD_OPEN, filename, imode, attrblock) t, msg = self._request(CMD_OPEN, filename, imode, attrblock)
if t != CMD_HANDLE: if t != CMD_HANDLE:
raise SFTPError('Expected handle') raise SFTPError('Expected handle')
handle = msg.get_binary() handle = msg.get_string()
self._log(DEBUG, 'open(%r, %r) -> %s' % (filename, mode, hexlify(handle))) self._log(DEBUG, 'open(%r, %r) -> %s' % (filename, mode, hexlify(handle)))
return SFTPFile(self, handle, mode, bufsize) return SFTPFile(self, handle, mode, bufsize)
# Python continues to vacillate about "open" vs "file"... # python continues to vacillate about "open" vs "file"...
file = open file = open
def remove(self, path): def remove(self, path):
""" """
Remove the file at the given path. This only works on files; for Remove the file at the given path. This only works on files; for
removing folders (directories), use `rmdir`. removing folders (directories), use L{rmdir}.
:param str path: path (absolute or relative) of the file to remove @param path: path (absolute or relative) of the file to remove
@type path: str
:raises IOError: if the path refers to a folder (directory) @raise IOError: if the path refers to a folder (directory)
""" """
path = self._adjust_cwd(path) path = self._adjust_cwd(path)
self._log(DEBUG, 'remove(%r)' % path) self._log(DEBUG, 'remove(%r)' % path)
@ -269,12 +274,14 @@ class SFTPClient(BaseSFTP):
def rename(self, oldpath, newpath): def rename(self, oldpath, newpath):
""" """
Rename a file or folder from ``oldpath`` to ``newpath``. Rename a file or folder from C{oldpath} to C{newpath}.
:param str oldpath: existing name of the file or folder @param oldpath: existing name of the file or folder
:param str newpath: new name for the file or folder @type oldpath: str
@param newpath: new name for the file or folder
@type newpath: str
:raises IOError: if ``newpath`` is a folder, or something else goes @raise IOError: if C{newpath} is a folder, or something else goes
wrong wrong
""" """
oldpath = self._adjust_cwd(oldpath) oldpath = self._adjust_cwd(oldpath)
@ -282,14 +289,16 @@ class SFTPClient(BaseSFTP):
self._log(DEBUG, 'rename(%r, %r)' % (oldpath, newpath)) self._log(DEBUG, 'rename(%r, %r)' % (oldpath, newpath))
self._request(CMD_RENAME, oldpath, newpath) self._request(CMD_RENAME, oldpath, newpath)
def mkdir(self, path, mode=o777): def mkdir(self, path, mode= 0o777):
""" """
Create a folder (directory) named ``path`` with numeric mode ``mode``. Create a folder (directory) named C{path} with numeric mode C{mode}.
The default mode is 0777 (octal). On some systems, mode is ignored. The default mode is 0o777 (octal). On some systems, mode is ignored.
Where it is used, the current umask value is first masked out. Where it is used, the current umask value is first masked out.
:param str path: name of the folder to create @param path: name of the folder to create
:param int mode: permissions (posix-style) for the newly-created folder @type path: str
@param mode: permissions (posix-style) for the newly-created folder
@type mode: int
""" """
path = self._adjust_cwd(path) path = self._adjust_cwd(path)
self._log(DEBUG, 'mkdir(%r, %r)' % (path, mode)) self._log(DEBUG, 'mkdir(%r, %r)' % (path, mode))
@ -299,9 +308,10 @@ class SFTPClient(BaseSFTP):
def rmdir(self, path): def rmdir(self, path):
""" """
Remove the folder named ``path``. Remove the folder named C{path}.
:param str path: name of the folder to remove @param path: name of the folder to remove
@type path: str
""" """
path = self._adjust_cwd(path) path = self._adjust_cwd(path)
self._log(DEBUG, 'rmdir(%r)' % path) self._log(DEBUG, 'rmdir(%r)' % path)
@ -311,20 +321,20 @@ class SFTPClient(BaseSFTP):
""" """
Retrieve information about a file on the remote system. The return Retrieve information about a file on the remote system. The return
value is an object whose attributes correspond to the attributes of value is an object whose attributes correspond to the attributes of
Python's ``stat`` structure as returned by ``os.stat``, except that it python's C{stat} structure as returned by C{os.stat}, except that it
contains fewer fields. An SFTP server may return as much or as little contains fewer fields. An SFTP server may return as much or as little
info as it wants, so the results may vary from server to server. info as it wants, so the results may vary from server to server.
Unlike a Python `python:stat` object, the result may not be accessed as Unlike a python C{stat} object, the result may not be accessed as a
a tuple. This is mostly due to the author's slack factor. tuple. This is mostly due to the author's slack factor.
The fields supported are: ``st_mode``, ``st_size``, ``st_uid``, The fields supported are: C{st_mode}, C{st_size}, C{st_uid}, C{st_gid},
``st_gid``, ``st_atime``, and ``st_mtime``. C{st_atime}, and C{st_mtime}.
:param str path: the filename to stat @param path: the filename to stat
:return: @type path: str
an `.SFTPAttributes` object containing attributes about the given @return: an object containing attributes about the given file
file @rtype: SFTPAttributes
""" """
path = self._adjust_cwd(path) path = self._adjust_cwd(path)
self._log(DEBUG, 'stat(%r)' % path) self._log(DEBUG, 'stat(%r)' % path)
@ -337,12 +347,12 @@ class SFTPClient(BaseSFTP):
""" """
Retrieve information about a file on the remote system, without Retrieve information about a file on the remote system, without
following symbolic links (shortcuts). This otherwise behaves exactly following symbolic links (shortcuts). This otherwise behaves exactly
the same as `stat`. the same as L{stat}.
:param str path: the filename to stat @param path: the filename to stat
:return: @type path: str
an `.SFTPAttributes` object containing attributes about the given @return: an object containing attributes about the given file
file @rtype: SFTPAttributes
""" """
path = self._adjust_cwd(path) path = self._adjust_cwd(path)
self._log(DEBUG, 'lstat(%r)' % path) self._log(DEBUG, 'lstat(%r)' % path)
@ -353,25 +363,30 @@ class SFTPClient(BaseSFTP):
def symlink(self, source, dest): def symlink(self, source, dest):
""" """
Create a symbolic link (shortcut) of the ``source`` path at Create a symbolic link (shortcut) of the C{source} path at
``destination``. C{destination}.
:param str source: path of the original file @param source: path of the original file
:param str dest: path of the newly created symlink @type source: str
@param dest: path of the newly created symlink
@type dest: str
""" """
dest = self._adjust_cwd(dest) dest = self._adjust_cwd(dest)
self._log(DEBUG, 'symlink(%r, %r)' % (source, dest)) self._log(DEBUG, 'symlink(%r, %r)' % (source, dest))
source = bytestring(source) if isinstance(source, six.text_type):
source = source.encode('utf-8')
self._request(CMD_SYMLINK, source, dest) self._request(CMD_SYMLINK, source, dest)
def chmod(self, path, mode): def chmod(self, path, mode):
""" """
Change the mode (permissions) of a file. The permissions are Change the mode (permissions) of a file. The permissions are
unix-style and identical to those used by Python's `os.chmod` unix-style and identical to those used by python's C{os.chmod}
function. function.
:param str path: path of the file to change the permissions of @param path: path of the file to change the permissions of
:param int mode: new permissions @type path: str
@param mode: new permissions
@type mode: int
""" """
path = self._adjust_cwd(path) path = self._adjust_cwd(path)
self._log(DEBUG, 'chmod(%r, %r)' % (path, mode)) self._log(DEBUG, 'chmod(%r, %r)' % (path, mode))
@ -381,14 +396,17 @@ class SFTPClient(BaseSFTP):
def chown(self, path, uid, gid): def chown(self, path, uid, gid):
""" """
Change the owner (``uid``) and group (``gid``) of a file. As with Change the owner (C{uid}) and group (C{gid}) of a file. As with
Python's `os.chown` function, you must pass both arguments, so if you python's C{os.chown} function, you must pass both arguments, so if you
only want to change one, use `stat` first to retrieve the current only want to change one, use L{stat} first to retrieve the current
owner and group. owner and group.
:param str path: path of the file to change the owner and group of @param path: path of the file to change the owner and group of
:param int uid: new owner's uid @type path: str
:param int gid: new group id @param uid: new owner's uid
@type uid: int
@param gid: new group id
@type gid: int
""" """
path = self._adjust_cwd(path) path = self._adjust_cwd(path)
self._log(DEBUG, 'chown(%r, %r, %r)' % (path, uid, gid)) self._log(DEBUG, 'chown(%r, %r, %r)' % (path, uid, gid))
@ -398,17 +416,18 @@ class SFTPClient(BaseSFTP):
def utime(self, path, times): def utime(self, path, times):
""" """
Set the access and modified times of the file specified by ``path``. If Set the access and modified times of the file specified by C{path}. If
``times`` is ``None``, then the file's access and modified times are set C{times} is C{None}, then the file's access and modified times are set
to the current time. Otherwise, ``times`` must be a 2-tuple of numbers, to the current time. Otherwise, C{times} must be a 2-tuple of numbers,
of the form ``(atime, mtime)``, which is used to set the access and of the form C{(atime, mtime)}, which is used to set the access and
modified times, respectively. This bizarre API is mimicked from Python modified times, respectively. This bizarre API is mimicked from python
for the sake of consistency -- I apologize. for the sake of consistency -- I apologize.
:param str path: path of the file to modify @param path: path of the file to modify
:param tuple times: @type path: str
``None`` or a tuple of (access time, modified time) in standard @param times: C{None} or a tuple of (access time, modified time) in
internet epoch time (seconds since 01 January 1970 GMT) standard internet epoch time (seconds since 01 January 1970 GMT)
@type times: tuple(int)
""" """
path = self._adjust_cwd(path) path = self._adjust_cwd(path)
if times is None: if times is None:
@ -420,13 +439,14 @@ class SFTPClient(BaseSFTP):
def truncate(self, path, size): def truncate(self, path, size):
""" """
Change the size of the file specified by ``path``. This usually Change the size of the file specified by C{path}. This usually extends
extends or shrinks the size of the file, just like the `~file.truncate` or shrinks the size of the file, just like the C{truncate()} method on
method on Python file objects. python file objects.
:param str path: path of the file to modify @param path: path of the file to modify
:param size: the new size of the file @type path: str
:type size: int or long @param size: the new size of the file
@type size: int or long
""" """
path = self._adjust_cwd(path) path = self._adjust_cwd(path)
self._log(DEBUG, 'truncate(%r, %r)' % (path, size)) self._log(DEBUG, 'truncate(%r, %r)' % (path, size))
@ -437,11 +457,13 @@ class SFTPClient(BaseSFTP):
def readlink(self, path): def readlink(self, path):
""" """
Return the target of a symbolic link (shortcut). You can use Return the target of a symbolic link (shortcut). You can use
`symlink` to create these. The result may be either an absolute or L{symlink} to create these. The result may be either an absolute or
relative pathname. relative pathname.
:param str path: path of the symbolic link file @param path: path of the symbolic link file
:return: target path, as a `str` @type path: str
@return: target path
@rtype: str
""" """
path = self._adjust_cwd(path) path = self._adjust_cwd(path)
self._log(DEBUG, 'readlink(%r)' % path) self._log(DEBUG, 'readlink(%r)' % path)
@ -459,13 +481,15 @@ class SFTPClient(BaseSFTP):
""" """
Return the normalized path (on the server) of a given path. This Return the normalized path (on the server) of a given path. This
can be used to quickly resolve symbolic links or determine what the can be used to quickly resolve symbolic links or determine what the
server is considering to be the "current folder" (by passing ``'.'`` server is considering to be the "current folder" (by passing C{'.'}
as ``path``). as C{path}).
:param str path: path to be normalized @param path: path to be normalized
:return: normalized form of the given path (as a `str`) @type path: str
@return: normalized form of the given path
@rtype: str
:raises IOError: if the path can't be resolved on the server @raise IOError: if the path can't be resolved on the server
""" """
path = self._adjust_cwd(path) path = self._adjust_cwd(path)
self._log(DEBUG, 'normalize(%r)' % path) self._log(DEBUG, 'normalize(%r)' % path)
@ -475,72 +499,76 @@ class SFTPClient(BaseSFTP):
count = msg.get_int() count = msg.get_int()
if count != 1: if count != 1:
raise SFTPError('Realpath returned %d results' % count) raise SFTPError('Realpath returned %d results' % count)
return msg.get_text() return _to_unicode(msg.get_string())
def chdir(self, path=None): def chdir(self, path):
""" """
Change the "current directory" of this SFTP session. Since SFTP Change the "current directory" of this SFTP session. Since SFTP
doesn't really have the concept of a current working directory, this is doesn't really have the concept of a current working directory, this
emulated by Paramiko. Once you use this method to set a working is emulated by paramiko. Once you use this method to set a working
directory, all operations on this `.SFTPClient` object will be relative directory, all operations on this SFTPClient object will be relative
to that path. You can pass in ``None`` to stop using a current working to that path. You can pass in C{None} to stop using a current working
directory. directory.
:param str path: new current working directory @param path: new current working directory
@type path: str
:raises IOError: if the requested path doesn't exist on the server @raise IOError: if the requested path doesn't exist on the server
.. versionadded:: 1.4 @since: 1.4
""" """
if path is None: if path is None:
self._cwd = None self._cwd = None
return return
if not stat.S_ISDIR(self.stat(path).st_mode): if not stat.S_ISDIR(self.stat(path).st_mode):
raise SFTPError(errno.ENOTDIR, "%s: %s" % (os.strerror(errno.ENOTDIR), path)) raise SFTPError(errno.ENOTDIR, "%s: %s" % (os.strerror(errno.ENOTDIR), path))
self._cwd = b(self.normalize(path)) self._cwd = self.normalize(path).encode('utf-8')
def getcwd(self): def getcwd(self):
""" """
Return the "current working directory" for this SFTP session, as Return the "current working directory" for this SFTP session, as
emulated by Paramiko. If no directory has been set with `chdir`, emulated by paramiko. If no directory has been set with L{chdir},
this method will return ``None``. this method will return C{None}.
.. versionadded:: 1.4 @return: the current working directory on the server, or C{None}
@rtype: str
@since: 1.4
""" """
return self._cwd and u(self._cwd) return self._cwd
def putfo(self, fl, remotepath, file_size=0, callback=None, confirm=True): def putfo(self, fl, remotepath, file_size=0, callback=None, confirm=True):
""" """
Copy the contents of an open file object (``fl``) to the SFTP server as Copy the contents of an open file object (C{fl}) to the SFTP server as
``remotepath``. Any exception raised by operations will be passed C{remotepath}. Any exception raised by operations will be passed through.
through.
The SFTP operations use pipelining for speed. The SFTP operations use pipelining for speed.
:param file fl: opened file or file-like object to copy @param fl: opened file or file-like object to copy
:param str remotepath: the destination path on the SFTP server @type localpath: object
:param int file_size: @param remotepath: the destination path on the SFTP server
optional size parameter passed to callback. If none is specified, @type remotepath: str
size defaults to 0 @param file_size: optional size parameter passed to callback. If none is
:param callable callback: specified, size defaults to 0
optional callback function (form: ``func(int, int)``) that accepts @type file_size: int
the bytes transferred so far and the total bytes to be transferred @param callback: optional callback function that accepts the bytes
transferred so far and the total bytes to be transferred
(since 1.7.4) (since 1.7.4)
:param bool confirm: @type callback: function(int, int)
whether to do a stat() on the file afterwards to confirm the file @param confirm: whether to do a stat() on the file afterwards to
size (since 1.7.7) confirm the file size (since 1.7.7)
@type confirm: bool
:return: @return: an object containing attributes about the given file
an `.SFTPAttributes` object containing attributes about the given (since 1.7.4)
file. @rtype: SFTPAttributes
.. versionadded:: 1.4 @since: 1.4
.. versionchanged:: 1.7.4
Began returning rich attribute objects.
""" """
with self.file(remotepath, 'wb') as fr: fr = self.file(remotepath, 'wb')
fr.set_pipelined(True) fr.set_pipelined(True)
size = 0 size = 0
try:
while True: while True:
data = fl.read(32768) data = fl.read(32768)
fr.write(data) fr.write(data)
@ -549,6 +577,8 @@ class SFTPClient(BaseSFTP):
callback(size, file_size) callback(size, file_size)
if len(data) == 0: if len(data) == 0:
break break
finally:
fr.close()
if confirm: if confirm:
s = self.stat(remotepath) s = self.stat(remotepath)
if s.st_size != size: if s.st_size != size:
@ -559,55 +589,61 @@ class SFTPClient(BaseSFTP):
def put(self, localpath, remotepath, callback=None, confirm=True): def put(self, localpath, remotepath, callback=None, confirm=True):
""" """
Copy a local file (``localpath``) to the SFTP server as ``remotepath``. Copy a local file (C{localpath}) to the SFTP server as C{remotepath}.
Any exception raised by operations will be passed through. This Any exception raised by operations will be passed through. This
method is primarily provided as a convenience. method is primarily provided as a convenience.
The SFTP operations use pipelining for speed. The SFTP operations use pipelining for speed.
:param str localpath: the local file to copy @param localpath: the local file to copy
:param str remotepath: the destination path on the SFTP server @type localpath: str
:param callable callback: @param remotepath: the destination path on the SFTP server
optional callback function (form: ``func(int, int)``) that accepts @type remotepath: str
the bytes transferred so far and the total bytes to be transferred @param callback: optional callback function that accepts the bytes
:param bool confirm: transferred so far and the total bytes to be transferred
whether to do a stat() on the file afterwards to confirm the file (since 1.7.4)
size @type callback: function(int, int)
@param confirm: whether to do a stat() on the file afterwards to
confirm the file size (since 1.7.7)
@type confirm: bool
:return: an `.SFTPAttributes` object containing attributes about the given file @return: an object containing attributes about the given file
(since 1.7.4)
@rtype: SFTPAttributes
.. versionadded:: 1.4 @since: 1.4
.. versionchanged:: 1.7.4
``callback`` and rich attribute return value added.
.. versionchanged:: 1.7.7
``confirm`` param added.
""" """
file_size = os.stat(localpath).st_size file_size = os.stat(localpath).st_size
with open(localpath, 'rb') as fl: fl = file(localpath, 'rb')
try:
return self.putfo(fl, remotepath, os.stat(localpath).st_size, callback, confirm) return self.putfo(fl, remotepath, os.stat(localpath).st_size, callback, confirm)
finally:
fl.close()
def getfo(self, remotepath, fl, callback=None): def getfo(self, remotepath, fl, callback=None):
""" """
Copy a remote file (``remotepath``) from the SFTP server and write to Copy a remote file (C{remotepath}) from the SFTP server and write to
an open file or file-like object, ``fl``. Any exception raised by an open file or file-like object, C{fl}. Any exception raised by
operations will be passed through. This method is primarily provided operations will be passed through. This method is primarily provided
as a convenience. as a convenience.
:param object remotepath: opened file or file-like object to copy to @param remotepath: opened file or file-like object to copy to
:param str fl: @type remotepath: object
the destination path on the local host or open file object @param fl: the destination path on the local host or open file
:param callable callback: object
optional callback function (form: ``func(int, int)``) that accepts @type localpath: str
the bytes transferred so far and the total bytes to be transferred @param callback: optional callback function that accepts the bytes
:return: the `number <int>` of bytes written to the opened file object transferred so far and the total bytes to be transferred
(since 1.7.4)
@type callback: function(int, int)
@return: the number of bytes written to the opened file object
.. versionadded:: 1.4 @since: 1.4
.. versionchanged:: 1.7.4
Added the ``callable`` param.
""" """
with self.open(remotepath, 'rb') as fr: fr = self.file(remotepath, 'rb')
file_size = self.stat(remotepath).st_size file_size = self.stat(remotepath).st_size
fr.prefetch() fr.prefetch()
try:
size = 0 size = 0
while True: while True:
data = fr.read(32768) data = fr.read(32768)
@ -617,33 +653,41 @@ class SFTPClient(BaseSFTP):
callback(size, file_size) callback(size, file_size)
if len(data) == 0: if len(data) == 0:
break break
finally:
fr.close()
return size return size
def get(self, remotepath, localpath, callback=None): def get(self, remotepath, localpath, callback=None):
""" """
Copy a remote file (``remotepath``) from the SFTP server to the local Copy a remote file (C{remotepath}) from the SFTP server to the local
host as ``localpath``. Any exception raised by operations will be host as C{localpath}. Any exception raised by operations will be
passed through. This method is primarily provided as a convenience. passed through. This method is primarily provided as a convenience.
:param str remotepath: the remote file to copy @param remotepath: the remote file to copy
:param str localpath: the destination path on the local host @type remotepath: str
:param callable callback: @param localpath: the destination path on the local host
optional callback function (form: ``func(int, int)``) that accepts @type localpath: str
the bytes transferred so far and the total bytes to be transferred @param callback: optional callback function that accepts the bytes
transferred so far and the total bytes to be transferred
(since 1.7.4)
@type callback: function(int, int)
.. versionadded:: 1.4 @since: 1.4
.. versionchanged:: 1.7.4
Added the ``callback`` param
""" """
file_size = self.stat(remotepath).st_size file_size = self.stat(remotepath).st_size
with open(localpath, 'wb') as fl: fl = file(localpath, 'wb')
try:
size = self.getfo(remotepath, fl, callback) size = self.getfo(remotepath, fl, callback)
finally:
fl.close()
s = os.stat(localpath) s = os.stat(localpath)
if s.st_size != size: if s.st_size != size:
raise IOError('size mismatch in get! %d != %d' % (s.st_size, size)) raise IOError('size mismatch in get! %d != %d' % (s.st_size, size))
### internals... ### internals...
def _request(self, t, *arg): def _request(self, t, *arg):
num = self._async_request(type(None), t, *arg) num = self._async_request(type(None), t, *arg)
return self._read_response(num) return self._read_response(num)
@ -655,11 +699,11 @@ class SFTPClient(BaseSFTP):
msg = Message() msg = Message()
msg.add_int(self.request_number) msg.add_int(self.request_number)
for item in arg: for item in arg:
if isinstance(item, long): if isinstance(item, int):
msg.add_int64(item)
elif isinstance(item, int):
msg.add_int(item) msg.add_int(item)
elif isinstance(item, (string_types, bytes_types)): elif isinstance(item, long):
msg.add_int64(item)
elif isinstance(item, str):
msg.add_string(item) msg.add_string(item)
elif isinstance(item, SFTPAttributes): elif isinstance(item, SFTPAttributes):
item._pack(msg) item._pack(msg)
@ -667,7 +711,7 @@ class SFTPClient(BaseSFTP):
raise Exception('unknown type for %r type %r' % (item, type(item))) raise Exception('unknown type for %r type %r' % (item, type(item)))
num = self.request_number num = self.request_number
self._expecting[num] = fileobj self._expecting[num] = fileobj
self._send_packet(t, msg) self._send_packet(t, str(msg))
self.request_number += 1 self.request_number += 1
finally: finally:
self._lock.release() self._lock.release()
@ -678,7 +722,7 @@ class SFTPClient(BaseSFTP):
try: try:
t, data = self._read_packet() t, data = self._read_packet()
except EOFError as e: except EOFError as e:
raise SSHException('Server connection dropped: %s' % str(e)) raise SSHException('Server connection dropped: %s' % (str(e),))
msg = Message(data) msg = Message(data)
num = msg.get_int() num = msg.get_int()
if num not in self._expecting: if num not in self._expecting:
@ -696,11 +740,11 @@ class SFTPClient(BaseSFTP):
self._convert_status(msg) self._convert_status(msg)
return t, msg return t, msg
if fileobj is not type(None): if fileobj is not type(None):
fileobj._async_response(t, msg, num) fileobj._async_response(t, msg)
if waitfor is None: if waitfor is None:
# just doing a single check # just doing a single check
break break
return None, None return (None, None)
def _finish_responses(self, fileobj): def _finish_responses(self, fileobj):
while fileobj in self._expecting.values(): while fileobj in self._expecting.values():
@ -712,7 +756,7 @@ class SFTPClient(BaseSFTP):
Raises EOFError or IOError on error status; otherwise does nothing. Raises EOFError or IOError on error status; otherwise does nothing.
""" """
code = msg.get_int() code = msg.get_int()
text = msg.get_text() text = msg.get_string()
if code == SFTP_OK: if code == SFTP_OK:
return return
elif code == SFTP_EOF: elif code == SFTP_EOF:
@ -730,19 +774,18 @@ class SFTPClient(BaseSFTP):
Return an adjusted path if we're emulating a "current working Return an adjusted path if we're emulating a "current working
directory" for the server. directory" for the server.
""" """
path = b(path) if isinstance(path, six.text_type):
path = path.encode('utf-8')
if self._cwd is None: if self._cwd is None:
return path return path
if len(path) and path[0:1] == b_slash: if (len(path) > 0) and (path[0] == '/'):
# absolute path # absolute path
return path return path
if self._cwd == b_slash: if self._cwd == '/':
return self._cwd + path return self._cwd + path
return self._cwd + b_slash + path return self._cwd + '/' + path
class SFTP(SFTPClient): class SFTP (SFTPClient):
""" "an alias for L{SFTPClient} for backwards compatability"
An alias for `.SFTPClient` for backwards compatability.
"""
pass pass

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -17,22 +17,22 @@
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
""" """
SFTP file object L{SFTPFile}
""" """
from __future__ import with_statement import six
if six.PY3:
long = lambda x: int(x)
from binascii import hexlify from binascii import hexlify
from collections import deque from collections import deque
import socket import socket
import threading import threading
import time import time
from paramiko.common import DEBUG
from paramiko.common import *
from paramiko.sftp import *
from paramiko.file import BufferedFile from paramiko.file import BufferedFile
from paramiko.py3compat import long
from paramiko.sftp import CMD_CLOSE, CMD_READ, CMD_DATA, SFTPError, CMD_WRITE, \
CMD_STATUS, CMD_FSTAT, CMD_ATTRS, CMD_FSETSTAT, CMD_EXTENDED
from paramiko.sftp_attr import SFTPAttributes from paramiko.sftp_attr import SFTPAttributes
@ -57,8 +57,7 @@ class SFTPFile (BufferedFile):
self._prefetching = False self._prefetching = False
self._prefetch_done = False self._prefetch_done = False
self._prefetch_data = {} self._prefetch_data = {}
self._prefetch_extents = {} self._prefetch_reads = []
self._prefetch_lock = threading.Lock()
self._saved_exception = None self._saved_exception = None
self._reqs = deque() self._reqs = deque()
@ -66,9 +65,6 @@ class SFTPFile (BufferedFile):
self._close(async=True) self._close(async=True)
def close(self): def close(self):
"""
Close the file.
"""
self._close(async=False) self._close(async=False)
def _close(self, async=False): def _close(self, async=False):
@ -99,10 +95,10 @@ class SFTPFile (BufferedFile):
pass pass
def _data_in_prefetch_requests(self, offset, size): def _data_in_prefetch_requests(self, offset, size):
k = [x for x in list(self._prefetch_extents.values()) if x[0] <= offset] k = [i for i in self._prefetch_reads if i[0] <= offset]
if len(k) == 0: if len(k) == 0:
return False return False
k.sort(key=lambda x: x[0]) k.sort(lambda x, y: cmp(x[0], y[0]))
buf_offset, buf_size = k[-1] buf_offset, buf_size = k[-1]
if buf_offset + buf_size <= offset: if buf_offset + buf_size <= offset:
# prefetch request ends before this one begins # prefetch request ends before this one begins
@ -173,7 +169,7 @@ class SFTPFile (BufferedFile):
def _write(self, data): def _write(self, data):
# may write less than requested if it would exceed max packet size # may write less than requested if it would exceed max packet size
chunk = min(len(data), self.MAX_REQUEST_SIZE) chunk = min(len(data), self.MAX_REQUEST_SIZE)
self._reqs.append(self.sftp._async_request(type(None), CMD_WRITE, self.handle, long(self._realpos), data[:chunk])) self._reqs.append(self.sftp._async_request(type(None), CMD_WRITE, self.handle, long(self._realpos), str(data[:chunk])))
if not self.pipelined or (len(self._reqs) > 100 and self.sftp.sock.recv_ready()): if not self.pipelined or (len(self._reqs) > 100 and self.sftp.sock.recv_ready()):
while len(self._reqs): while len(self._reqs):
req = self._reqs.popleft() req = self._reqs.popleft()
@ -186,34 +182,34 @@ class SFTPFile (BufferedFile):
def settimeout(self, timeout): def settimeout(self, timeout):
""" """
Set a timeout on read/write operations on the underlying socket or Set a timeout on read/write operations on the underlying socket or
ssh `.Channel`. ssh L{Channel}.
:param float timeout: @see: L{Channel.settimeout}
seconds to wait for a pending read/write operation before raising @param timeout: seconds to wait for a pending read/write operation
``socket.timeout``, or ``None`` for no timeout before raising C{socket.timeout}, or C{None} for no timeout
@type timeout: float
.. seealso:: `.Channel.settimeout`
""" """
self.sftp.sock.settimeout(timeout) self.sftp.sock.settimeout(timeout)
def gettimeout(self): def gettimeout(self):
""" """
Returns the timeout in seconds (as a `float`) associated with the Returns the timeout in seconds (as a float) associated with the socket
socket or ssh `.Channel` used for this file. or ssh L{Channel} used for this file.
.. seealso:: `.Channel.gettimeout` @see: L{Channel.gettimeout}
@rtype: float
""" """
return self.sftp.sock.gettimeout() return self.sftp.sock.gettimeout()
def setblocking(self, blocking): def setblocking(self, blocking):
""" """
Set blocking or non-blocking mode on the underiying socket or ssh Set blocking or non-blocking mode on the underiying socket or ssh
`.Channel`. L{Channel}.
:param int blocking: @see: L{Channel.setblocking}
0 to set non-blocking mode; non-0 to set blocking mode. @param blocking: 0 to set non-blocking mode; non-0 to set blocking
mode.
.. seealso:: `.Channel.setblocking` @type blocking: int
""" """
self.sftp.sock.setblocking(blocking) self.sftp.sock.setblocking(blocking)
@ -226,15 +222,16 @@ class SFTPFile (BufferedFile):
self._realpos = self._pos self._realpos = self._pos
else: else:
self._realpos = self._pos = self._get_size() + offset self._realpos = self._pos = self._get_size() + offset
self._rbuffer = bytes() self._rbuffer = ''
def stat(self): def stat(self):
""" """
Retrieve information about this file from the remote system. This is Retrieve information about this file from the remote system. This is
exactly like `.SFTPClient.stat`, except that it operates on an exactly like L{SFTP.stat}, except that it operates on an already-open
already-open file. file.
:return: an `.SFTPAttributes` object containing attributes about this file. @return: an object containing attributes about this file.
@rtype: SFTPAttributes
""" """
t, msg = self.sftp._request(CMD_FSTAT, self.handle) t, msg = self.sftp._request(CMD_FSTAT, self.handle)
if t != CMD_ATTRS: if t != CMD_ATTRS:
@ -244,10 +241,11 @@ class SFTPFile (BufferedFile):
def chmod(self, mode): def chmod(self, mode):
""" """
Change the mode (permissions) of this file. The permissions are Change the mode (permissions) of this file. The permissions are
unix-style and identical to those used by Python's `os.chmod` unix-style and identical to those used by python's C{os.chmod}
function. function.
:param int mode: new permissions @param mode: new permissions
@type mode: int
""" """
self.sftp._log(DEBUG, 'chmod(%s, %r)' % (hexlify(self.handle), mode)) self.sftp._log(DEBUG, 'chmod(%s, %r)' % (hexlify(self.handle), mode))
attr = SFTPAttributes() attr = SFTPAttributes()
@ -256,13 +254,15 @@ class SFTPFile (BufferedFile):
def chown(self, uid, gid): def chown(self, uid, gid):
""" """
Change the owner (``uid``) and group (``gid``) of this file. As with Change the owner (C{uid}) and group (C{gid}) of this file. As with
Python's `os.chown` function, you must pass both arguments, so if you python's C{os.chown} function, you must pass both arguments, so if you
only want to change one, use `stat` first to retrieve the current only want to change one, use L{stat} first to retrieve the current
owner and group. owner and group.
:param int uid: new owner's uid @param uid: new owner's uid
:param int gid: new group id @type uid: int
@param gid: new group id
@type gid: int
""" """
self.sftp._log(DEBUG, 'chown(%s, %r, %r)' % (hexlify(self.handle), uid, gid)) self.sftp._log(DEBUG, 'chown(%s, %r, %r)' % (hexlify(self.handle), uid, gid))
attr = SFTPAttributes() attr = SFTPAttributes()
@ -272,15 +272,15 @@ class SFTPFile (BufferedFile):
def utime(self, times): def utime(self, times):
""" """
Set the access and modified times of this file. If Set the access and modified times of this file. If
``times`` is ``None``, then the file's access and modified times are set C{times} is C{None}, then the file's access and modified times are set
to the current time. Otherwise, ``times`` must be a 2-tuple of numbers, to the current time. Otherwise, C{times} must be a 2-tuple of numbers,
of the form ``(atime, mtime)``, which is used to set the access and of the form C{(atime, mtime)}, which is used to set the access and
modified times, respectively. This bizarre API is mimicked from Python modified times, respectively. This bizarre API is mimicked from python
for the sake of consistency -- I apologize. for the sake of consistency -- I apologize.
:param tuple times: @param times: C{None} or a tuple of (access time, modified time) in
``None`` or a tuple of (access time, modified time) in standard standard internet epoch time (seconds since 01 January 1970 GMT)
internet epoch time (seconds since 01 January 1970 GMT) @type times: tuple(int)
""" """
if times is None: if times is None:
times = (time.time(), time.time()) times = (time.time(), time.time())
@ -292,11 +292,11 @@ class SFTPFile (BufferedFile):
def truncate(self, size): def truncate(self, size):
""" """
Change the size of this file. This usually extends Change the size of this file. This usually extends
or shrinks the size of the file, just like the ``truncate()`` method on or shrinks the size of the file, just like the C{truncate()} method on
Python file objects. python file objects.
:param size: the new size of the file @param size: the new size of the file
:type size: int or long @type size: int or long
""" """
self.sftp._log(DEBUG, 'truncate(%s, %r)' % (hexlify(self.handle), size)) self.sftp._log(DEBUG, 'truncate(%s, %r)' % (hexlify(self.handle), size))
attr = SFTPAttributes() attr = SFTPAttributes()
@ -309,53 +309,51 @@ class SFTPFile (BufferedFile):
to verify a successful upload or download, or for various rsync-like to verify a successful upload or download, or for various rsync-like
operations. operations.
The file is hashed from ``offset``, for ``length`` bytes. If ``length`` The file is hashed from C{offset}, for C{length} bytes. If C{length}
is 0, the remainder of the file is hashed. Thus, if both ``offset`` is 0, the remainder of the file is hashed. Thus, if both C{offset}
and ``length`` are zero, the entire file is hashed. and C{length} are zero, the entire file is hashed.
Normally, ``block_size`` will be 0 (the default), and this method will Normally, C{block_size} will be 0 (the default), and this method will
return a byte string representing the requested hash (for example, a return a byte string representing the requested hash (for example, a
string of length 16 for MD5, or 20 for SHA-1). If a non-zero string of length 16 for MD5, or 20 for SHA-1). If a non-zero
``block_size`` is given, each chunk of the file (from ``offset`` to C{block_size} is given, each chunk of the file (from C{offset} to
``offset + length``) of ``block_size`` bytes is computed as a separate C{offset + length}) of C{block_size} bytes is computed as a separate
hash. The hash results are all concatenated and returned as a single hash. The hash results are all concatenated and returned as a single
string. string.
For example, ``check('sha1', 0, 1024, 512)`` will return a string of For example, C{check('sha1', 0, 1024, 512)} will return a string of
length 40. The first 20 bytes will be the SHA-1 of the first 512 bytes length 40. The first 20 bytes will be the SHA-1 of the first 512 bytes
of the file, and the last 20 bytes will be the SHA-1 of the next 512 of the file, and the last 20 bytes will be the SHA-1 of the next 512
bytes. bytes.
:param str hash_algorithm: @param hash_algorithm: the name of the hash algorithm to use (normally
the name of the hash algorithm to use (normally ``"sha1"`` or C{"sha1"} or C{"md5"})
``"md5"``) @type hash_algorithm: str
:param offset: @param offset: offset into the file to begin hashing (0 means to start
offset into the file to begin hashing (0 means to start from the from the beginning)
beginning) @type offset: int or long
:type offset: int or long @param length: number of bytes to hash (0 means continue to the end of
:param length: the file)
number of bytes to hash (0 means continue to the end of the file) @type length: int or long
:type length: int or long @param block_size: number of bytes to hash per result (must not be less
:param int block_size: than 256; 0 means to compute only one hash of the entire segment)
number of bytes to hash per result (must not be less than 256; 0 @type block_size: int
means to compute only one hash of the entire segment) @return: string of bytes representing the hash of each block,
:type block_size: int concatenated together
:return: @rtype: str
`str` of bytes representing the hash of each block, concatenated
together
:raises IOError: if the server doesn't support the "check-file" @note: Many (most?) servers don't support this extension yet.
@raise IOError: if the server doesn't support the "check-file"
extension, or possibly doesn't support the hash algorithm extension, or possibly doesn't support the hash algorithm
requested requested
.. note:: Many (most?) servers don't support this extension yet. @since: 1.4
.. versionadded:: 1.4
""" """
t, msg = self.sftp._request(CMD_EXTENDED, 'check-file', self.handle, t, msg = self.sftp._request(CMD_EXTENDED, 'check-file', self.handle,
hash_algorithm, long(offset), long(length), block_size) hash_algorithm, long(offset), long(length), block_size)
ext = msg.get_text() ext = msg.get_string()
alg = msg.get_text() alg = msg.get_string()
data = msg.get_remainder() data = msg.get_remainder()
return data return data
@ -363,35 +361,35 @@ class SFTPFile (BufferedFile):
""" """
Turn on/off the pipelining of write operations to this file. When Turn on/off the pipelining of write operations to this file. When
pipelining is on, paramiko won't wait for the server response after pipelining is on, paramiko won't wait for the server response after
each write operation. Instead, they're collected as they come in. At each write operation. Instead, they're collected as they come in.
the first non-write operation (including `.close`), all remaining At the first non-write operation (including L{close}), all remaining
server responses are collected. This means that if there was an error server responses are collected. This means that if there was an error
with one of your later writes, an exception might be thrown from within with one of your later writes, an exception might be thrown from
`.close` instead of `.write`. within L{close} instead of L{write}.
By default, files are not pipelined. By default, files are I{not} pipelined.
:param bool pipelined: @param pipelined: C{True} if pipelining should be turned on for this
``True`` if pipelining should be turned on for this file; ``False`` file; C{False} otherwise
otherwise @type pipelined: bool
.. versionadded:: 1.5 @since: 1.5
""" """
self.pipelined = pipelined self.pipelined = pipelined
def prefetch(self): def prefetch(self):
""" """
Pre-fetch the remaining contents of this file in anticipation of future Pre-fetch the remaining contents of this file in anticipation of
`.read` calls. If reading the entire file, pre-fetching can future L{read} calls. If reading the entire file, pre-fetching can
dramatically improve the download speed by avoiding roundtrip latency. dramatically improve the download speed by avoiding roundtrip latency.
The file's contents are incrementally buffered in a background thread. The file's contents are incrementally buffered in a background thread.
The prefetched data is stored in a buffer until read via the `.read` The prefetched data is stored in a buffer until read via the L{read}
method. Once data has been read, it's removed from the buffer. The method. Once data has been read, it's removed from the buffer. The
data may be read in a random order (using `.seek`); chunks of the data may be read in a random order (using L{seek}); chunks of the
buffer that haven't been read will continue to be buffered. buffer that haven't been read will continue to be buffered.
.. versionadded:: 1.5.1 @since: 1.5.1
""" """
size = self.stat().st_size size = self.stat().st_size
# queue up async reads for the rest of the file # queue up async reads for the rest of the file
@ -407,17 +405,17 @@ class SFTPFile (BufferedFile):
def readv(self, chunks): def readv(self, chunks):
""" """
Read a set of blocks from the file by (offset, length). This is more Read a set of blocks from the file by (offset, length). This is more
efficient than doing a series of `.seek` and `.read` calls, since the efficient than doing a series of L{seek} and L{read} calls, since the
prefetch machinery is used to retrieve all the requested blocks at prefetch machinery is used to retrieve all the requested blocks at
once. once.
:param chunks: @param chunks: a list of (offset, length) tuples indicating which
a list of (offset, length) tuples indicating which sections of the sections of the file to read
file to read @type chunks: list(tuple(long, int))
:type chunks: list(tuple(long, int)) @return: a list of blocks read, in the same order as in C{chunks}
:return: a list of blocks read, in the same order as in ``chunks`` @rtype: list(str)
.. versionadded:: 1.5.4 @since: 1.5.4
""" """
self.sftp._log(DEBUG, 'readv(%s, %r)' % (hexlify(self.handle), chunks)) self.sftp._log(DEBUG, 'readv(%s, %r)' % (hexlify(self.handle), chunks))
@ -439,9 +437,11 @@ class SFTPFile (BufferedFile):
for x in chunks: for x in chunks:
self.seek(x[0]) self.seek(x[0])
yield self.read(x[1]) yield self.read(x[1])
### internals... ### internals...
def _get_size(self): def _get_size(self):
try: try:
return self.stat().st_size return self.stat().st_size
@ -451,6 +451,7 @@ class SFTPFile (BufferedFile):
def _start_prefetch(self, chunks): def _start_prefetch(self, chunks):
self._prefetching = True self._prefetching = True
self._prefetch_done = False self._prefetch_done = False
self._prefetch_reads.extend(chunks)
t = threading.Thread(target=self._prefetch_thread, args=(chunks,)) t = threading.Thread(target=self._prefetch_thread, args=(chunks,))
t.setDaemon(True) t.setDaemon(True)
@ -460,30 +461,26 @@ class SFTPFile (BufferedFile):
# do these read requests in a temporary thread because there may be # do these read requests in a temporary thread because there may be
# a lot of them, so it may block. # a lot of them, so it may block.
for offset, length in chunks: for offset, length in chunks:
with self._prefetch_lock: self.sftp._async_request(self, CMD_READ, self.handle, long(offset), int(length))
num = self.sftp._async_request(self, CMD_READ, self.handle, long(offset), int(length))
self._prefetch_extents[num] = (offset, length)
def _async_response(self, t, msg, num): def _async_response(self, t, msg):
if t == CMD_STATUS: if t == CMD_STATUS:
# save exception and re-raise it on next file operation # save exception and re-raise it on next file operation
try: try:
self.sftp._convert_status(msg) self.sftp._convert_status(msg)
except Exception as e: except Exception as x:
self._saved_exception = e self._saved_exception = x
return return
if t != CMD_DATA: if t != CMD_DATA:
raise SFTPError('Expected data') raise SFTPError('Expected data')
data = msg.get_string() data = msg.get_string()
with self._prefetch_lock: offset, length = self._prefetch_reads.pop(0)
offset, length = self._prefetch_extents[num] self._prefetch_data[offset] = data
self._prefetch_data[offset] = data if len(self._prefetch_reads) == 0:
del self._prefetch_extents[num] self._prefetch_done = True
if len(self._prefetch_extents) == 0:
self._prefetch_done = True
def _check_exception(self): def _check_exception(self):
"""if there's a saved exception, raise & clear it""" "if there's a saved exception, raise & clear it"
if self._saved_exception is not None: if self._saved_exception is not None:
x = self._saved_exception x = self._saved_exception
self._saved_exception = None self._saved_exception = None

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -21,7 +21,9 @@ Abstraction of an SFTP file handle (for server mode).
""" """
import os import os
from paramiko.sftp import SFTP_OP_UNSUPPORTED, SFTP_OK
from paramiko.common import *
from paramiko.sftp import *
class SFTPHandle (object): class SFTPHandle (object):
@ -31,20 +33,21 @@ class SFTPHandle (object):
by the client to refer to the underlying file. by the client to refer to the underlying file.
Server implementations can (and should) subclass SFTPHandle to implement Server implementations can (and should) subclass SFTPHandle to implement
features of a file handle, like `stat` or `chattr`. features of a file handle, like L{stat} or L{chattr}.
""" """
def __init__(self, flags=0): def __init__(self, flags=0):
""" """
Create a new file handle representing a local file being served over Create a new file handle representing a local file being served over
SFTP. If ``flags`` is passed in, it's used to determine if the file SFTP. If C{flags} is passed in, it's used to determine if the file
is open in append mode. is open in append mode.
:param int flags: optional flags as passed to `.SFTPServerInterface.open` @param flags: optional flags as passed to L{SFTPServerInterface.open}
@type flags: int
""" """
self.__flags = flags self.__flags = flags
self.__name = None self.__name = None
# only for handles to folders: # only for handles to folders:
self.__files = {} self.__files = { }
self.__tell = None self.__tell = None
def close(self): def close(self):
@ -53,10 +56,10 @@ class SFTPHandle (object):
Normally you would use this method to close the underlying OS level Normally you would use this method to close the underlying OS level
file object(s). file object(s).
The default implementation checks for attributes on ``self`` named The default implementation checks for attributes on C{self} named
``readfile`` and/or ``writefile``, and if either or both are present, C{readfile} and/or C{writefile}, and if either or both are present,
their ``close()`` methods are called. This means that if you are their C{close()} methods are called. This means that if you are
using the default implementations of `read` and `write`, this using the default implementations of L{read} and L{write}, this
method's default implementation should be fine also. method's default implementation should be fine also.
""" """
readfile = getattr(self, 'readfile', None) readfile = getattr(self, 'readfile', None)
@ -68,22 +71,24 @@ class SFTPHandle (object):
def read(self, offset, length): def read(self, offset, length):
""" """
Read up to ``length`` bytes from this file, starting at position Read up to C{length} bytes from this file, starting at position
``offset``. The offset may be a Python long, since SFTP allows it C{offset}. The offset may be a python long, since SFTP allows it
to be 64 bits. to be 64 bits.
If the end of the file has been reached, this method may return an If the end of the file has been reached, this method may return an
empty string to signify EOF, or it may also return `.SFTP_EOF`. empty string to signify EOF, or it may also return L{SFTP_EOF}.
The default implementation checks for an attribute on ``self`` named The default implementation checks for an attribute on C{self} named
``readfile``, and if present, performs the read operation on the Python C{readfile}, and if present, performs the read operation on the python
file-like object found there. (This is meant as a time saver for the file-like object found there. (This is meant as a time saver for the
common case where you are wrapping a Python file object.) common case where you are wrapping a python file object.)
:param offset: position in the file to start reading from. @param offset: position in the file to start reading from.
:type offset: int or long @type offset: int or long
:param int length: number of bytes to attempt to read. @param length: number of bytes to attempt to read.
:return: data read from the file, or an SFTP error code, as a `str`. @type length: int
@return: data read from the file, or an SFTP error code.
@rtype: str
""" """
readfile = getattr(self, 'readfile', None) readfile = getattr(self, 'readfile', None)
if readfile is None: if readfile is None:
@ -103,22 +108,23 @@ class SFTPHandle (object):
def write(self, offset, data): def write(self, offset, data):
""" """
Write ``data`` into this file at position ``offset``. Extending the Write C{data} into this file at position C{offset}. Extending the
file past its original end is expected. Unlike Python's normal file past its original end is expected. Unlike python's normal
``write()`` methods, this method cannot do a partial write: it must C{write()} methods, this method cannot do a partial write: it must
write all of ``data`` or else return an error. write all of C{data} or else return an error.
The default implementation checks for an attribute on ``self`` named The default implementation checks for an attribute on C{self} named
``writefile``, and if present, performs the write operation on the C{writefile}, and if present, performs the write operation on the
Python file-like object found there. The attribute is named python file-like object found there. The attribute is named
differently from ``readfile`` to make it easy to implement read-only differently from C{readfile} to make it easy to implement read-only
(or write-only) files, but if both attributes are present, they should (or write-only) files, but if both attributes are present, they should
refer to the same file. refer to the same file.
:param offset: position in the file to start reading from. @param offset: position in the file to start reading from.
:type offset: int or long @type offset: int or long
:param str data: data to write into the file. @param data: data to write into the file.
:return: an SFTP error code like `.SFTP_OK`. @type data: str
@return: an SFTP error code like L{SFTP_OK}.
""" """
writefile = getattr(self, 'writefile', None) writefile = getattr(self, 'writefile', None)
if writefile is None: if writefile is None:
@ -142,30 +148,33 @@ class SFTPHandle (object):
def stat(self): def stat(self):
""" """
Return an `.SFTPAttributes` object referring to this open file, or an Return an L{SFTPAttributes} object referring to this open file, or an
error code. This is equivalent to `.SFTPServerInterface.stat`, except error code. This is equivalent to L{SFTPServerInterface.stat}, except
it's called on an open file instead of a path. it's called on an open file instead of a path.
:return: @return: an attributes object for the given file, or an SFTP error
an attributes object for the given file, or an SFTP error code code (like L{SFTP_PERMISSION_DENIED}).
(like `.SFTP_PERMISSION_DENIED`). @rtype: L{SFTPAttributes} I{or error code}
:rtype: `.SFTPAttributes` or error code
""" """
return SFTP_OP_UNSUPPORTED return SFTP_OP_UNSUPPORTED
def chattr(self, attr): def chattr(self, attr):
""" """
Change the attributes of this file. The ``attr`` object will contain Change the attributes of this file. The C{attr} object will contain
only those fields provided by the client in its request, so you should only those fields provided by the client in its request, so you should
check for the presence of fields before using them. check for the presence of fields before using them.
:param .SFTPAttributes attr: the attributes to change on this file. @param attr: the attributes to change on this file.
:return: an `int` error code like `.SFTP_OK`. @type attr: L{SFTPAttributes}
@return: an error code like L{SFTP_OK}.
@rtype: int
""" """
return SFTP_OP_UNSUPPORTED return SFTP_OP_UNSUPPORTED
### internals... ### internals...
def _set_files(self, files): def _set_files(self, files):
""" """
Used by the SFTP server code to cache a directory listing. (In Used by the SFTP server code to cache a directory listing. (In

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -22,55 +22,46 @@ Server-mode SFTP support.
import os import os
import errno import errno
import sys
from hashlib import md5, sha1
from paramiko import util from Crypto.Hash import MD5, SHA
from paramiko.sftp import BaseSFTP, Message, SFTP_FAILURE, \ from paramiko.common import *
SFTP_PERMISSION_DENIED, SFTP_NO_SUCH_FILE
from paramiko.sftp_si import SFTPServerInterface
from paramiko.sftp_attr import SFTPAttributes
from paramiko.common import DEBUG
from paramiko.py3compat import long, string_types, bytes_types, b
from paramiko.server import SubsystemHandler from paramiko.server import SubsystemHandler
from paramiko.sftp import *
from paramiko.sftp_si import *
from paramiko.sftp_attr import *
# known hash algorithms for the "check-file" extension # known hash algorithms for the "check-file" extension
from paramiko.sftp import CMD_HANDLE, SFTP_DESC, CMD_STATUS, SFTP_EOF, CMD_NAME, \
SFTP_BAD_MESSAGE, CMD_EXTENDED_REPLY, SFTP_FLAG_READ, SFTP_FLAG_WRITE, \
SFTP_FLAG_APPEND, SFTP_FLAG_CREATE, SFTP_FLAG_TRUNC, SFTP_FLAG_EXCL, \
CMD_NAMES, CMD_OPEN, CMD_CLOSE, SFTP_OK, CMD_READ, CMD_DATA, CMD_WRITE, \
CMD_REMOVE, CMD_RENAME, CMD_MKDIR, CMD_RMDIR, CMD_OPENDIR, CMD_READDIR, \
CMD_STAT, CMD_ATTRS, CMD_LSTAT, CMD_FSTAT, CMD_SETSTAT, CMD_FSETSTAT, \
CMD_READLINK, CMD_SYMLINK, CMD_REALPATH, CMD_EXTENDED, SFTP_OP_UNSUPPORTED
_hash_class = { _hash_class = {
'sha1': sha1, 'sha1': SHA,
'md5': md5, 'md5': MD5,
} }
class SFTPServer (BaseSFTP, SubsystemHandler): class SFTPServer (BaseSFTP, SubsystemHandler):
""" """
Server-side SFTP subsystem support. Since this is a `.SubsystemHandler`, Server-side SFTP subsystem support. Since this is a L{SubsystemHandler},
it can be (and is meant to be) set as the handler for ``"sftp"`` requests. it can be (and is meant to be) set as the handler for C{"sftp"} requests.
Use `.Transport.set_subsystem_handler` to activate this class. Use L{Transport.set_subsystem_handler} to activate this class.
""" """
def __init__(self, channel, name, server, sftp_si=SFTPServerInterface, *largs, **kwargs): def __init__(self, channel, name, server, sftp_si=SFTPServerInterface, *largs, **kwargs):
""" """
The constructor for SFTPServer is meant to be called from within the The constructor for SFTPServer is meant to be called from within the
`.Transport` as a subsystem handler. ``server`` and any additional L{Transport} as a subsystem handler. C{server} and any additional
parameters or keyword parameters are passed from the original call to parameters or keyword parameters are passed from the original call to
`.Transport.set_subsystem_handler`. L{Transport.set_subsystem_handler}.
:param .Channel channel: channel passed from the `.Transport`. @param channel: channel passed from the L{Transport}.
:param str name: name of the requested subsystem. @type channel: L{Channel}
:param .ServerInterface server: @param name: name of the requested subsystem.
the server object associated with this channel and subsystem @type name: str
:param class sftp_si: @param server: the server object associated with this channel and
a subclass of `.SFTPServerInterface` to use for handling individual subsystem
requests. @type server: L{ServerInterface}
@param sftp_si: a subclass of L{SFTPServerInterface} to use for handling
individual requests.
@type sftp_si: class
""" """
BaseSFTP.__init__(self) BaseSFTP.__init__(self)
SubsystemHandler.__init__(self, channel, name, server) SubsystemHandler.__init__(self, channel, name, server)
@ -79,17 +70,17 @@ class SFTPServer (BaseSFTP, SubsystemHandler):
self.ultra_debug = transport.get_hexdump() self.ultra_debug = transport.get_hexdump()
self.next_handle = 1 self.next_handle = 1
# map of handle-string to SFTPHandle for files & folders: # map of handle-string to SFTPHandle for files & folders:
self.file_table = {} self.file_table = { }
self.folder_table = {} self.folder_table = { }
self.server = sftp_si(server, *largs, **kwargs) self.server = sftp_si(server, *largs, **kwargs)
def _log(self, level, msg): def _log(self, level, msg):
if issubclass(type(msg), list): if issubclass(type(msg), list):
for m in msg: for m in msg:
super(SFTPServer, self)._log(level, "[chan " + self.sock.get_name() + "] " + m) super(SFTPServer, self)._log(level, "[chan " + self.sock.get_name() + "] " + m)
else: else:
super(SFTPServer, self)._log(level, "[chan " + self.sock.get_name() + "] " + msg) super(SFTPServer, self)._log(level, "[chan " + self.sock.get_name() + "] " + msg)
def start_subsystem(self, name, transport, channel): def start_subsystem(self, name, transport, channel):
self.sock = channel self.sock = channel
self._log(DEBUG, 'Started sftp server on channel %s' % repr(channel)) self._log(DEBUG, 'Started sftp server on channel %s' % repr(channel))
@ -122,21 +113,23 @@ class SFTPServer (BaseSFTP, SubsystemHandler):
self.server.session_ended() self.server.session_ended()
super(SFTPServer, self).finish_subsystem() super(SFTPServer, self).finish_subsystem()
# close any file handles that were left open (so we can return them to the OS quickly) # close any file handles that were left open (so we can return them to the OS quickly)
for f in self.file_table.values(): for f in self.file_table.itervalues():
f.close() f.close()
for f in self.folder_table.values(): for f in self.folder_table.itervalues():
f.close() f.close()
self.file_table = {} self.file_table = {}
self.folder_table = {} self.folder_table = {}
def convert_errno(e): def convert_errno(e):
""" """
Convert an errno value (as from an ``OSError`` or ``IOError``) into a Convert an errno value (as from an C{OSError} or C{IOError}) into a
standard SFTP result code. This is a convenience function for trapping standard SFTP result code. This is a convenience function for trapping
exceptions in server code and returning an appropriate result. exceptions in server code and returning an appropriate result.
:param int e: an errno code, as from ``OSError.errno``. @param e: an errno code, as from C{OSError.errno}.
:return: an `int` SFTP error code like ``SFTP_NO_SUCH_FILE``. @type e: int
@return: an SFTP error code like L{SFTP_NO_SUCH_FILE}.
@rtype: int
""" """
if e == errno.EACCES: if e == errno.EACCES:
# permission denied # permission denied
@ -151,16 +144,18 @@ class SFTPServer (BaseSFTP, SubsystemHandler):
def set_file_attr(filename, attr): def set_file_attr(filename, attr):
""" """
Change a file's attributes on the local filesystem. The contents of Change a file's attributes on the local filesystem. The contents of
``attr`` are used to change the permissions, owner, group ownership, C{attr} are used to change the permissions, owner, group ownership,
and/or modification & access time of the file, depending on which and/or modification & access time of the file, depending on which
attributes are present in ``attr``. attributes are present in C{attr}.
This is meant to be a handy helper function for translating SFTP file This is meant to be a handy helper function for translating SFTP file
requests into local file operations. requests into local file operations.
:param str filename: @param filename: name of the file to alter (should usually be an
name of the file to alter (should usually be an absolute path). absolute path).
:param .SFTPAttributes attr: attributes to change. @type filename: str
@param attr: attributes to change.
@type attr: L{SFTPAttributes}
""" """
if sys.platform != 'win32': if sys.platform != 'win32':
# mode operations are meaningless on win32 # mode operations are meaningless on win32
@ -171,34 +166,35 @@ class SFTPServer (BaseSFTP, SubsystemHandler):
if attr._flags & attr.FLAG_AMTIME: if attr._flags & attr.FLAG_AMTIME:
os.utime(filename, (attr.st_atime, attr.st_mtime)) os.utime(filename, (attr.st_atime, attr.st_mtime))
if attr._flags & attr.FLAG_SIZE: if attr._flags & attr.FLAG_SIZE:
with open(filename, 'w+') as f: open(filename, 'w+').truncate(attr.st_size)
f.truncate(attr.st_size)
set_file_attr = staticmethod(set_file_attr) set_file_attr = staticmethod(set_file_attr)
### internals... ### internals...
def _response(self, request_number, t, *arg): def _response(self, request_number, t, *arg):
msg = Message() msg = Message()
msg.add_int(request_number) msg.add_int(request_number)
for item in arg: for item in arg:
if isinstance(item, long): if type(item) is int:
msg.add_int64(item)
elif isinstance(item, int):
msg.add_int(item) msg.add_int(item)
elif isinstance(item, (string_types, bytes_types)): elif type(item) is long:
msg.add_int64(item)
elif type(item) is str:
msg.add_string(item) msg.add_string(item)
elif type(item) is SFTPAttributes: elif type(item) is SFTPAttributes:
item._pack(msg) item._pack(msg)
else: else:
raise Exception('unknown type for ' + repr(item) + ' type ' + repr(type(item))) raise Exception('unknown type for ' + repr(item) + ' type ' + repr(type(item)))
self._send_packet(t, msg) self._send_packet(t, str(msg))
def _send_handle_response(self, request_number, handle, folder=False): def _send_handle_response(self, request_number, handle, folder=False):
if not issubclass(type(handle), SFTPHandle): if not issubclass(type(handle), SFTPHandle):
# must be error code # must be error code
self._send_status(request_number, handle) self._send_status(request_number, handle)
return return
handle._set_name(b('hx%d' % self.next_handle)) handle._set_name('hx%d' % self.next_handle)
self.next_handle += 1 self.next_handle += 1
if folder: if folder:
self.folder_table[handle._get_name()] = handle self.folder_table[handle._get_name()] = handle
@ -236,16 +232,16 @@ class SFTPServer (BaseSFTP, SubsystemHandler):
msg.add_int(len(flist)) msg.add_int(len(flist))
for attr in flist: for attr in flist:
msg.add_string(attr.filename) msg.add_string(attr.filename)
msg.add_string(attr) msg.add_string(str(attr))
attr._pack(msg) attr._pack(msg)
self._send_packet(CMD_NAME, msg) self._send_packet(CMD_NAME, str(msg))
def _check_file(self, request_number, msg): def _check_file(self, request_number, msg):
# this extension actually comes from v6 protocol, but since it's an # this extension actually comes from v6 protocol, but since it's an
# extension, i feel like we can reasonably support it backported. # extension, i feel like we can reasonably support it backported.
# it's very useful for verifying uploaded files or checking for # it's very useful for verifying uploaded files or checking for
# rsync-like differences between local and remote files. # rsync-like differences between local and remote files.
handle = msg.get_binary() handle = msg.get_string()
alg_list = msg.get_list() alg_list = msg.get_list()
start = msg.get_int64() start = msg.get_int64()
length = msg.get_int64() length = msg.get_int64()
@ -274,17 +270,17 @@ class SFTPServer (BaseSFTP, SubsystemHandler):
self._send_status(request_number, SFTP_FAILURE, 'Block size too small') self._send_status(request_number, SFTP_FAILURE, 'Block size too small')
return return
sum_out = bytes() sum_out = ''
offset = start offset = start
while offset < start + length: while offset < start + length:
blocklen = min(block_size, start + length - offset) blocklen = min(block_size, start + length - offset)
# don't try to read more than about 64KB at a time # don't try to read more than about 64KB at a time
chunklen = min(blocklen, 65536) chunklen = min(blocklen, 65536)
count = 0 count = 0
hash_obj = alg() hash_obj = alg.new()
while count < blocklen: while count < blocklen:
data = f.read(offset, chunklen) data = f.read(offset, chunklen)
if not isinstance(data, bytes_types): if not type(data) is str:
self._send_status(request_number, data, 'Unable to hash file') self._send_status(request_number, data, 'Unable to hash file')
return return
hash_obj.update(data) hash_obj.update(data)
@ -297,10 +293,10 @@ class SFTPServer (BaseSFTP, SubsystemHandler):
msg.add_string('check-file') msg.add_string('check-file')
msg.add_string(algname) msg.add_string(algname)
msg.add_bytes(sum_out) msg.add_bytes(sum_out)
self._send_packet(CMD_EXTENDED_REPLY, msg) self._send_packet(CMD_EXTENDED_REPLY, str(msg))
def _convert_pflags(self, pflags): def _convert_pflags(self, pflags):
"""convert SFTP-style open() flags to Python's os.open() flags""" "convert SFTP-style open() flags to python's os.open() flags"
if (pflags & SFTP_FLAG_READ) and (pflags & SFTP_FLAG_WRITE): if (pflags & SFTP_FLAG_READ) and (pflags & SFTP_FLAG_WRITE):
flags = os.O_RDWR flags = os.O_RDWR
elif pflags & SFTP_FLAG_WRITE: elif pflags & SFTP_FLAG_WRITE:
@ -320,12 +316,12 @@ class SFTPServer (BaseSFTP, SubsystemHandler):
def _process(self, t, request_number, msg): def _process(self, t, request_number, msg):
self._log(DEBUG, 'Request: %s' % CMD_NAMES[t]) self._log(DEBUG, 'Request: %s' % CMD_NAMES[t])
if t == CMD_OPEN: if t == CMD_OPEN:
path = msg.get_text() path = msg.get_string()
flags = self._convert_pflags(msg.get_int()) flags = self._convert_pflags(msg.get_int())
attr = SFTPAttributes._from_msg(msg) attr = SFTPAttributes._from_msg(msg)
self._send_handle_response(request_number, self.server.open(path, flags, attr)) self._send_handle_response(request_number, self.server.open(path, flags, attr))
elif t == CMD_CLOSE: elif t == CMD_CLOSE:
handle = msg.get_binary() handle = msg.get_string()
if handle in self.folder_table: if handle in self.folder_table:
del self.folder_table[handle] del self.folder_table[handle]
self._send_status(request_number, SFTP_OK) self._send_status(request_number, SFTP_OK)
@ -337,14 +333,14 @@ class SFTPServer (BaseSFTP, SubsystemHandler):
return return
self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle') self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle')
elif t == CMD_READ: elif t == CMD_READ:
handle = msg.get_binary() handle = msg.get_string()
offset = msg.get_int64() offset = msg.get_int64()
length = msg.get_int() length = msg.get_int()
if handle not in self.file_table: if handle not in self.file_table:
self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle') self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle')
return return
data = self.file_table[handle].read(offset, length) data = self.file_table[handle].read(offset, length)
if isinstance(data, (bytes_types, string_types)): if type(data) is str:
if len(data) == 0: if len(data) == 0:
self._send_status(request_number, SFTP_EOF) self._send_status(request_number, SFTP_EOF)
else: else:
@ -352,54 +348,54 @@ class SFTPServer (BaseSFTP, SubsystemHandler):
else: else:
self._send_status(request_number, data) self._send_status(request_number, data)
elif t == CMD_WRITE: elif t == CMD_WRITE:
handle = msg.get_binary() handle = msg.get_string()
offset = msg.get_int64() offset = msg.get_int64()
data = msg.get_binary() data = msg.get_string()
if handle not in self.file_table: if handle not in self.file_table:
self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle') self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle')
return return
self._send_status(request_number, self.file_table[handle].write(offset, data)) self._send_status(request_number, self.file_table[handle].write(offset, data))
elif t == CMD_REMOVE: elif t == CMD_REMOVE:
path = msg.get_text() path = msg.get_string()
self._send_status(request_number, self.server.remove(path)) self._send_status(request_number, self.server.remove(path))
elif t == CMD_RENAME: elif t == CMD_RENAME:
oldpath = msg.get_text() oldpath = msg.get_string()
newpath = msg.get_text() newpath = msg.get_string()
self._send_status(request_number, self.server.rename(oldpath, newpath)) self._send_status(request_number, self.server.rename(oldpath, newpath))
elif t == CMD_MKDIR: elif t == CMD_MKDIR:
path = msg.get_text() path = msg.get_string()
attr = SFTPAttributes._from_msg(msg) attr = SFTPAttributes._from_msg(msg)
self._send_status(request_number, self.server.mkdir(path, attr)) self._send_status(request_number, self.server.mkdir(path, attr))
elif t == CMD_RMDIR: elif t == CMD_RMDIR:
path = msg.get_text() path = msg.get_string()
self._send_status(request_number, self.server.rmdir(path)) self._send_status(request_number, self.server.rmdir(path))
elif t == CMD_OPENDIR: elif t == CMD_OPENDIR:
path = msg.get_text() path = msg.get_string()
self._open_folder(request_number, path) self._open_folder(request_number, path)
return return
elif t == CMD_READDIR: elif t == CMD_READDIR:
handle = msg.get_binary() handle = msg.get_string()
if handle not in self.folder_table: if handle not in self.folder_table:
self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle') self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle')
return return
folder = self.folder_table[handle] folder = self.folder_table[handle]
self._read_folder(request_number, folder) self._read_folder(request_number, folder)
elif t == CMD_STAT: elif t == CMD_STAT:
path = msg.get_text() path = msg.get_string()
resp = self.server.stat(path) resp = self.server.stat(path)
if issubclass(type(resp), SFTPAttributes): if issubclass(type(resp), SFTPAttributes):
self._response(request_number, CMD_ATTRS, resp) self._response(request_number, CMD_ATTRS, resp)
else: else:
self._send_status(request_number, resp) self._send_status(request_number, resp)
elif t == CMD_LSTAT: elif t == CMD_LSTAT:
path = msg.get_text() path = msg.get_string()
resp = self.server.lstat(path) resp = self.server.lstat(path)
if issubclass(type(resp), SFTPAttributes): if issubclass(type(resp), SFTPAttributes):
self._response(request_number, CMD_ATTRS, resp) self._response(request_number, CMD_ATTRS, resp)
else: else:
self._send_status(request_number, resp) self._send_status(request_number, resp)
elif t == CMD_FSTAT: elif t == CMD_FSTAT:
handle = msg.get_binary() handle = msg.get_string()
if handle not in self.file_table: if handle not in self.file_table:
self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle') self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle')
return return
@ -409,34 +405,34 @@ class SFTPServer (BaseSFTP, SubsystemHandler):
else: else:
self._send_status(request_number, resp) self._send_status(request_number, resp)
elif t == CMD_SETSTAT: elif t == CMD_SETSTAT:
path = msg.get_text() path = msg.get_string()
attr = SFTPAttributes._from_msg(msg) attr = SFTPAttributes._from_msg(msg)
self._send_status(request_number, self.server.chattr(path, attr)) self._send_status(request_number, self.server.chattr(path, attr))
elif t == CMD_FSETSTAT: elif t == CMD_FSETSTAT:
handle = msg.get_binary() handle = msg.get_string()
attr = SFTPAttributes._from_msg(msg) attr = SFTPAttributes._from_msg(msg)
if handle not in self.file_table: if handle not in self.file_table:
self._response(request_number, SFTP_BAD_MESSAGE, 'Invalid handle') self._response(request_number, SFTP_BAD_MESSAGE, 'Invalid handle')
return return
self._send_status(request_number, self.file_table[handle].chattr(attr)) self._send_status(request_number, self.file_table[handle].chattr(attr))
elif t == CMD_READLINK: elif t == CMD_READLINK:
path = msg.get_text() path = msg.get_string()
resp = self.server.readlink(path) resp = self.server.readlink(path)
if isinstance(resp, (bytes_types, string_types)): if type(resp) is str:
self._response(request_number, CMD_NAME, 1, resp, '', SFTPAttributes()) self._response(request_number, CMD_NAME, 1, resp, '', SFTPAttributes())
else: else:
self._send_status(request_number, resp) self._send_status(request_number, resp)
elif t == CMD_SYMLINK: elif t == CMD_SYMLINK:
# the sftp 2 draft is incorrect here! path always follows target_path # the sftp 2 draft is incorrect here! path always follows target_path
target_path = msg.get_text() target_path = msg.get_string()
path = msg.get_text() path = msg.get_string()
self._send_status(request_number, self.server.symlink(target_path, path)) self._send_status(request_number, self.server.symlink(target_path, path))
elif t == CMD_REALPATH: elif t == CMD_REALPATH:
path = msg.get_text() path = msg.get_string()
rpath = self.server.canonicalize(path) rpath = self.server.canonicalize(path)
self._response(request_number, CMD_NAME, 1, rpath, '', SFTPAttributes()) self._response(request_number, CMD_NAME, 1, rpath, '', SFTPAttributes())
elif t == CMD_EXTENDED: elif t == CMD_EXTENDED:
tag = msg.get_text() tag = msg.get_string()
if tag == 'check-file': if tag == 'check-file':
self._check_file(request_number, msg) self._check_file(request_number, msg)
else: else:

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -17,18 +17,19 @@
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
""" """
An interface to override for SFTP server support. L{SFTPServerInterface} is an interface to override for SFTP server support.
""" """
import os import os
import sys
from paramiko.sftp import SFTP_OP_UNSUPPORTED from paramiko.common import *
from paramiko.sftp import *
class SFTPServerInterface (object): class SFTPServerInterface (object):
""" """
This class defines an interface for controlling the behavior of paramiko This class defines an interface for controlling the behavior of paramiko
when using the `.SFTPServer` subsystem to provide an SFTP server. when using the L{SFTPServer} subsystem to provide an SFTP server.
Methods on this class are called from the SFTP session's thread, so you can Methods on this class are called from the SFTP session's thread, so you can
block as long as necessary without affecting other sessions (even other block as long as necessary without affecting other sessions (even other
@ -40,13 +41,14 @@ class SFTPServerInterface (object):
clients & servers obey the requirement that paths be encoded in UTF-8. clients & servers obey the requirement that paths be encoded in UTF-8.
""" """
def __init__(self, server, *largs, **kwargs): def __init__ (self, server, *largs, **kwargs):
""" """
Create a new SFTPServerInterface object. This method does nothing by Create a new SFTPServerInterface object. This method does nothing by
default and is meant to be overridden by subclasses. default and is meant to be overridden by subclasses.
:param .ServerInterface server: @param server: the server object associated with this channel and
the server object associated with this channel and SFTP subsystem SFTP subsystem
@type server: L{ServerInterface}
""" """
super(SFTPServerInterface, self).__init__(*largs, **kwargs) super(SFTPServerInterface, self).__init__(*largs, **kwargs)
@ -62,7 +64,7 @@ class SFTPServerInterface (object):
""" """
The SFTP server session has just ended, either cleanly or via an The SFTP server session has just ended, either cleanly or via an
exception. This method is meant to be overridden to perform any exception. This method is meant to be overridden to perform any
necessary cleanup before this `.SFTPServerInterface` object is necessary cleanup before this C{SFTPServerInterface} object is
destroyed. destroyed.
""" """
pass pass
@ -70,105 +72,103 @@ class SFTPServerInterface (object):
def open(self, path, flags, attr): def open(self, path, flags, attr):
""" """
Open a file on the server and create a handle for future operations Open a file on the server and create a handle for future operations
on that file. On success, a new object subclassed from `.SFTPHandle` on that file. On success, a new object subclassed from L{SFTPHandle}
should be returned. This handle will be used for future operations should be returned. This handle will be used for future operations
on the file (read, write, etc). On failure, an error code such as on the file (read, write, etc). On failure, an error code such as
`.SFTP_PERMISSION_DENIED` should be returned. L{SFTP_PERMISSION_DENIED} should be returned.
``flags`` contains the requested mode for opening (read-only, C{flags} contains the requested mode for opening (read-only,
write-append, etc) as a bitset of flags from the ``os`` module: write-append, etc) as a bitset of flags from the C{os} module:
- C{os.O_RDONLY}
- ``os.O_RDONLY`` - C{os.O_WRONLY}
- ``os.O_WRONLY`` - C{os.O_RDWR}
- ``os.O_RDWR`` - C{os.O_APPEND}
- ``os.O_APPEND`` - C{os.O_CREAT}
- ``os.O_CREAT`` - C{os.O_TRUNC}
- ``os.O_TRUNC`` - C{os.O_EXCL}
- ``os.O_EXCL`` (One of C{os.O_RDONLY}, C{os.O_WRONLY}, or C{os.O_RDWR} will always
(One of ``os.O_RDONLY``, ``os.O_WRONLY``, or ``os.O_RDWR`` will always
be set.) be set.)
The ``attr`` object contains requested attributes of the file if it The C{attr} object contains requested attributes of the file if it
has to be created. Some or all attribute fields may be missing if has to be created. Some or all attribute fields may be missing if
the client didn't specify them. the client didn't specify them.
.. note:: The SFTP protocol defines all files to be in "binary" mode. @note: The SFTP protocol defines all files to be in "binary" mode.
There is no equivalent to Python's "text" mode. There is no equivalent to python's "text" mode.
:param str path: @param path: the requested path (relative or absolute) of the file
the requested path (relative or absolute) of the file to be opened. to be opened.
:param int flags: @type path: str
flags or'd together from the ``os`` module indicating the requested @param flags: flags or'd together from the C{os} module indicating the
mode for opening the file. requested mode for opening the file.
:param .SFTPAttributes attr: @type flags: int
requested attributes of the file if it is newly created. @param attr: requested attributes of the file if it is newly created.
:return: a new `.SFTPHandle` or error code. @type attr: L{SFTPAttributes}
@return: a new L{SFTPHandle} I{or error code}.
@rtype L{SFTPHandle}
""" """
return SFTP_OP_UNSUPPORTED return SFTP_OP_UNSUPPORTED
def list_folder(self, path): def list_folder(self, path):
""" """
Return a list of files within a given folder. The ``path`` will use Return a list of files within a given folder. The C{path} will use
posix notation (``"/"`` separates folder names) and may be an absolute posix notation (C{"/"} separates folder names) and may be an absolute
or relative path. or relative path.
The list of files is expected to be a list of `.SFTPAttributes` The list of files is expected to be a list of L{SFTPAttributes}
objects, which are similar in structure to the objects returned by objects, which are similar in structure to the objects returned by
``os.stat``. In addition, each object should have its ``filename`` C{os.stat}. In addition, each object should have its C{filename}
field filled in, since this is important to a directory listing and field filled in, since this is important to a directory listing and
not normally present in ``os.stat`` results. The method not normally present in C{os.stat} results. The method
`.SFTPAttributes.from_stat` will usually do what you want. L{SFTPAttributes.from_stat} will usually do what you want.
In case of an error, you should return one of the ``SFTP_*`` error In case of an error, you should return one of the C{SFTP_*} error
codes, such as `.SFTP_PERMISSION_DENIED`. codes, such as L{SFTP_PERMISSION_DENIED}.
:param str path: the requested path (relative or absolute) to be listed. @param path: the requested path (relative or absolute) to be listed.
:return: @type path: str
a list of the files in the given folder, using `.SFTPAttributes` @return: a list of the files in the given folder, using
objects. L{SFTPAttributes} objects.
@rtype: list of L{SFTPAttributes} I{or error code}
.. note:: @note: You should normalize the given C{path} first (see the
You should normalize the given ``path`` first (see the `os.path` C{os.path} module) and check appropriate permissions before returning
module) and check appropriate permissions before returning the list the list of files. Be careful of malicious clients attempting to use
of files. Be careful of malicious clients attempting to use relative paths to escape restricted folders, if you're doing a direct
relative paths to escape restricted folders, if you're doing a translation from the SFTP server path to your local filesystem.
direct translation from the SFTP server path to your local
filesystem.
""" """
return SFTP_OP_UNSUPPORTED return SFTP_OP_UNSUPPORTED
def stat(self, path): def stat(self, path):
""" """
Return an `.SFTPAttributes` object for a path on the server, or an Return an L{SFTPAttributes} object for a path on the server, or an
error code. If your server supports symbolic links (also known as error code. If your server supports symbolic links (also known as
"aliases"), you should follow them. (`lstat` is the corresponding "aliases"), you should follow them. (L{lstat} is the corresponding
call that doesn't follow symlinks/aliases.) call that doesn't follow symlinks/aliases.)
:param str path: @param path: the requested path (relative or absolute) to fetch
the requested path (relative or absolute) to fetch file statistics file statistics for.
for. @type path: str
:return: @return: an attributes object for the given file, or an SFTP error
an `.SFTPAttributes` object for the given file, or an SFTP error code (like L{SFTP_PERMISSION_DENIED}).
code (like `.SFTP_PERMISSION_DENIED`). @rtype: L{SFTPAttributes} I{or error code}
""" """
return SFTP_OP_UNSUPPORTED return SFTP_OP_UNSUPPORTED
def lstat(self, path): def lstat(self, path):
""" """
Return an `.SFTPAttributes` object for a path on the server, or an Return an L{SFTPAttributes} object for a path on the server, or an
error code. If your server supports symbolic links (also known as error code. If your server supports symbolic links (also known as
"aliases"), you should not follow them -- instead, you should "aliases"), you should I{not} follow them -- instead, you should
return data on the symlink or alias itself. (`stat` is the return data on the symlink or alias itself. (L{stat} is the
corresponding call that follows symlinks/aliases.) corresponding call that follows symlinks/aliases.)
:param str path: @param path: the requested path (relative or absolute) to fetch
the requested path (relative or absolute) to fetch file statistics file statistics for.
for. @type path: str
:type path: str @return: an attributes object for the given file, or an SFTP error
:return: code (like L{SFTP_PERMISSION_DENIED}).
an `.SFTPAttributes` object for the given file, or an SFTP error @rtype: L{SFTPAttributes} I{or error code}
code (like `.SFTP_PERMISSION_DENIED`).
""" """
return SFTP_OP_UNSUPPORTED return SFTP_OP_UNSUPPORTED
@ -176,9 +176,11 @@ class SFTPServerInterface (object):
""" """
Delete a file, if possible. Delete a file, if possible.
:param str path: @param path: the requested path (relative or absolute) of the file
the requested path (relative or absolute) of the file to delete. to delete.
:return: an SFTP error code `int` like `.SFTP_OK`. @type path: str
@return: an SFTP error code like L{SFTP_OK}.
@rtype: int
""" """
return SFTP_OP_UNSUPPORTED return SFTP_OP_UNSUPPORTED
@ -190,74 +192,83 @@ class SFTPServerInterface (object):
probably a good idea to implement "move" in this method too, even for probably a good idea to implement "move" in this method too, even for
files that cross disk partition boundaries, if at all possible. files that cross disk partition boundaries, if at all possible.
.. note:: You should return an error if a file with the same name as @note: You should return an error if a file with the same name as
``newpath`` already exists. (The rename operation should be C{newpath} already exists. (The rename operation should be
non-desctructive.) non-desctructive.)
:param str oldpath: @param oldpath: the requested path (relative or absolute) of the
the requested path (relative or absolute) of the existing file. existing file.
:param str newpath: the requested new path of the file. @type oldpath: str
:return: an SFTP error code `int` like `.SFTP_OK`. @param newpath: the requested new path of the file.
@type newpath: str
@return: an SFTP error code like L{SFTP_OK}.
@rtype: int
""" """
return SFTP_OP_UNSUPPORTED return SFTP_OP_UNSUPPORTED
def mkdir(self, path, attr): def mkdir(self, path, attr):
""" """
Create a new directory with the given attributes. The ``attr`` Create a new directory with the given attributes. The C{attr}
object may be considered a "hint" and ignored. object may be considered a "hint" and ignored.
The ``attr`` object will contain only those fields provided by the The C{attr} object will contain only those fields provided by the
client in its request, so you should use ``hasattr`` to check for client in its request, so you should use C{hasattr} to check for
the presense of fields before using them. In some cases, the ``attr`` the presense of fields before using them. In some cases, the C{attr}
object may be completely empty. object may be completely empty.
:param str path: @param path: requested path (relative or absolute) of the new
requested path (relative or absolute) of the new folder. folder.
:param .SFTPAttributes attr: requested attributes of the new folder. @type path: str
:return: an SFTP error code `int` like `.SFTP_OK`. @param attr: requested attributes of the new folder.
@type attr: L{SFTPAttributes}
@return: an SFTP error code like L{SFTP_OK}.
@rtype: int
""" """
return SFTP_OP_UNSUPPORTED return SFTP_OP_UNSUPPORTED
def rmdir(self, path): def rmdir(self, path):
""" """
Remove a directory if it exists. The ``path`` should refer to an Remove a directory if it exists. The C{path} should refer to an
existing, empty folder -- otherwise this method should return an existing, empty folder -- otherwise this method should return an
error. error.
:param str path: @param path: requested path (relative or absolute) of the folder
requested path (relative or absolute) of the folder to remove. to remove.
:return: an SFTP error code `int` like `.SFTP_OK`. @type path: str
@return: an SFTP error code like L{SFTP_OK}.
@rtype: int
""" """
return SFTP_OP_UNSUPPORTED return SFTP_OP_UNSUPPORTED
def chattr(self, path, attr): def chattr(self, path, attr):
""" """
Change the attributes of a file. The ``attr`` object will contain Change the attributes of a file. The C{attr} object will contain
only those fields provided by the client in its request, so you only those fields provided by the client in its request, so you
should check for the presence of fields before using them. should check for the presence of fields before using them.
:param str path: @param path: requested path (relative or absolute) of the file to
requested path (relative or absolute) of the file to change. change.
:param attr: @type path: str
requested attributes to change on the file (an `.SFTPAttributes` @param attr: requested attributes to change on the file.
object) @type attr: L{SFTPAttributes}
:return: an error code `int` like `.SFTP_OK`. @return: an error code like L{SFTP_OK}.
@rtype: int
""" """
return SFTP_OP_UNSUPPORTED return SFTP_OP_UNSUPPORTED
def canonicalize(self, path): def canonicalize(self, path):
""" """
Return the canonical form of a path on the server. For example, Return the canonical form of a path on the server. For example,
if the server's home folder is ``/home/foo``, the path if the server's home folder is C{/home/foo}, the path
``"../betty"`` would be canonicalized to ``"/home/betty"``. Note C{"../betty"} would be canonicalized to C{"/home/betty"}. Note
the obvious security issues: if you're serving files only from a the obvious security issues: if you're serving files only from a
specific folder, you probably don't want this method to reveal path specific folder, you probably don't want this method to reveal path
names outside that folder. names outside that folder.
You may find the Python methods in ``os.path`` useful, especially You may find the python methods in C{os.path} useful, especially
``os.path.normpath`` and ``os.path.realpath``. C{os.path.normpath} and C{os.path.realpath}.
The default implementation returns ``os.path.normpath('/' + path)``. The default implementation returns C{os.path.normpath('/' + path)}.
""" """
if os.path.isabs(path): if os.path.isabs(path):
out = os.path.normpath(path) out = os.path.normpath(path)
@ -274,23 +285,26 @@ class SFTPServerInterface (object):
If the specified path doesn't refer to a symbolic link, an error If the specified path doesn't refer to a symbolic link, an error
should be returned. should be returned.
:param str path: path (relative or absolute) of the symbolic link. @param path: path (relative or absolute) of the symbolic link.
:return: @type path: str
the target `str` path of the symbolic link, or an error code like @return: the target path of the symbolic link, or an error code like
`.SFTP_NO_SUCH_FILE`. L{SFTP_NO_SUCH_FILE}.
@rtype: str I{or error code}
""" """
return SFTP_OP_UNSUPPORTED return SFTP_OP_UNSUPPORTED
def symlink(self, target_path, path): def symlink(self, target_path, path):
""" """
Create a symbolic link on the server, as new pathname ``path``, Create a symbolic link on the server, as new pathname C{path},
with ``target_path`` as the target of the link. with C{target_path} as the target of the link.
:param str target_path: @param target_path: path (relative or absolute) of the target for
path (relative or absolute) of the target for this new symbolic this new symbolic link.
link. @type target_path: str
:param str path: @param path: path (relative or absolute) of the symbolic link to
path (relative or absolute) of the symbolic link to create. create.
:return: an error code `int` like ``SFTP_OK``. @type path: str
@return: an error code like C{SFTP_OK}.
@rtype: int
""" """
return SFTP_OP_UNSUPPORTED return SFTP_OP_UNSUPPORTED

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -16,6 +16,10 @@
# along with Paramiko; if not, write to the Free Software Foundation, Inc., # along with Paramiko; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
"""
Exceptions defined by paramiko.
"""
class SSHException (Exception): class SSHException (Exception):
""" """
@ -30,7 +34,7 @@ class AuthenticationException (SSHException):
possible to retry with different credentials. (Other classes specify more possible to retry with different credentials. (Other classes specify more
specific reasons.) specific reasons.)
.. versionadded:: 1.6 @since: 1.6
""" """
pass pass
@ -48,20 +52,19 @@ class BadAuthenticationType (AuthenticationException):
the server isn't allowing that type. (It may only allow public-key, for the server isn't allowing that type. (It may only allow public-key, for
example.) example.)
:ivar list allowed_types: @ivar allowed_types: list of allowed authentication types provided by the
list of allowed authentication types provided by the server (possible server (possible values are: C{"none"}, C{"password"}, and
values are: ``"none"``, ``"password"``, and ``"publickey"``). C{"publickey"}).
@type allowed_types: list
.. versionadded:: 1.1 @since: 1.1
""" """
allowed_types = [] allowed_types = []
def __init__(self, explanation, types): def __init__(self, explanation, types):
AuthenticationException.__init__(self, explanation) AuthenticationException.__init__(self, explanation)
self.allowed_types = types self.allowed_types = types
# for unpickling
self.args = (explanation, types, )
def __str__(self): def __str__(self):
return SSHException.__str__(self) + ' (allowed_types=%r)' % self.allowed_types return SSHException.__str__(self) + ' (allowed_types=%r)' % self.allowed_types
@ -75,50 +78,50 @@ class PartialAuthentication (AuthenticationException):
def __init__(self, types): def __init__(self, types):
AuthenticationException.__init__(self, 'partial authentication') AuthenticationException.__init__(self, 'partial authentication')
self.allowed_types = types self.allowed_types = types
# for unpickling
self.args = (types, )
class ChannelException (SSHException): class ChannelException (SSHException):
""" """
Exception raised when an attempt to open a new `.Channel` fails. Exception raised when an attempt to open a new L{Channel} fails.
:ivar int code: the error code returned by the server @ivar code: the error code returned by the server
@type code: int
.. versionadded:: 1.6 @since: 1.6
""" """
def __init__(self, code, text): def __init__(self, code, text):
SSHException.__init__(self, text) SSHException.__init__(self, text)
self.code = code self.code = code
# for unpickling
self.args = (code, text, )
class BadHostKeyException (SSHException): class BadHostKeyException (SSHException):
""" """
The host key given by the SSH server did not match what we were expecting. The host key given by the SSH server did not match what we were expecting.
:ivar str hostname: the hostname of the SSH server @ivar hostname: the hostname of the SSH server
:ivar PKey got_key: the host key presented by the server @type hostname: str
:ivar PKey expected_key: the host key expected @ivar key: the host key presented by the server
@type key: L{PKey}
@ivar expected_key: the host key expected
@type expected_key: L{PKey}
.. versionadded:: 1.6 @since: 1.6
""" """
def __init__(self, hostname, got_key, expected_key): def __init__(self, hostname, got_key, expected_key):
SSHException.__init__(self, 'Host key for server %s does not match!' % hostname) SSHException.__init__(self, 'Host key for server %s does not match!' % hostname)
self.hostname = hostname self.hostname = hostname
self.key = got_key self.key = got_key
self.expected_key = expected_key self.expected_key = expected_key
# for unpickling
self.args = (hostname, got_key, expected_key, )
class ProxyCommandFailure (SSHException): class ProxyCommandFailure (SSHException):
""" """
The "ProxyCommand" found in the .ssh/config file returned an error. The "ProxyCommand" found in the .ssh/config file returned an error.
:ivar str command: The command line that is generating this exception. @ivar command: The command line that is generating this exception.
:ivar str error: The error captured from the proxy command output. @type command: str
@ivar error: The error captured from the proxy command output.
@type error: str
""" """
def __init__(self, command, error): def __init__(self, command, error):
SSHException.__init__(self, SSHException.__init__(self,
@ -127,5 +130,3 @@ class ProxyCommandFailure (SSHException):
) )
) )
self.error = error self.error = error
# for unpickling
self.args = (command, error, )

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -25,29 +25,40 @@ from __future__ import generators
import array import array
from binascii import hexlify, unhexlify from binascii import hexlify, unhexlify
import errno import errno
import sys
import struct import struct
import traceback import traceback
import threading import threading
import logging
from paramiko.common import DEBUG, zero_byte, xffffffff, max_byte from paramiko.common import *
from paramiko.py3compat import PY2, long, byte_ord, b, byte_chr
from paramiko.config import SSHConfig from paramiko.config import SSHConfig
import six
if six.PY3:
long = lambda x: int(x)
# Change by RogerB - python < 2.3 doesn't have enumerate so we implement it
if sys.version_info < (2,3):
class enumerate:
def __init__ (self, sequence):
self.sequence = sequence
def __iter__ (self):
count = 0
for item in self.sequence:
yield (count, item)
count += 1
def inflate_long(s, always_positive=False): def inflate_long(s, always_positive=False):
"""turns a normalized byte string into a long-int (adapted from Crypto.Util.number)""" "turns a normalized byte string into a long-int (adapted from Crypto.Util.number)"
out = long(0) out = 0
negative = 0 negative = 0
if not always_positive and (len(s) > 0) and (byte_ord(s[0]) >= 0x80): if not always_positive and (len(s) > 0) and (ord(s[0]) >= 0x80):
negative = 1 negative = 1
if len(s) % 4: if len(s) % 4:
filler = zero_byte filler = '\x00'
if negative: if negative:
filler = max_byte filler = '\xff'
# never convert this to ``s +=`` because this is a string, not a number
# noinspection PyAugmentAssignment
s = filler * (4 - len(s) % 4) + s s = filler * (4 - len(s) % 4) + s
for i in range(0, len(s), 4): for i in range(0, len(s), 4):
out = (out << 32) + struct.unpack('>I', s[i:i+4])[0] out = (out << 32) + struct.unpack('>I', s[i:i+4])[0]
@ -55,39 +66,44 @@ def inflate_long(s, always_positive=False):
out -= (long(1) << (8 * len(s))) out -= (long(1) << (8 * len(s)))
return out return out
deflate_zero = zero_byte if PY2 else 0
deflate_ff = max_byte if PY2 else 0xff
def deflate_long(n, add_sign_padding=True): def deflate_long(n, add_sign_padding=True):
"""turns a long-int into a normalized byte string (adapted from Crypto.Util.number)""" "turns a long-int into a normalized byte string (adapted from Crypto.Util.number)"
# after much testing, this algorithm was deemed to be the fastest # after much testing, this algorithm was deemed to be the fastest
s = bytes() s = ''
n = long(n) n = long(n)
while (n != 0) and (n != -1): while (n != 0) and (n != -1):
s = struct.pack('>I', n & xffffffff) + s s = struct.pack('>I', n & long(0xffffffff)) + s
n >>= 32 n = n >> 32
# strip off leading zeros, FFs # strip off leading zeros, FFs
for i in enumerate(s): for i in enumerate(s):
if (n == 0) and (i[1] != deflate_zero): if (n == 0) and (i[1] != '\000'):
break break
if (n == -1) and (i[1] != deflate_ff): if (n == -1) and (i[1] != '\xff'):
break break
else: else:
# degenerate case, n was either 0 or -1 # degenerate case, n was either 0 or -1
i = (0,) i = (0,)
if n == 0: if n == 0:
s = zero_byte s = '\000'
else: else:
s = max_byte s = '\xff'
s = s[i[0]:] s = s[i[0]:]
if add_sign_padding: if add_sign_padding:
if (n == 0) and (byte_ord(s[0]) >= 0x80): if (n == 0) and (ord(s[0]) >= 0x80):
s = zero_byte + s s = '\x00' + s
if (n == -1) and (byte_ord(s[0]) < 0x80): if (n == -1) and (ord(s[0]) < 0x80):
s = max_byte + s s = '\xff' + s
return s return s
def format_binary_weird(data):
out = ''
for i in enumerate(data):
out += '%02X' % ord(i[1])
if i[0] % 2:
out += ' '
if i[0] % 16 == 15:
out += '\n'
return out
def format_binary(data, prefix=''): def format_binary(data, prefix=''):
x = 0 x = 0
@ -99,73 +115,69 @@ def format_binary(data, prefix=''):
out.append(format_binary_line(data[x:])) out.append(format_binary_line(data[x:]))
return [prefix + x for x in out] return [prefix + x for x in out]
def format_binary_line(data): def format_binary_line(data):
left = ' '.join(['%02X' % byte_ord(c) for c in data]) left = ' '.join(['%02X' % ord(c) for c in data])
right = ''.join([('.%c..' % c)[(byte_ord(c)+63)//95] for c in data]) right = ''.join([('.%c..' % c)[(ord(c)+63)//95] for c in data])
return '%-50s %s' % (left, right) return '%-50s %s' % (left, right)
def hexify(s): def hexify(s):
return hexlify(s).upper() return hexlify(s).upper()
def unhexify(s): def unhexify(s):
return unhexlify(s) return unhexlify(s)
def safe_string(s): def safe_string(s):
out = '' out = ''
for c in s: for c in s:
if (byte_ord(c) >= 32) and (byte_ord(c) <= 127): if (ord(c) >= 32) and (ord(c) <= 127):
out += c out += c
else: else:
out += '%%%02X' % byte_ord(c) out += '%%%02X' % ord(c)
return out return out
# ''.join([['%%%02X' % ord(c), c][(ord(c) >= 32) and (ord(c) <= 127)] for c in s])
def bit_length(n): def bit_length(n):
try: norm = deflate_long(n, 0)
return n.bitlength() hbyte = ord(norm[0])
except AttributeError: if hbyte == 0:
norm = deflate_long(n, False) return 1
hbyte = byte_ord(norm[0]) bitlen = len(norm) * 8
if hbyte == 0: while not (hbyte & 0x80):
return 1 hbyte <<= 1
bitlen = len(norm) * 8 bitlen -= 1
while not (hbyte & 0x80): return bitlen
hbyte <<= 1
bitlen -= 1
return bitlen
def tb_strings(): def tb_strings():
return ''.join(traceback.format_exception(*sys.exc_info())).split('\n') 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 Given a password, passphrase, or other human-source key, scramble it
through a secure hash into some keyworthy bytes. This specific algorithm through a secure hash into some keyworthy bytes. This specific algorithm
is used for encrypting/decrypting private key files. is used for encrypting/decrypting private key files.
:param function hash_alg: A function which creates a new hash object, such @param hashclass: class from L{Crypto.Hash} that can be used as a secure
as ``hashlib.sha256``. hashing function (like C{MD5} or C{SHA}).
:param salt: data to salt the hash with. @type hashclass: L{Crypto.Hash}
:type salt: byte string @param salt: data to salt the hash with.
:param str key: human-entered password or passphrase. @type salt: string
:param int nbytes: number of bytes to generate. @param key: human-entered password or passphrase.
:return: Key data `str` @type key: string
@param nbytes: number of bytes to generate.
@type nbytes: int
@return: key data
@rtype: string
""" """
keydata = bytes() keydata = ''
digest = bytes() digest = ''
if len(salt) > 8: if len(salt) > 8:
salt = salt[:8] salt = salt[:8]
while nbytes > 0: while nbytes > 0:
hash_obj = hash_alg() hash_obj = hashclass.new()
if len(digest) > 0: if len(digest) > 0:
hash_obj.update(digest) hash_obj.update(digest)
hash_obj.update(b(key)) hash_obj.update(key)
hash_obj.update(salt) hash_obj.update(salt)
digest = hash_obj.digest() digest = hash_obj.digest()
size = min(nbytes, len(digest)) size = min(nbytes, len(digest))
@ -173,45 +185,42 @@ def generate_key_bytes(hash_alg, salt, key, nbytes):
nbytes -= size nbytes -= size
return keydata return keydata
def load_host_keys(filename): def load_host_keys(filename):
""" """
Read a file of known SSH host keys, in the format used by openssh, and Read a file of known SSH host keys, in the format used by openssh, and
return a compound dict of ``hostname -> keytype ->`` `PKey return a compound dict of C{hostname -> keytype ->} L{PKey <paramiko.pkey.PKey>}.
<paramiko.pkey.PKey>`. The hostname may be an IP address or DNS name. The The hostname may be an IP address or DNS name. The keytype will be either
keytype will be either ``"ssh-rsa"`` or ``"ssh-dss"``. C{"ssh-rsa"} or C{"ssh-dss"}.
This type of file unfortunately doesn't exist on Windows, but on posix, This type of file unfortunately doesn't exist on Windows, but on posix,
it will usually be stored in ``os.path.expanduser("~/.ssh/known_hosts")``. it will usually be stored in C{os.path.expanduser("~/.ssh/known_hosts")}.
Since 1.5.3, this is just a wrapper around `.HostKeys`. Since 1.5.3, this is just a wrapper around L{HostKeys}.
:param str filename: name of the file to read host keys from @param filename: name of the file to read host keys from
:return: @type filename: str
nested dict of `.PKey` objects, indexed by hostname and then keytype @return: dict of host keys, indexed by hostname and then keytype
@rtype: dict(hostname, dict(keytype, L{PKey <paramiko.pkey.PKey>}))
""" """
from paramiko.hostkeys import HostKeys from paramiko.hostkeys import HostKeys
return HostKeys(filename) return HostKeys(filename)
def parse_ssh_config(file_obj): def parse_ssh_config(file_obj):
""" """
Provided only as a backward-compatible wrapper around `.SSHConfig`. Provided only as a backward-compatible wrapper around L{SSHConfig}.
""" """
config = SSHConfig() config = SSHConfig()
config.parse(file_obj) config.parse(file_obj)
return config return config
def lookup_ssh_host_config(hostname, config): def lookup_ssh_host_config(hostname, config):
""" """
Provided only as a backward-compatible wrapper around `.SSHConfig`. Provided only as a backward-compatible wrapper around L{SSHConfig}.
""" """
return config.lookup(hostname) return config.lookup(hostname)
def mod_inverse(x, m): def mod_inverse(x, m):
# it's crazy how small Python can make this function. # it's crazy how small python can make this function.
u1, u2, u3 = 1, 0, m u1, u2, u3 = 1, 0, m
v1, v2, v3 = 0, 1, x v1, v2, v3 = 0, 1, x
@ -227,8 +236,6 @@ def mod_inverse(x, m):
_g_thread_ids = {} _g_thread_ids = {}
_g_thread_counter = 0 _g_thread_counter = 0
_g_thread_lock = threading.Lock() _g_thread_lock = threading.Lock()
def get_thread_id(): def get_thread_id():
global _g_thread_ids, _g_thread_counter, _g_thread_lock global _g_thread_ids, _g_thread_counter, _g_thread_lock
tid = id(threading.currentThread()) tid = id(threading.currentThread())
@ -243,9 +250,8 @@ def get_thread_id():
_g_thread_lock.release() _g_thread_lock.release()
return ret return ret
def log_to_file(filename, level=DEBUG): def log_to_file(filename, level=DEBUG):
"""send paramiko logs to a logfile, if they're not already going somewhere""" "send paramiko logs to a logfile, if they're not already going somewhere"
l = logging.getLogger("paramiko") l = logging.getLogger("paramiko")
if len(l.handlers) > 0: if len(l.handlers) > 0:
return return
@ -256,7 +262,6 @@ def log_to_file(filename, level=DEBUG):
'%Y%m%d-%H:%M:%S')) '%Y%m%d-%H:%M:%S'))
l.addHandler(lh) l.addHandler(lh)
# make only one filter object, so it doesn't get applied more than once # make only one filter object, so it doesn't get applied more than once
class PFilter (object): class PFilter (object):
def filter(self, record): def filter(self, record):
@ -264,13 +269,11 @@ class PFilter (object):
return True return True
_pfilter = PFilter() _pfilter = PFilter()
def get_logger(name): def get_logger(name):
l = logging.getLogger(name) l = logging.getLogger(name)
l.addFilter(_pfilter) l.addFilter(_pfilter)
return l return l
def retry_on_signal(function): def retry_on_signal(function):
"""Retries function until it doesn't raise an EINTR error""" """Retries function until it doesn't raise an EINTR error"""
while True: while True:
@ -280,43 +283,32 @@ def retry_on_signal(function):
if e.errno != errno.EINTR: if e.errno != errno.EINTR:
raise raise
class Counter (object): class Counter (object):
"""Stateful counter for CTR mode crypto""" """Stateful counter for CTR mode crypto"""
def __init__(self, nbits, initial_value=long(1), overflow=long(0)): def __init__(self, nbits, initial_value=long(1), overflow=0):
self.blocksize = nbits / 8 self.blocksize = nbits / 8
self.overflow = overflow self.overflow = overflow
# start with value - 1 so we don't have to store intermediate values when counting # start with value - 1 so we don't have to store intermediate values when counting
# could the iv be 0? # could the iv be 0?
if initial_value == 0: if initial_value == 0:
self.value = array.array('c', max_byte * self.blocksize) self.value = array.array('c', '\xFF' * self.blocksize)
else: else:
x = deflate_long(initial_value - 1, add_sign_padding=False) x = deflate_long(initial_value - 1, add_sign_padding=False)
self.value = array.array('c', zero_byte * (self.blocksize - len(x)) + x) self.value = array.array('c', '\x00' * (self.blocksize - len(x)) + x)
def __call__(self): def __call__(self):
"""Increament the counter and return the new value""" """Increament the counter and return the new value"""
i = self.blocksize - 1 i = self.blocksize - 1
while i > -1: while i > -1:
c = self.value[i] = byte_chr((byte_ord(self.value[i]) + 1) % 256) c = self.value[i] = chr((ord(self.value[i]) + 1) % 256)
if c != zero_byte: if c != '\x00':
return self.value.tostring() return self.value.tostring()
i -= 1 i -= 1
# counter reset # counter reset
x = deflate_long(self.overflow, add_sign_padding=False) x = deflate_long(self.overflow, add_sign_padding=False)
self.value = array.array('c', zero_byte * (self.blocksize - len(x)) + x) self.value = array.array('c', '\x00' * (self.blocksize - len(x)) + x)
return self.value.tostring() return self.value.tostring()
def new(cls, nbits, initial_value=long(1), overflow=long(0)): def new(cls, nbits, initial_value=long(1), overflow=0):
return cls(nbits, initial_value=initial_value, overflow=overflow) return cls(nbits, initial_value=initial_value, overflow=overflow)
new = classmethod(new) new = classmethod(new)
def constant_time_bytes_eq(a, b):
if len(a) != len(b):
return False
res = 0
# noinspection PyUnresolvedReferences
for i in (xrange if PY2 else range)(len(a)):
res |= byte_ord(a[i]) ^ byte_ord(b[i])
return res == 0

View File

@ -8,7 +8,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -21,20 +21,16 @@
Functions for communicating with Pageant, the basic windows ssh agent program. Functions for communicating with Pageant, the basic windows ssh agent program.
""" """
import array from __future__ import with_statement
import ctypes.wintypes
import platform
import struct
from paramiko.util import *
try: import struct
import _thread as thread # Python 3.x import threading
except ImportError: import array
import thread # Python 2.5-2.7 import platform
import ctypes.wintypes
from . import _winapi from . import _winapi
_AGENT_COPYDATA_ID = 0x804e50ba _AGENT_COPYDATA_ID = 0x804e50ba
_AGENT_MAX_MSGLEN = 8192 _AGENT_MAX_MSGLEN = 8192
# Note: The WM_COPYDATA value is pulled from win32con, as a workaround # Note: The WM_COPYDATA value is pulled from win32con, as a workaround
@ -55,10 +51,7 @@ def can_talk_to_agent():
""" """
return bool(_get_pageant_window_object()) return bool(_get_pageant_window_object())
ULONG_PTR = ctypes.c_uint64 if platform.architecture()[0] == '64bit' else ctypes.c_uint32 ULONG_PTR = ctypes.c_uint64 if platform.architecture()[0] == '64bit' else ctypes.c_uint32
class COPYDATASTRUCT(ctypes.Structure): class COPYDATASTRUCT(ctypes.Structure):
""" """
ctypes implementation of ctypes implementation of
@ -68,8 +61,7 @@ class COPYDATASTRUCT(ctypes.Structure):
('num_data', ULONG_PTR), ('num_data', ULONG_PTR),
('data_size', ctypes.wintypes.DWORD), ('data_size', ctypes.wintypes.DWORD),
('data_loc', ctypes.c_void_p), ('data_loc', ctypes.c_void_p),
] ]
def _query_pageant(msg): def _query_pageant(msg):
""" """
@ -82,7 +74,7 @@ def _query_pageant(msg):
return None return None
# create a name for the mmap # create a name for the mmap
map_name = 'PageantRequest%08x' % thread.get_ident() map_name = 'PageantRequest%08x' % threading.current_thread().ident
pymap = _winapi.MemoryMap(map_name, _AGENT_MAX_MSGLEN, pymap = _winapi.MemoryMap(map_name, _AGENT_MAX_MSGLEN,
_winapi.get_security_attributes_for_user(), _winapi.get_security_attributes_for_user(),
@ -90,7 +82,7 @@ def _query_pageant(msg):
with pymap: with pymap:
pymap.write(msg) pymap.write(msg)
# Create an array buffer containing the mapped filename # Create an array buffer containing the mapped filename
char_buffer = array.array("c", b(map_name) + zero_byte) char_buffer = array.array("c", map_name + '\0')
char_buffer_address, char_buffer_size = char_buffer.buffer_info() char_buffer_address, char_buffer_size = char_buffer.buffer_info()
# Create a string to use for the SendMessage function call # Create a string to use for the SendMessage function call
cds = COPYDATASTRUCT(_AGENT_COPYDATA_ID, char_buffer_size, cds = COPYDATASTRUCT(_AGENT_COPYDATA_ID, char_buffer_size,
@ -106,8 +98,7 @@ def _query_pageant(msg):
return datalen + pymap.read(retlen) return datalen + pymap.read(retlen)
return None return None
class PageantConnection (object):
class PageantConnection(object):
""" """
Mock "connection" to an agent which roughly approximates the behavior of Mock "connection" to an agent which roughly approximates the behavior of
a unix local-domain socket (as used by Agent). Requests are sent to the a unix local-domain socket (as used by Agent). Requests are sent to the

View File

@ -1,2 +0,0 @@
[wheel]
universal = 1

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -40,10 +40,7 @@ import sys
try: try:
from setuptools import setup from setuptools import setup
kw = { kw = {
'install_requires': [ 'install_requires': 'pycrypto >= 2.1, != 2.4',
'pycrypto >= 2.1, != 2.4',
'ecdsa',
],
} }
except ImportError: except ImportError:
from distutils.core import setup from distutils.core import setup
@ -54,31 +51,21 @@ if sys.platform == 'darwin':
setup_helper.install_custom_make_tarball() setup_helper.install_custom_make_tarball()
setup( setup(name = "paramiko",
name = "paramiko", version = "1.11.0",
version = "1.14.0", description = "SSH2 protocol library",
description = "SSH2 protocol library", author = "Jeff Forcier",
long_description = longdesc, author_email = "jeff@bitprophet.org",
author = "Jeff Forcier", url = "https://github.com/paramiko/paramiko/",
author_email = "jeff@bitprophet.org", packages = [ 'paramiko' ],
url = "https://github.com/paramiko/paramiko/", license = 'LGPL',
packages = [ 'paramiko' ], platforms = 'Posix; MacOS X; Windows',
license = 'LGPL', classifiers = [ 'Development Status :: 5 - Production/Stable',
platforms = 'Posix; MacOS X; Windows', 'Intended Audience :: Developers',
classifiers = [ 'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)',
'Development Status :: 5 - Production/Stable', 'Operating System :: OS Independent',
'Intended Audience :: Developers', 'Topic :: Internet',
'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)', 'Topic :: Security :: Cryptography' ],
'Operating System :: OS Independent', long_description = longdesc,
'Topic :: Internet', **kw
'Topic :: Security :: Cryptography', )
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.2',
'Programming Language :: Python :: 3.3',
],
**kw
)

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.

View File

@ -1,6 +0,0 @@
SSH Agents
==========
.. automodule:: paramiko.agent
:inherited-members:
:no-special-members:

View File

@ -1,4 +0,0 @@
Buffered pipes
==============
.. automodule:: paramiko.buffered_pipe

View File

@ -1,4 +0,0 @@
Channel
=======
.. automodule:: paramiko.channel

View File

@ -1,5 +0,0 @@
Client
======
.. automodule:: paramiko.client
:member-order: bysource

View File

@ -1,5 +0,0 @@
Configuration
=============
.. automodule:: paramiko.config
:member-order: bysource

View File

@ -1,4 +0,0 @@
Buffered files
==============
.. automodule:: paramiko.file

View File

@ -1,5 +0,0 @@
Host keys / ``known_hosts`` files
=================================
.. automodule:: paramiko.hostkeys
:member-order: bysource

View File

@ -1,6 +0,0 @@
Key handling
============
.. automodule:: paramiko.pkey
.. automodule:: paramiko.dsskey
.. automodule:: paramiko.rsakey

View File

@ -1,4 +0,0 @@
Message
=======
.. automodule:: paramiko.message

View File

@ -1,4 +0,0 @@
Packetizer
==========
.. automodule:: paramiko.packet

View File

@ -1,4 +0,0 @@
Cross-platform pipe implementations
===================================
.. automodule:: paramiko.pipe

View File

@ -1,4 +0,0 @@
``ProxyCommand`` support
========================
.. automodule:: paramiko.proxy

View File

@ -1,5 +0,0 @@
Server implementation
=====================
.. automodule:: paramiko.server
:member-order: bysource

View File

@ -1,13 +0,0 @@
SFTP
====
.. automodule:: paramiko.sftp
.. automodule:: paramiko.sftp_client
.. automodule:: paramiko.sftp_server
.. automodule:: paramiko.sftp_attr
.. automodule:: paramiko.sftp_file
:inherited-members:
:no-special-members:
:show-inheritance:
.. automodule:: paramiko.sftp_handle
.. automodule:: paramiko.sftp_si

View File

@ -1,4 +0,0 @@
Exceptions
==========
.. automodule:: paramiko.ssh_exception

View File

@ -1,5 +0,0 @@
Transport
=========
.. automodule:: paramiko.transport
:member-order: bysource

View File

@ -1,16 +0,0 @@
# Obtain shared config values
import os, sys
sys.path.append(os.path.abspath('..'))
sys.path.append(os.path.abspath('../..'))
from shared_conf import *
# Enable autodoc, intersphinx
extensions.extend(['sphinx.ext.autodoc'])
# Autodoc settings
autodoc_default_flags = ['members', 'special-members']
# Sister-site links to WWW
html_theme_options['extra_nav_links'] = {
"Main website": 'http://www.paramiko.org',
}

View File

@ -1,72 +0,0 @@
====================================
Welcome to Paramiko's documentation!
====================================
This site covers Paramiko's usage & API documentation. For basic info on what
Paramiko is, including its public changelog & how the project is maintained,
please see `the main project website <http://paramiko.org>`_.
API documentation
=================
The high-level client API starts with creation of an `.SSHClient` object. For
more direct control, pass a socket (or socket-like object) to a `.Transport`,
and use `start_server <.Transport.start_server>` or `start_client
<.Transport.start_client>` to negotiate with the remote host as either a server
or client.
As a client, you are responsible for authenticating using a password or private
key, and checking the server's host key. (Key signature and verification is
done by paramiko, but you will need to provide private keys and check that the
content of a public key matches what you expected to see.)
As a server, you are responsible for deciding which users, passwords, and keys
to allow, and what kind of channels to allow.
Once you have finished, either side may request flow-controlled `channels
<.Channel>` to the other side, which are Python objects that act like sockets,
but send and receive data over the encrypted session.
For details, please see the following tables of contents (which are organized
by area of interest.)
Core SSH protocol classes
-------------------------
.. toctree::
api/channel
api/client
api/message
api/packet
api/transport
Authentication & keys
---------------------
.. toctree::
api/agent
api/hostkeys
api/keys
Other primary functions
-----------------------
.. toctree::
api/config
api/proxy
api/server
api/sftp
Miscellany
----------
.. toctree::
api/buffered_pipe
api/file
api/pipe
api/ssh_exception

View File

@ -1,41 +0,0 @@
from datetime import datetime
import alabaster
# Alabaster theme + mini-extension
html_theme_path = [alabaster.get_path()]
extensions = ['alabaster', 'sphinx.ext.intersphinx']
# Paths relative to invoking conf.py - not this shared file
html_theme = 'alabaster'
html_theme_options = {
'description': "A Python implementation of SSHv2.",
'github_user': 'paramiko',
'github_repo': 'paramiko',
'gittip_user': 'bitprophet',
'analytics_id': 'UA-18486793-2',
'travis_button': True,
}
html_sidebars = {
'**': [
'about.html',
'navigation.html',
'searchbox.html',
'donate.html',
]
}
# Everything intersphinx's to Python
intersphinx_mapping = {
'python': ('http://docs.python.org/2.6', None),
}
# Regular settings
project = 'Paramiko'
year = datetime.now().year
copyright = '%d Jeff Forcier' % year
master_doc = 'index'
templates_path = ['_templates']
exclude_trees = ['_build']
source_suffix = '.rst'
default_role = 'obj'

View File

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<atom:link href="{{ atom }}" rel="self" type="application/rss+xml" />
<title>{{ title }}</title>
<link>{{ link }}</link>
<description>{{ description }}</description>
<pubDate>{{ date }}</pubDate>
{% for link, title, desc, date in posts %}
<item>
<link>{{ link }}</link>
<guid>{{ link }}</guid>
<title><![CDATA[{{ title }}]]></title>
<description><![CDATA[{{ desc }}]]></description>
<pubDate>{{ date }}</pubDate>
</item>
{% endfor %}
</channel>
</rss>

View File

@ -1,187 +0,0 @@
=========
Changelog
=========
* :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.
* :support:`284` Add Python language trove identifiers to ``setup.py``. Thanks
to Alex Gaynor for catch & patch.
* :bug:`235` Improve string type testing in a handful of spots (e.g. ``s/if
type(x) is str/if isinstance(x, basestring)/g``.) Thanks to ``@ksamuel`` for
the report.
* :release:`1.13.0 <2014-03-13>`
* :release:`1.12.3 <2014-03-13>`
* :release:`1.11.5 <2014-03-13>`
* :release:`1.10.7 <2014-03-13>`
* :feature:`16` **Python 3 support!** Our test suite passes under Python 3, and
it (& Fabric's test suite) continues to pass under Python 2. **Python 2.5 is
no longer supported with this change!**
The merged code was built on many contributors' efforts, both code &
feedback. In no particular order, we thank Daniel Goertzen, Ivan Kolodyazhny,
Tomi Pieviläinen, Jason R. Coombs, Jan N. Schulze, ``@Lazik``, Dorian Pula,
Scott Maxwell, Tshepang Lekhonkhobe, Aaron Meurer, and Dave Halter.
* :support:`256 backported` Convert API documentation to Sphinx, yielding a new
API docs website to replace the old Epydoc one. Thanks to Olle Lundberg for
the initial conversion work.
* :bug:`-` Use constant-time hash comparison operations where possible, to
protect against `timing-based attacks
<http://codahale.com/a-lesson-in-timing-attacks/>`_. Thanks to Alex Gaynor
for the patch.
* :release:`1.12.2 <2014-02-14>`
* :release:`1.11.4 <2014-02-14>`
* :release:`1.10.6 <2014-02-14>`
* :feature:`58` Allow client code to access the stored SSH server banner via
`Transport.get_banner <paramiko.transport.Transport.get_banner>`. Thanks to
``@Jhoanor`` for the patch.
* :bug:`252` (`Fabric #1020 <https://github.com/fabric/fabric/issues/1020>`_)
Enhanced the implementation of ``ProxyCommand`` to avoid a deadlock/hang
condition that frequently occurs at ``Transport`` shutdown time. Thanks to
Mateusz Kobos, Matthijs van der Vleuten and Guillaume Zitta for the original
reports and to Marius Gedminas for helping test nontrivial use cases.
* :bug:`268` Fix some missed renames of ``ProxyCommand`` related error classes.
Thanks to Marius Gedminas for catch & patch.
* :bug:`34` (PR :issue:`35`) Fix SFTP prefetching incompatibility with some
SFTP servers regarding request/response ordering. Thanks to Richard
Kettlewell.
* :bug:`193` (and its attentant PRs :issue:`230` & :issue:`253`) Fix SSH agent
problems present on Windows. Thanks to David Hobbs for initial report and to
Aarni Koskela & Olle Lundberg for the patches.
* :release:`1.12.1 <2014-01-08>`
* :release:`1.11.3 <2014-01-08>`
* :release:`1.10.5 <2014-01-08>`
* :bug:`225 (1.12+)` Note ecdsa requirement in README. Thanks to Amaury
Rodriguez for the catch.
* :bug:`176` Fix AttributeError bugs in known_hosts file (re)loading. Thanks
to Nathan Scowcroft for the patch & Martin Blumenstingl for the initial test
case.
* :release:`1.12.0 <2013-09-27>`
* :release:`1.11.2 <2013-09-27>`
* :release:`1.10.4 <2013-09-27>`
* :feature:`152` Add tentative support for ECDSA keys. **This adds the ecdsa
module as a new dependency of Paramiko.** The module is available at
`warner/python-ecdsa on Github <https://github.com/warner/python-ecdsa>`_ and
`ecdsa on PyPI <https://pypi.python.org/pypi/ecdsa>`_.
* Note that you might still run into problems with key negotiation --
Paramiko picks the first key that the server offers, which might not be
what you have in your known_hosts file.
* Mega thanks to Ethan Glasser-Camp for the patch.
* :feature:`136` Add server-side support for the SSH protocol's 'env' command.
Thanks to Benjamin Pollack for the patch.
* :bug:`156 (1.11+)` Fix potential deadlock condition when using Channel
objects as sockets (e.g. when using SSH gatewaying). Thanks to Steven Noonan
and Frank Arnold for catch & patch.
* :bug:`179` Fix a missing variable causing errors when an ssh_config file has
a non-default AddressFamily set. Thanks to Ed Marshall & Tomaz Muraus for
catch & patch.
* :bug:`200` Fix an exception-causing typo in ``demo_simple.py``. Thanks to Alex
Buchanan for catch & Dave Foster for patch.
* :bug:`199` Typo fix in the license header cross-project. Thanks to Armin
Ronacher for catch & patch.
* :release:`1.11.1 <2013-09-20>`
* :release:`1.10.3 <2013-09-20>`
* :bug:`162` Clean up HMAC module import to avoid deadlocks in certain uses of
SSHClient. Thanks to Gernot Hillier for the catch & suggested fix.
* :bug:`36` Fix the port-forwarding demo to avoid file descriptor errors.
Thanks to Jonathan Halcrow for catch & patch.
* :bug:`168` Update config handling to properly handle multiple 'localforward'
and 'remoteforward' keys. Thanks to Emre Yılmaz for the patch.
* :release:`1.11.0 <2013-07-26>`
* :release:`1.10.2 <2013-07-26>`
* :bug:`98 major` On Windows, when interacting with the PuTTY PAgeant, Paramiko
now creates the shared memory map with explicit Security Attributes of the
user, which is the same technique employed by the canonical PuTTY library to
avoid permissions issues when Paramiko is running under a different UAC
context than the PuTTY Ageant process. Thanks to Jason R. Coombs for the
patch.
* :support:`100` Remove use of PyWin32 in ``win_pageant`` module. Module was
already dependent on ctypes for constructing appropriate structures and had
ctypes implementations of all functionality. Thanks to Jason R. Coombs for
the patch.
* :bug:`87 major` Ensure updates to ``known_hosts`` files account for any
updates to said files after Paramiko initially read them. (Includes related
fix to guard against duplicate entries during subsequent ``known_hosts``
loads.) Thanks to ``@sunweaver`` for the contribution.
* :bug:`153` (also :issue:`67`) Warn on parse failure when reading known_hosts
file. Thanks to ``@glasserc`` for patch.
* :bug:`146` Indentation fixes for readability. Thanks to Abhinav Upadhyay for
catch & patch.
* :release:`1.10.1 <2013-04-05>`
* :bug:`142` (`Fabric #811 <https://github.com/fabric/fabric/issues/811>`_)
SFTP put of empty file will still return the attributes of the put file.
Thanks to Jason R. Coombs for the patch.
* :bug:`154` (`Fabric #876 <https://github.com/fabric/fabric/issues/876>`_)
Forwarded SSH agent connections left stale local pipes lying around, which
could cause local (and sometimes remote or network) resource starvation when
running many agent-using remote commands. Thanks to Kevin Tegtmeier for catch
& patch.
* :release:`1.10.0 <2013-03-01>`
* :feature:`66` Batch SFTP writes to help speed up file transfers. Thanks to
Olle Lundberg for the patch.
* :bug:`133 major` Fix handling of window-change events to be on-spec and not
attempt to wait for a response from the remote sshd; this fixes problems with
less common targets such as some Cisco devices. Thanks to Phillip Heller for
catch & patch.
* :feature:`93` Overhaul SSH config parsing to be in line with ``man
ssh_config`` (& the behavior of ``ssh`` itself), including addition of parameter
expansion within config values. Thanks to Olle Lundberg for the patch.
* :feature:`110` Honor SSH config ``AddressFamily`` setting when looking up
local host's FQDN. Thanks to John Hensley for the patch.
* :feature:`128` Defer FQDN resolution until needed, when parsing SSH config
files. Thanks to Parantapa Bhattacharya for catch & patch.
* :bug:`102 major` Forego random padding for packets when running under
``*-ctr`` ciphers. This corrects some slowdowns on platforms where random
byte generation is inefficient (e.g. Windows). Thanks to ``@warthog618`` for
catch & patch, and Michael van der Kolff for code/technique review.
* :feature:`127` Turn ``SFTPFile`` into a context manager. Thanks to Michael
Williamson for the patch.
* :feature:`116` Limit ``Message.get_bytes`` to an upper bound of 1MB to protect
against potential DoS vectors. Thanks to ``@mvschaik`` for catch & patch.
* :feature:`115` Add convenience ``get_pty`` kwarg to ``Client.exec_command`` so
users not manually controlling a channel object can still toggle PTY
creation. Thanks to Michael van der Kolff for the patch.
* :feature:`71` Add ``SFTPClient.putfo`` and ``.getfo`` methods to allow direct
uploading/downloading of file-like objects. Thanks to Eric Buehl for the
patch.
* :feature:`113` Add ``timeout`` parameter to ``SSHClient.exec_command`` for
easier setting of the command's internal channel object's timeout. Thanks to
Cernov Vladimir for the patch.
* :support:`94` Remove duplication of SSH port constant. Thanks to Olle
Lundberg for the catch.
* :feature:`80` Expose the internal "is closed" property of the file transfer
class ``BufferedFile`` as ``.closed``, better conforming to Python's file
interface. Thanks to ``@smunaut`` and James Hiscock for catch & patch.

View File

@ -1,28 +0,0 @@
# Obtain shared config values
import sys
import os
from os.path import abspath, join, dirname
sys.path.append(abspath(join(dirname(__file__), '..')))
from shared_conf import *
# Releases changelog extension
extensions.append('releases')
# 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)
# Sister-site links to API docs
html_theme_options['extra_nav_links'] = {
"API Docs": 'http://docs.paramiko.org',
}

View File

@ -1,11 +0,0 @@
=======
Contact
=======
You can get in touch with the developer & user community in any of the
following ways:
* IRC: ``#paramiko`` on Freenode
* Mailing list: ``paramiko@librelist.com`` (see `the LibreList homepage
<http://librelist.com>`_ for usage details).
* This website - a blog section is forthcoming.

View File

@ -1,22 +0,0 @@
============
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.)
How to submit bug reports or new code
=====================================
Please see `this project-agnostic contribution guide
<http://contribution-guide.org>`_ - we follow it explicitly.
Our current changelog is located in ``sites/www/changelog.rst`` - the top
level files like ``ChangeLog.*`` and ``NEWS`` are historical only.

View File

@ -1,9 +0,0 @@
===================================
Frequently Asked/Answered Questions
===================================
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.

View File

@ -1,36 +0,0 @@
Welcome to Paramiko!
====================
Paramiko is a Python (2.6+, 3.3+) implementation of the SSHv2 protocol [#]_,
providing both client and server functionality. While it leverages a Python C
extension for low level cryptography (`PyCrypto <http://pycrypto.org>`_),
Paramiko itself is a pure Python interface around SSH networking concepts.
This website covers project information for Paramiko such as the changelog,
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
.. rubric:: Footnotes
.. [#]
SSH is defined in RFCs
`4251 <http://www.rfc-editor.org/rfc/rfc4251.txt>`_,
`4252 <http://www.rfc-editor.org/rfc/rfc4252.txt>`_,
`4253 <http://www.rfc-editor.org/rfc/rfc4253.txt>`_, and
`4254 <http://www.rfc-editor.org/rfc/rfc4254.txt>`_;
the primary working implementation of the protocol is the `OpenSSH project
<http://openssh.org>`_. Paramiko implements a large portion of the SSH
feature set, but there are occasional gaps.

View File

@ -1,101 +0,0 @@
==========
Installing
==========
.. _paramiko-itself:
Paramiko itself
===============
The recommended way to get Paramiko is to **install the latest stable release**
via `pip <http://pip-installer.org>`_::
$ pip install paramiko
.. note::
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
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
obtained Paramiko's package; PyCrypto may require more work. Read on for
details.
.. _release-lines:
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**.
If you're unsure which version to install, we have suggestions:
* **Completely new users** should always default to the **latest stable
release** (as above, whatever is newest / whatever shows up with ``pip
install paramiko``.)
* **Users upgrading from a much older version** (e.g. the 1.7.x line) should
probably get the **oldest actively supported line** (see the paragraph above
this list for what that currently is.)
* **Everybody else** is hopefully already "on" a given version and can
carefully upgrade to whichever version they care to, when their release line
stops being supported.
PyCrypto
========
`PyCrypto <https://www.dlitz.net/software/pycrypto/>`_ provides the low-level
(C-based) encryption algorithms we need to implement the SSH protocol. There
are a couple gotchas associated with installing PyCrypto: its compatibility
with Python's package tools, and the fact that it is a C-based extension.
C extension
-----------
Unless you are installing from a precompiled source such as a Debian apt
repository or RedHat RPM, or using :ref:`pypm <pypm>`, you will also need the
ability to build Python C-based modules from source in order to install
PyCrypto. Users on **Unix-based platforms** such as Ubuntu or Mac OS X will
need the traditional C build toolchain installed (e.g. Developer Tools / XCode
Tools on the Mac, or the ``build-essential`` package on Ubuntu or Debian Linux
-- basically, anything with ``gcc``, ``make`` and so forth) as well as the
Python development libraries, often named ``python-dev`` or similar.
For **Windows** users we recommend using :ref:`pypm`, installing a C
development environment such as `Cygwin <http://cygwin.com>`_ or obtaining a
precompiled Win32 PyCrypto package from `voidspace's Python modules page
<http://www.voidspace.org.uk/python/modules.shtml#pycrypto>`_.
.. note::
Some Windows users whose Python is 64-bit have found that the PyCrypto
dependency ``winrandom`` may not install properly, leading to ImportErrors.
In this scenario, you'll probably need to compile ``winrandom`` yourself
via e.g. MS Visual Studio. See `Fabric #194
<https://github.com/fabric/fabric/issues/194>`_ for info.
.. _pypm:
ActivePython and PyPM
=====================
Windows users who already have ActiveState's `ActivePython
<http://www.activestate.com/activepython/downloads>`_ distribution installed
may find Paramiko is best installed with `its package manager, PyPM
<http://code.activestate.com/pypm/>`_. Below is example output from an
installation of Paramiko via ``pypm``::
C:\> pypm install paramiko
The following packages will be installed into "%APPDATA%\Python" (2.7):
paramiko-1.7.8 pycrypto-2.4
Get: [pypm-free.activestate.com] paramiko 1.7.8
Get: [pypm-free.activestate.com] pycrypto 2.4
Installing paramiko-1.7.8
Installing pycrypto-2.4
C:\>

View File

@ -1,49 +0,0 @@
from os import mkdir
from os.path import join
from shutil import rmtree, copytree
from invoke import Collection, ctask as task
from invocations import docs as _docs
from invocations.packaging import publish
d = 'sites'
# Usage doc/API site (published as docs.paramiko.org)
docs_path = join(d, 'docs')
docs_build = join(docs_path, '_build')
docs = Collection.from_module(_docs, name='docs', config={
'sphinx.source': docs_path,
'sphinx.target': docs_build,
})
# Main/about/changelog site ((www.)?paramiko.org)
www_path = join(d, 'www')
www = Collection.from_module(_docs, name='www', config={
'sphinx.source': www_path,
'sphinx.target': join(www_path, '_build'),
})
# Until we move to spec-based testing
@task
def test(ctx):
ctx.run("python test.py --verbose")
@task
def coverage(ctx):
ctx.run("coverage run --source=paramiko test.py --verbose")
# Until we stop bundling docs w/ releases. Need to discover use cases first.
@task('docs') # Will invoke the API doc site build
def release(ctx):
# Move the built docs into where Epydocs used to live
target = 'docs'
rmtree(target, ignore_errors=True)
copytree(docs_build, target)
# Publish
publish(ctx, wheel=True)
ns = Collection(test, coverage, release, docs=docs, www=www)

43
test.py
View File

@ -9,7 +9,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -29,21 +29,22 @@ import unittest
from optparse import OptionParser from optparse import OptionParser
import paramiko import paramiko
import threading import threading
from paramiko.py3compat import PY2
sys.path.append('tests') sys.path.append('tests')
from tests.test_message import MessageTest from test_message import MessageTest
from tests.test_file import BufferedFileTest from test_file import BufferedFileTest
from tests.test_buffered_pipe import BufferedPipeTest from test_buffered_pipe import BufferedPipeTest
from tests.test_util import UtilTest from test_util import UtilTest
from tests.test_hostkeys import HostKeysTest from test_hostkeys import HostKeysTest
from tests.test_pkey import KeyTest from test_pkey import KeyTest
from tests.test_kex import KexTest from test_kex import KexTest
from tests.test_packetizer import PacketizerTest from test_packetizer import PacketizerTest
from tests.test_auth import AuthTest from test_auth import AuthTest
from tests.test_transport import TransportTest from test_transport import TransportTest
from tests.test_client import SSHClientTest from test_sftp import SFTPTest
from test_sftp_big import BigSFTPTest
from test_client import SSHClientTest
default_host = 'localhost' default_host = 'localhost'
default_user = os.environ.get('USER', 'nobody') default_user = os.environ.get('USER', 'nobody')
@ -101,23 +102,20 @@ def main():
parser.add_option('-P', '--sftp-passwd', dest='password', type='string', default=default_passwd, parser.add_option('-P', '--sftp-passwd', dest='password', type='string', default=default_passwd,
metavar='<password>', metavar='<password>',
help='[with -R] (optional) password to unlock the private key for remote sftp tests') help='[with -R] (optional) password to unlock the private key for remote sftp tests')
options, args = parser.parse_args() options, args = parser.parse_args()
# setup logging # setup logging
paramiko.util.log_to_file('test.log') paramiko.util.log_to_file('test.log')
if options.use_sftp: if options.use_sftp:
from tests.test_sftp import SFTPTest
if options.use_loopback_sftp: if options.use_loopback_sftp:
SFTPTest.init_loopback() SFTPTest.init_loopback()
else: else:
SFTPTest.init(options.hostname, options.username, options.keyfile, options.password) SFTPTest.init(options.hostname, options.username, options.keyfile, options.password)
if not options.use_big_file: if not options.use_big_file:
SFTPTest.set_big_file_test(False) SFTPTest.set_big_file_test(False)
if options.use_big_file:
from tests.test_sftp_big import BigSFTPTest
suite = unittest.TestSuite() suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(MessageTest)) suite.addTest(unittest.makeSuite(MessageTest))
suite.addTest(unittest.makeSuite(BufferedFileTest)) suite.addTest(unittest.makeSuite(BufferedFileTest))
@ -149,10 +147,7 @@ def main():
# TODO: make that not a problem, jeez # TODO: make that not a problem, jeez
for thread in threading.enumerate(): for thread in threading.enumerate():
if thread is not threading.currentThread(): if thread is not threading.currentThread():
if PY2: thread._Thread__stop()
thread._Thread__stop()
else:
thread._stop()
# Exit correctly # Exit correctly
if not result.wasSuccessful(): if not result.wasSuccessful():
sys.exit(1) sys.exit(1)

View File

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -21,7 +21,6 @@
""" """
import threading, socket import threading, socket
from paramiko.common import asbytes
class LoopSocket (object): class LoopSocket (object):
@ -32,7 +31,7 @@ class LoopSocket (object):
""" """
def __init__(self): def __init__(self):
self.__in_buffer = bytes() self.__in_buffer = ''
self.__lock = threading.Lock() self.__lock = threading.Lock()
self.__cv = threading.Condition(self.__lock) self.__cv = threading.Condition(self.__lock)
self.__timeout = None self.__timeout = None
@ -42,12 +41,11 @@ class LoopSocket (object):
self.__unlink() self.__unlink()
try: try:
self.__lock.acquire() self.__lock.acquire()
self.__in_buffer = bytes() self.__in_buffer = ''
finally: finally:
self.__lock.release() self.__lock.release()
def send(self, data): def send(self, data):
data = asbytes(data)
if self.__mate is None: if self.__mate is None:
# EOF # EOF
raise EOFError() raise EOFError()
@ -59,7 +57,7 @@ class LoopSocket (object):
try: try:
if self.__mate is None: if self.__mate is None:
# EOF # EOF
return bytes() return ''
if len(self.__in_buffer) == 0: if len(self.__in_buffer) == 0:
self.__cv.wait(self.__timeout) self.__cv.wait(self.__timeout)
if len(self.__in_buffer) == 0: if len(self.__in_buffer) == 0:

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -23,7 +23,6 @@ A stub SFTP server for loopback SFTP testing.
import os import os
from paramiko import ServerInterface, SFTPServerInterface, SFTPServer, SFTPAttributes, \ from paramiko import ServerInterface, SFTPServerInterface, SFTPServer, SFTPAttributes, \
SFTPHandle, SFTP_OK, AUTH_SUCCESSFUL, OPEN_SUCCEEDED SFTPHandle, SFTP_OK, AUTH_SUCCESSFUL, OPEN_SUCCEEDED
from paramiko.common import o666
class StubServer (ServerInterface): class StubServer (ServerInterface):
@ -63,7 +62,7 @@ class StubSFTPServer (SFTPServerInterface):
def list_folder(self, path): def list_folder(self, path):
path = self._realpath(path) path = self._realpath(path)
try: try:
out = [] out = [ ]
flist = os.listdir(path) flist = os.listdir(path)
for fname in flist: for fname in flist:
attr = SFTPAttributes.from_stat(os.stat(os.path.join(path, fname))) attr = SFTPAttributes.from_stat(os.stat(os.path.join(path, fname)))
@ -90,15 +89,15 @@ class StubSFTPServer (SFTPServerInterface):
def open(self, path, flags, attr): def open(self, path, flags, attr):
path = self._realpath(path) path = self._realpath(path)
try: try:
binary_flag = getattr(os, 'O_BINARY', 0) binary_flag = getattr(os, 'O_BINARY', 0)
flags |= binary_flag flags |= binary_flag
mode = getattr(attr, 'st_mode', None) mode = getattr(attr, 'st_mode', None)
if mode is not None: if mode is not None:
fd = os.open(path, flags, mode) fd = os.open(path, flags, mode)
else: else:
# os.open() defaults to 0777 which is # os.open() defaults to 0o777 which is
# an odd default mode for files # an odd default mode for files
fd = os.open(path, flags, o666) fd = os.open(path, flags, 0o666)
except OSError as e: except OSError as e:
return SFTPServer.convert_errno(e.errno) return SFTPServer.convert_errno(e.errno)
if (flags & os.O_CREAT) and (attr is not None): if (flags & os.O_CREAT) and (attr is not None):

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -25,21 +25,18 @@ import threading
import unittest import unittest
from paramiko import Transport, ServerInterface, RSAKey, DSSKey, \ from paramiko import Transport, ServerInterface, RSAKey, DSSKey, \
BadAuthenticationType, InteractiveQuery, \ SSHException, BadAuthenticationType, InteractiveQuery, ChannelException, \
AuthenticationException AuthenticationException
from paramiko import AUTH_FAILED, AUTH_PARTIALLY_SUCCESSFUL, AUTH_SUCCESSFUL from paramiko import AUTH_FAILED, AUTH_PARTIALLY_SUCCESSFUL, AUTH_SUCCESSFUL
from paramiko.py3compat import u from paramiko import OPEN_SUCCEEDED, OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
from tests.loop import LoopSocket from loop import LoopSocket
from tests.util import test_path
_pwd = u('\u2022')
class NullServer (ServerInterface): class NullServer (ServerInterface):
paranoid_did_password = False paranoid_did_password = False
paranoid_did_public_key = False paranoid_did_public_key = False
paranoid_key = DSSKey.from_private_key_file(test_path('test_dss.key')) paranoid_key = DSSKey.from_private_key_file('tests/test_dss.key')
def get_allowed_auths(self, username): def get_allowed_auths(self, username):
if username == 'slowdive': if username == 'slowdive':
return 'publickey,password' return 'publickey,password'
@ -67,7 +64,7 @@ class NullServer (ServerInterface):
if self.paranoid_did_public_key: if self.paranoid_did_public_key:
return AUTH_SUCCESSFUL return AUTH_SUCCESSFUL
return AUTH_PARTIALLY_SUCCESSFUL return AUTH_PARTIALLY_SUCCESSFUL
if (username == 'utf8') and (password == _pwd): if (username == 'utf8') and (password == u'\u2022'):
return AUTH_SUCCESSFUL return AUTH_SUCCESSFUL
if (username == 'non-utf8') and (password == '\xff'): if (username == 'non-utf8') and (password == '\xff'):
return AUTH_SUCCESSFUL return AUTH_SUCCESSFUL
@ -113,18 +110,18 @@ class AuthTest (unittest.TestCase):
self.sockc.close() self.sockc.close()
def start_server(self): def start_server(self):
host_key = RSAKey.from_private_key_file(test_path('test_rsa.key')) host_key = RSAKey.from_private_key_file('tests/test_rsa.key')
self.public_host_key = RSAKey(data=host_key.asbytes()) self.public_host_key = RSAKey(data=str(host_key))
self.ts.add_server_key(host_key) self.ts.add_server_key(host_key)
self.event = threading.Event() self.event = threading.Event()
self.server = NullServer() self.server = NullServer()
self.assertTrue(not self.event.isSet()) self.assert_(not self.event.isSet())
self.ts.start_server(self.event, self.server) self.ts.start_server(self.event, self.server)
def verify_finished(self): def verify_finished(self):
self.event.wait(1.0) self.event.wait(1.0)
self.assertTrue(self.event.isSet()) self.assert_(self.event.isSet())
self.assertTrue(self.ts.is_active()) self.assert_(self.ts.is_active())
def test_1_bad_auth_type(self): def test_1_bad_auth_type(self):
""" """
@ -135,11 +132,11 @@ class AuthTest (unittest.TestCase):
try: try:
self.tc.connect(hostkey=self.public_host_key, self.tc.connect(hostkey=self.public_host_key,
username='unknown', password='error') username='unknown', password='error')
self.assertTrue(False) self.assert_(False)
except: except:
etype, evalue, etb = sys.exc_info() etype, evalue, etb = sys.exc_info()
self.assertEqual(BadAuthenticationType, etype) self.assertEquals(BadAuthenticationType, etype)
self.assertEqual(['publickey'], evalue.allowed_types) self.assertEquals(['publickey'], evalue.allowed_types)
def test_2_bad_password(self): def test_2_bad_password(self):
""" """
@ -150,10 +147,10 @@ class AuthTest (unittest.TestCase):
self.tc.connect(hostkey=self.public_host_key) self.tc.connect(hostkey=self.public_host_key)
try: try:
self.tc.auth_password(username='slowdive', password='error') self.tc.auth_password(username='slowdive', password='error')
self.assertTrue(False) self.assert_(False)
except: except:
etype, evalue, etb = sys.exc_info() etype, evalue, etb = sys.exc_info()
self.assertTrue(issubclass(etype, AuthenticationException)) self.assert_(issubclass(etype, AuthenticationException))
self.tc.auth_password(username='slowdive', password='pygmalion') self.tc.auth_password(username='slowdive', password='pygmalion')
self.verify_finished() self.verify_finished()
@ -164,10 +161,10 @@ class AuthTest (unittest.TestCase):
self.start_server() self.start_server()
self.tc.connect(hostkey=self.public_host_key) self.tc.connect(hostkey=self.public_host_key)
remain = self.tc.auth_password(username='paranoid', password='paranoid') remain = self.tc.auth_password(username='paranoid', password='paranoid')
self.assertEqual(['publickey'], remain) self.assertEquals(['publickey'], remain)
key = DSSKey.from_private_key_file(test_path('test_dss.key')) key = DSSKey.from_private_key_file('tests/test_dss.key')
remain = self.tc.auth_publickey(username='paranoid', key=key) remain = self.tc.auth_publickey(username='paranoid', key=key)
self.assertEqual([], remain) self.assertEquals([], remain)
self.verify_finished() self.verify_finished()
def test_4_interactive_auth(self): def test_4_interactive_auth(self):
@ -183,9 +180,9 @@ class AuthTest (unittest.TestCase):
self.got_prompts = prompts self.got_prompts = prompts
return ['cat'] return ['cat']
remain = self.tc.auth_interactive('commie', handler) remain = self.tc.auth_interactive('commie', handler)
self.assertEqual(self.got_title, 'password') self.assertEquals(self.got_title, 'password')
self.assertEqual(self.got_prompts, [('Password', False)]) self.assertEquals(self.got_prompts, [('Password', False)])
self.assertEqual([], remain) self.assertEquals([], remain)
self.verify_finished() self.verify_finished()
def test_5_interactive_auth_fallback(self): def test_5_interactive_auth_fallback(self):
@ -196,7 +193,7 @@ class AuthTest (unittest.TestCase):
self.start_server() self.start_server()
self.tc.connect(hostkey=self.public_host_key) self.tc.connect(hostkey=self.public_host_key)
remain = self.tc.auth_password('commie', 'cat') remain = self.tc.auth_password('commie', 'cat')
self.assertEqual([], remain) self.assertEquals([], remain)
self.verify_finished() self.verify_finished()
def test_6_auth_utf8(self): def test_6_auth_utf8(self):
@ -205,8 +202,8 @@ class AuthTest (unittest.TestCase):
""" """
self.start_server() self.start_server()
self.tc.connect(hostkey=self.public_host_key) self.tc.connect(hostkey=self.public_host_key)
remain = self.tc.auth_password('utf8', _pwd) remain = self.tc.auth_password('utf8', u'\u2022')
self.assertEqual([], remain) self.assertEquals([], remain)
self.verify_finished() self.verify_finished()
def test_7_auth_non_utf8(self): def test_7_auth_non_utf8(self):
@ -217,7 +214,7 @@ class AuthTest (unittest.TestCase):
self.start_server() self.start_server()
self.tc.connect(hostkey=self.public_host_key) self.tc.connect(hostkey=self.public_host_key)
remain = self.tc.auth_password('non-utf8', '\xff') remain = self.tc.auth_password('non-utf8', '\xff')
self.assertEqual([], remain) self.assertEquals([], remain)
self.verify_finished() self.verify_finished()
def test_8_auth_gets_disconnected(self): def test_8_auth_gets_disconnected(self):
@ -231,4 +228,4 @@ class AuthTest (unittest.TestCase):
remain = self.tc.auth_password('bad-server', 'hello') remain = self.tc.auth_password('bad-server', 'hello')
except: except:
etype, evalue, etb = sys.exc_info() etype, evalue, etb = sys.exc_info()
self.assertTrue(issubclass(etype, AuthenticationException)) self.assert_(issubclass(etype, AuthenticationException))

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -22,60 +22,61 @@ Some unit tests for BufferedPipe.
import threading import threading
import time import time
import unittest
from paramiko.buffered_pipe import BufferedPipe, PipeTimeout from paramiko.buffered_pipe import BufferedPipe, PipeTimeout
from paramiko import pipe from paramiko import pipe
from tests.util import ParamikoTest from util import ParamikoTest
def delay_thread(p): def delay_thread(pipe):
p.feed('a') pipe.feed('a')
time.sleep(0.5) time.sleep(0.5)
p.feed('b') pipe.feed('b')
p.close() pipe.close()
def close_thread(p): def close_thread(pipe):
time.sleep(0.2) time.sleep(0.2)
p.close() pipe.close()
class BufferedPipeTest(ParamikoTest): class BufferedPipeTest(ParamikoTest):
def test_1_buffered_pipe(self): def test_1_buffered_pipe(self):
p = BufferedPipe() p = BufferedPipe()
self.assertTrue(not p.read_ready()) self.assert_(not p.read_ready())
p.feed('hello.') p.feed('hello.')
self.assertTrue(p.read_ready()) self.assert_(p.read_ready())
data = p.read(6) data = p.read(6)
self.assertEqual(b'hello.', data) self.assertEquals('hello.', data)
p.feed('plus/minus') p.feed('plus/minus')
self.assertEqual(b'plu', p.read(3)) self.assertEquals('plu', p.read(3))
self.assertEqual(b's/m', p.read(3)) self.assertEquals('s/m', p.read(3))
self.assertEqual(b'inus', p.read(4)) self.assertEquals('inus', p.read(4))
p.close() p.close()
self.assertTrue(not p.read_ready()) self.assert_(not p.read_ready())
self.assertEqual(b'', p.read(1)) self.assertEquals('', p.read(1))
def test_2_delay(self): def test_2_delay(self):
p = BufferedPipe() p = BufferedPipe()
self.assertTrue(not p.read_ready()) self.assert_(not p.read_ready())
threading.Thread(target=delay_thread, args=(p,)).start() threading.Thread(target=delay_thread, args=(p,)).start()
self.assertEqual(b'a', p.read(1, 0.1)) self.assertEquals('a', p.read(1, 0.1))
try: try:
p.read(1, 0.1) p.read(1, 0.1)
self.assertTrue(False) self.assert_(False)
except PipeTimeout: except PipeTimeout:
pass pass
self.assertEqual(b'b', p.read(1, 1.0)) self.assertEquals('b', p.read(1, 1.0))
self.assertEqual(b'', p.read(1)) self.assertEquals('', p.read(1))
def test_3_close_while_reading(self): def test_3_close_while_reading(self):
p = BufferedPipe() p = BufferedPipe()
threading.Thread(target=close_thread, args=(p,)).start() threading.Thread(target=close_thread, args=(p,)).start()
data = p.read(1, 1.0) data = p.read(1, 1.0)
self.assertEqual(b'', data) self.assertEquals('', data)
def test_4_or_pipe(self): def test_4_or_pipe(self):
p = pipe.make_pipe() p = pipe.make_pipe()
@ -89,3 +90,4 @@ class BufferedPipeTest(ParamikoTest):
self.assertTrue(p._set) self.assertTrue(p._set)
p2.clear() p2.clear()
self.assertFalse(p._set) self.assertFalse(p._set)

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -21,15 +21,13 @@ Some unit tests for SSHClient.
""" """
import socket import socket
from tempfile import mkstemp
import threading import threading
import time
import unittest import unittest
import weakref import weakref
import warnings from binascii import hexlify
import os
from tests.util import test_path
import paramiko import paramiko
from paramiko.common import PY2
class NullServer (paramiko.ServerInterface): class NullServer (paramiko.ServerInterface):
@ -45,7 +43,7 @@ class NullServer (paramiko.ServerInterface):
return paramiko.AUTH_FAILED return paramiko.AUTH_FAILED
def check_auth_publickey(self, username, key): 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': if (key.get_name() == 'ssh-dss') and (hexlify(key.get_fingerprint()) == '4478f0b9a23cc5182009ff755bc1d26c'):
return paramiko.AUTH_SUCCESSFUL return paramiko.AUTH_SUCCESSFUL
return paramiko.AUTH_FAILED return paramiko.AUTH_FAILED
@ -66,6 +64,8 @@ class SSHClientTest (unittest.TestCase):
self.sockl.listen(1) self.sockl.listen(1)
self.addr, self.port = self.sockl.getsockname() self.addr, self.port = self.sockl.getsockname()
self.event = threading.Event() self.event = threading.Event()
thread = threading.Thread(target=self._run)
thread.start()
def tearDown(self): def tearDown(self):
for attr in "tc ts socks sockl".split(): for attr in "tc ts socks sockl".split():
@ -75,28 +75,28 @@ class SSHClientTest (unittest.TestCase):
def _run(self): def _run(self):
self.socks, addr = self.sockl.accept() self.socks, addr = self.sockl.accept()
self.ts = paramiko.Transport(self.socks) self.ts = paramiko.Transport(self.socks)
host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key')) host_key = paramiko.RSAKey.from_private_key_file('tests/test_rsa.key')
self.ts.add_server_key(host_key) self.ts.add_server_key(host_key)
server = NullServer() server = NullServer()
self.ts.start_server(self.event, server) self.ts.start_server(self.event, server)
def test_1_client(self): def test_1_client(self):
""" """
verify that the SSHClient stuff works too. verify that the SSHClient stuff works too.
""" """
threading.Thread(target=self._run).start() host_key = paramiko.RSAKey.from_private_key_file('tests/test_rsa.key')
host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key')) public_host_key = paramiko.RSAKey(data=str(host_key))
public_host_key = paramiko.RSAKey(data=host_key.asbytes())
self.tc = paramiko.SSHClient() self.tc = paramiko.SSHClient()
self.tc.get_host_keys().add('[%s]:%d' % (self.addr, self.port), 'ssh-rsa', public_host_key) 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') self.tc.connect(self.addr, self.port, username='slowdive', password='pygmalion')
self.event.wait(1.0) self.event.wait(1.0)
self.assertTrue(self.event.isSet()) self.assert_(self.event.isSet())
self.assertTrue(self.ts.is_active()) self.assert_(self.ts.is_active())
self.assertEqual('slowdive', self.ts.get_username()) self.assertEquals('slowdive', self.ts.get_username())
self.assertEqual(True, self.ts.is_authenticated()) self.assertEquals(True, self.ts.is_authenticated())
stdin, stdout, stderr = self.tc.exec_command('yes') stdin, stdout, stderr = self.tc.exec_command('yes')
schan = self.ts.accept(1.0) schan = self.ts.accept(1.0)
@ -105,10 +105,10 @@ class SSHClientTest (unittest.TestCase):
schan.send_stderr('This is on stderr.\n') schan.send_stderr('This is on stderr.\n')
schan.close() schan.close()
self.assertEqual('Hello there.\n', stdout.readline()) self.assertEquals('Hello there.\n', stdout.readline())
self.assertEqual('', stdout.readline()) self.assertEquals('', stdout.readline())
self.assertEqual('This is on stderr.\n', stderr.readline()) self.assertEquals('This is on stderr.\n', stderr.readline())
self.assertEqual('', stderr.readline()) self.assertEquals('', stderr.readline())
stdin.close() stdin.close()
stdout.close() stdout.close()
@ -118,19 +118,18 @@ class SSHClientTest (unittest.TestCase):
""" """
verify that SSHClient works with a DSA key. verify that SSHClient works with a DSA key.
""" """
threading.Thread(target=self._run).start() host_key = paramiko.RSAKey.from_private_key_file('tests/test_rsa.key')
host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key')) public_host_key = paramiko.RSAKey(data=str(host_key))
public_host_key = paramiko.RSAKey(data=host_key.asbytes())
self.tc = paramiko.SSHClient() self.tc = paramiko.SSHClient()
self.tc.get_host_keys().add('[%s]:%d' % (self.addr, self.port), 'ssh-rsa', public_host_key) 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.tc.connect(self.addr, self.port, username='slowdive', key_filename='tests/test_dss.key')
self.event.wait(1.0) self.event.wait(1.0)
self.assertTrue(self.event.isSet()) self.assert_(self.event.isSet())
self.assertTrue(self.ts.is_active()) self.assert_(self.ts.is_active())
self.assertEqual('slowdive', self.ts.get_username()) self.assertEquals('slowdive', self.ts.get_username())
self.assertEqual(True, self.ts.is_authenticated()) self.assertEquals(True, self.ts.is_authenticated())
stdin, stdout, stderr = self.tc.exec_command('yes') stdin, stdout, stderr = self.tc.exec_command('yes')
schan = self.ts.accept(1.0) schan = self.ts.accept(1.0)
@ -139,10 +138,10 @@ class SSHClientTest (unittest.TestCase):
schan.send_stderr('This is on stderr.\n') schan.send_stderr('This is on stderr.\n')
schan.close() schan.close()
self.assertEqual('Hello there.\n', stdout.readline()) self.assertEquals('Hello there.\n', stdout.readline())
self.assertEqual('', stdout.readline()) self.assertEquals('', stdout.readline())
self.assertEqual('This is on stderr.\n', stderr.readline()) self.assertEquals('This is on stderr.\n', stderr.readline())
self.assertEqual('', stderr.readline()) self.assertEquals('', stderr.readline())
stdin.close() stdin.close()
stdout.close() stdout.close()
@ -152,103 +151,62 @@ class SSHClientTest (unittest.TestCase):
""" """
verify that SSHClient accepts and tries multiple key files. verify that SSHClient accepts and tries multiple key files.
""" """
threading.Thread(target=self._run).start() host_key = paramiko.RSAKey.from_private_key_file('tests/test_rsa.key')
host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key')) public_host_key = paramiko.RSAKey(data=str(host_key))
public_host_key = paramiko.RSAKey(data=host_key.asbytes())
self.tc = paramiko.SSHClient() self.tc = paramiko.SSHClient()
self.tc.get_host_keys().add('[%s]:%d' % (self.addr, self.port), 'ssh-rsa', public_host_key) 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.tc.connect(self.addr, self.port, username='slowdive', key_filename=[ 'tests/test_rsa.key', 'tests/test_dss.key' ])
self.event.wait(1.0) self.event.wait(1.0)
self.assertTrue(self.event.isSet()) self.assert_(self.event.isSet())
self.assertTrue(self.ts.is_active()) self.assert_(self.ts.is_active())
self.assertEqual('slowdive', self.ts.get_username()) self.assertEquals('slowdive', self.ts.get_username())
self.assertEqual(True, self.ts.is_authenticated()) self.assertEquals(True, self.ts.is_authenticated())
def test_4_auto_add_policy(self): def test_4_auto_add_policy(self):
""" """
verify that SSHClient's AutoAddPolicy works. verify that SSHClient's AutoAddPolicy works.
""" """
threading.Thread(target=self._run).start() host_key = paramiko.RSAKey.from_private_key_file('tests/test_rsa.key')
host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key')) public_host_key = paramiko.RSAKey(data=str(host_key))
public_host_key = paramiko.RSAKey(data=host_key.asbytes())
self.tc = paramiko.SSHClient() self.tc = paramiko.SSHClient()
self.tc.set_missing_host_key_policy(paramiko.AutoAddPolicy()) self.tc.set_missing_host_key_policy(paramiko.AutoAddPolicy())
self.assertEqual(0, len(self.tc.get_host_keys())) self.assertEquals(0, len(self.tc.get_host_keys()))
self.tc.connect(self.addr, self.port, username='slowdive', password='pygmalion') self.tc.connect(self.addr, self.port, username='slowdive', password='pygmalion')
self.event.wait(1.0) self.event.wait(1.0)
self.assertTrue(self.event.isSet()) self.assert_(self.event.isSet())
self.assertTrue(self.ts.is_active()) self.assert_(self.ts.is_active())
self.assertEqual('slowdive', self.ts.get_username()) self.assertEquals('slowdive', self.ts.get_username())
self.assertEqual(True, self.ts.is_authenticated()) self.assertEquals(True, self.ts.is_authenticated())
self.assertEqual(1, len(self.tc.get_host_keys())) self.assertEquals(1, len(self.tc.get_host_keys()))
self.assertEqual(public_host_key, self.tc.get_host_keys()['[%s]:%d' % (self.addr, self.port)]['ssh-rsa']) self.assertEquals(public_host_key, self.tc.get_host_keys()['[%s]:%d' % (self.addr, self.port)]['ssh-rsa'])
def test_5_save_host_keys(self): def test_5_cleanup(self):
"""
verify that SSHClient correctly saves a known_hosts file.
"""
warnings.filterwarnings('ignore', 'tempnam.*')
host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key'))
public_host_key = paramiko.RSAKey(data=host_key.asbytes())
fd, localname = mkstemp()
os.close(fd)
client = paramiko.SSHClient()
self.assertEquals(0, len(client.get_host_keys()))
host_id = '[%s]:%d' % (self.addr, self.port)
client.get_host_keys().add(host_id, 'ssh-rsa', public_host_key)
self.assertEquals(1, len(client.get_host_keys()))
self.assertEquals(public_host_key, client.get_host_keys()[host_id]['ssh-rsa'])
client.save_host_keys(localname)
with open(localname) as fd:
assert host_id in fd.read()
os.unlink(localname)
def test_6_cleanup(self):
""" """
verify that when an SSHClient is collected, its transport (and the verify that when an SSHClient is collected, its transport (and the
transport's packetizer) is closed. transport's packetizer) is closed.
""" """
# Unclear why this is borked on Py3, but it is, and does not seem worth host_key = paramiko.RSAKey.from_private_key_file('tests/test_rsa.key')
# pursuing at the moment. public_host_key = paramiko.RSAKey(data=str(host_key))
if not PY2:
return
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 = paramiko.SSHClient()
self.tc.set_missing_host_key_policy(paramiko.AutoAddPolicy()) self.tc.set_missing_host_key_policy(paramiko.AutoAddPolicy())
self.assertEqual(0, len(self.tc.get_host_keys())) self.assertEquals(0, len(self.tc.get_host_keys()))
self.tc.connect(self.addr, self.port, username='slowdive', password='pygmalion') self.tc.connect(self.addr, self.port, username='slowdive', password='pygmalion')
self.event.wait(1.0) self.event.wait(1.0)
self.assertTrue(self.event.isSet()) self.assert_(self.event.isSet())
self.assertTrue(self.ts.is_active()) self.assert_(self.ts.is_active())
p = weakref.ref(self.tc._transport.packetizer) p = weakref.ref(self.tc._transport.packetizer)
self.assertTrue(p() is not None) self.assert_(p() is not None)
self.tc.close()
del self.tc del self.tc
# hrm, sometimes p isn't cleared right away. why is that? # hrm, sometimes p isn't cleared right away. why is that?
#st = time.time() st = time.time()
#while (time.time() - st < 5.0) and (p() is not None): while (time.time() - st < 5.0) and (p() is not None):
# time.sleep(0.1) time.sleep(0.1)
self.assert_(p() is None)
# instead of dumbly waiting for the GC to collect, force a collection
# to see whether the SSHClient object is deallocated correctly
import gc
gc.collect()
self.assertTrue(p() is None)

View File

@ -1,5 +0,0 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIKB6ty3yVyKEnfF/zprx0qwC76MsMlHY4HXCnqho2eKioAoGCCqGSM49
AwEHoUQDQgAElI9mbdlaS+T9nHxY/59lFnn80EEecZDBHq4gLpccY8Mge5ZTMiMD
ADRvOqQ5R98Sxst765CAqXmRtz8vwoD96g==
-----END EC PRIVATE KEY-----

View File

@ -1,8 +0,0 @@
-----BEGIN EC PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,EEB56BC745EDB2DE04FC3FE1F8DA387E
wdt7QTCa6ahTJLaEPH7NhHyBcxhzrzf93d4UwQOuAhkM6//jKD4lF9fErHBW0f3B
ExberCU3UxfEF3xX2thXiLw47JgeOCeQUlqRFx92p36k6YmfNGX6W8CsZ3d+XodF
Z+pb6m285CiSX+W95NenFMexXFsIpntiCvTifTKJ8os=
-----END EC PRIVATE KEY-----

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -22,7 +22,6 @@ Some unit tests for the BufferedFile abstraction.
import unittest import unittest
from paramiko.file import BufferedFile from paramiko.file import BufferedFile
from paramiko.common import linefeed_byte, crlf, cr_byte
class LoopbackFile (BufferedFile): class LoopbackFile (BufferedFile):
@ -32,7 +31,7 @@ class LoopbackFile (BufferedFile):
def __init__(self, mode='r', bufsize=-1): def __init__(self, mode='r', bufsize=-1):
BufferedFile.__init__(self) BufferedFile.__init__(self)
self._set_mode(mode, bufsize) self._set_mode(mode, bufsize)
self.buffer = bytes() self.buffer = ''
def _read(self, size): def _read(self, size):
if len(self.buffer) == 0: if len(self.buffer) == 0:
@ -53,8 +52,8 @@ class BufferedFileTest (unittest.TestCase):
def test_1_simple(self): def test_1_simple(self):
f = LoopbackFile('r') f = LoopbackFile('r')
try: try:
f.write(b'hi') f.write('hi')
self.assertTrue(False, 'no exception on write to read-only file') self.assert_(False, 'no exception on write to read-only file')
except: except:
pass pass
f.close() f.close()
@ -62,14 +61,14 @@ class BufferedFileTest (unittest.TestCase):
f = LoopbackFile('w') f = LoopbackFile('w')
try: try:
f.read(1) f.read(1)
self.assertTrue(False, 'no exception to read from write-only file') self.assert_(False, 'no exception to read from write-only file')
except: except:
pass pass
f.close() f.close()
def test_2_readline(self): def test_2_readline(self):
f = LoopbackFile('r+U') f = LoopbackFile('r+U')
f.write(b'First line.\nSecond line.\r\nThird line.\nFinal line non-terminated.') f.write('First line.\nSecond line.\r\nThird line.\nFinal line non-terminated.')
self.assertEqual(f.readline(), 'First line.\n') self.assertEqual(f.readline(), 'First line.\n')
# universal newline mode should convert this linefeed: # universal newline mode should convert this linefeed:
self.assertEqual(f.readline(), 'Second line.\n') self.assertEqual(f.readline(), 'Second line.\n')
@ -81,31 +80,31 @@ class BufferedFileTest (unittest.TestCase):
f.close() f.close()
try: try:
f.readline() f.readline()
self.assertTrue(False, 'no exception on readline of closed file') self.assert_(False, 'no exception on readline of closed file')
except IOError: except IOError:
pass pass
self.assertTrue(linefeed_byte in f.newlines) self.assert_('\n' in f.newlines)
self.assertTrue(crlf in f.newlines) self.assert_('\r\n' in f.newlines)
self.assertTrue(cr_byte not in f.newlines) self.assert_('\r' not in f.newlines)
def test_3_lf(self): def test_3_lf(self):
""" """
try to trick the linefeed detector. try to trick the linefeed detector.
""" """
f = LoopbackFile('r+U') f = LoopbackFile('r+U')
f.write(b'First line.\r') f.write('First line.\r')
self.assertEqual(f.readline(), 'First line.\n') self.assertEqual(f.readline(), 'First line.\n')
f.write(b'\nSecond.\r\n') f.write('\nSecond.\r\n')
self.assertEqual(f.readline(), 'Second.\n') self.assertEqual(f.readline(), 'Second.\n')
f.close() f.close()
self.assertEqual(f.newlines, crlf) self.assertEqual(f.newlines, '\r\n')
def test_4_write(self): def test_4_write(self):
""" """
verify that write buffering is on. verify that write buffering is on.
""" """
f = LoopbackFile('r+', 1) f = LoopbackFile('r+', 1)
f.write(b'Complete line.\nIncomplete line.') f.write('Complete line.\nIncomplete line.')
self.assertEqual(f.readline(), 'Complete line.\n') self.assertEqual(f.readline(), 'Complete line.\n')
self.assertEqual(f.readline(), '') self.assertEqual(f.readline(), '')
f.write('..\n') f.write('..\n')
@ -118,12 +117,12 @@ class BufferedFileTest (unittest.TestCase):
""" """
f = LoopbackFile('r+', 512) f = LoopbackFile('r+', 512)
f.write('Not\nquite\n512 bytes.\n') f.write('Not\nquite\n512 bytes.\n')
self.assertEqual(f.read(1), b'') self.assertEqual(f.read(1), '')
f.flush() f.flush()
self.assertEqual(f.read(5), b'Not\nq') self.assertEqual(f.read(5), 'Not\nq')
self.assertEqual(f.read(10), b'uite\n512 b') self.assertEqual(f.read(10), 'uite\n512 b')
self.assertEqual(f.read(9), b'ytes.\n') self.assertEqual(f.read(9), 'ytes.\n')
self.assertEqual(f.read(3), b'') self.assertEqual(f.read(3), '')
f.close() f.close()
def test_6_buffering(self): def test_6_buffering(self):
@ -131,12 +130,12 @@ class BufferedFileTest (unittest.TestCase):
verify that flushing happens automatically on buffer crossing. verify that flushing happens automatically on buffer crossing.
""" """
f = LoopbackFile('r+', 16) f = LoopbackFile('r+', 16)
f.write(b'Too small.') f.write('Too small.')
self.assertEqual(f.read(4), b'') self.assertEqual(f.read(4), '')
f.write(b' ') f.write(' ')
self.assertEqual(f.read(4), b'') self.assertEqual(f.read(4), '')
f.write(b'Enough.') f.write('Enough.')
self.assertEqual(f.read(20), b'Too small. Enough.') self.assertEqual(f.read(20), 'Too small. Enough.')
f.close() f.close()
def test_7_read_all(self): def test_7_read_all(self):
@ -144,14 +143,9 @@ class BufferedFileTest (unittest.TestCase):
verify that read(-1) returns everything left in the file. verify that read(-1) returns everything left in the file.
""" """
f = LoopbackFile('r+', 16) f = LoopbackFile('r+', 16)
f.write(b'The first thing you need to do is open your eyes. ') f.write('The first thing you need to do is open your eyes. ')
f.write(b'Then, you need to close them again.\n') f.write('Then, you need to close them again.\n')
s = f.read(-1) s = f.read(-1)
self.assertEqual(s, b'The first thing you need to do is open your eyes. Then, you ' + self.assertEqual(s, 'The first thing you need to do is open your eyes. Then, you ' +
b'need to close them again.\n') 'need to close them again.\n')
f.close() f.close()
if __name__ == '__main__':
from unittest import main
main()

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -20,11 +20,11 @@
Some unit tests for HostKeys. Some unit tests for HostKeys.
""" """
import base64
from binascii import hexlify from binascii import hexlify
import os import os
import unittest import unittest
import paramiko import paramiko
from paramiko.py3compat import decodebytes
test_hosts_file = """\ test_hosts_file = """\
@ -36,12 +36,12 @@ BGQ3GQ/Fc7SX6gkpXkwcZryoi4kNFhHu5LvHcZPdxXV1D+uTMfGS1eyd2Yz/DoNWXNAl8TI0cAsW\
5ymME3bQ4J/k1IKxCtz/bAlAqFgKoc+EolMziDYqWIATtW0rYTJvzGAzTmMj80/QpsFH+Pc2M= 5ymME3bQ4J/k1IKxCtz/bAlAqFgKoc+EolMziDYqWIATtW0rYTJvzGAzTmMj80/QpsFH+Pc2M=
""" """
keyblob = b"""\ keyblob = """\
AAAAB3NzaC1yc2EAAAABIwAAAIEA8bP1ZA7DCZDB9J0s50l31MBGQ3GQ/Fc7SX6gkpXkwcZryoi4k\ AAAAB3NzaC1yc2EAAAABIwAAAIEA8bP1ZA7DCZDB9J0s50l31MBGQ3GQ/Fc7SX6gkpXkwcZryoi4k\
NFhHu5LvHcZPdxXV1D+uTMfGS1eyd2Yz/DoNWXNAl8TI0cAsW5ymME3bQ4J/k1IKxCtz/bAlAqFgK\ NFhHu5LvHcZPdxXV1D+uTMfGS1eyd2Yz/DoNWXNAl8TI0cAsW5ymME3bQ4J/k1IKxCtz/bAlAqFgK\
oc+EolMziDYqWIATtW0rYTJvzGAzTmMj80/QpsFH+Pc2M=""" oc+EolMziDYqWIATtW0rYTJvzGAzTmMj80/QpsFH+Pc2M="""
keyblob_dss = b"""\ keyblob_dss = """\
AAAAB3NzaC1kc3MAAACBAOeBpgNnfRzr/twmAQRu2XwWAp3CFtrVnug6s6fgwj/oLjYbVtjAy6pl/\ AAAAB3NzaC1kc3MAAACBAOeBpgNnfRzr/twmAQRu2XwWAp3CFtrVnug6s6fgwj/oLjYbVtjAy6pl/\
h0EKCWx2rf1IetyNsTxWrniA9I6HeDj65X1FyDkg6g8tvCnaNB8Xp/UUhuzHuGsMIipRxBxw9LF60\ h0EKCWx2rf1IetyNsTxWrniA9I6HeDj65X1FyDkg6g8tvCnaNB8Xp/UUhuzHuGsMIipRxBxw9LF60\
8EqZcj1E3ytktoW5B5OcjrkEoz3xG7C+rpIjYvAAAAFQDwz4UnmsGiSNu5iqjn3uTzwUpshwAAAIE\ 8EqZcj1E3ytktoW5B5OcjrkEoz3xG7C+rpIjYvAAAAFQDwz4UnmsGiSNu5iqjn3uTzwUpshwAAAIE\
@ -55,50 +55,51 @@ Ngw3qIch/WgRmMHy4kBq1SsXMjQCte1So6HBMvBPIW5SiMTmjCfZZiw4AYHK+B/JaOwaG9yRg2Ejg\
class HostKeysTest (unittest.TestCase): class HostKeysTest (unittest.TestCase):
def setUp(self): def setUp(self):
with open('hostfile.temp', 'w') as f: f = open('hostfile.temp', 'w')
f.write(test_hosts_file) f.write(test_hosts_file)
f.close()
def tearDown(self): def tearDown(self):
os.unlink('hostfile.temp') os.unlink('hostfile.temp')
def test_1_load(self): def test_1_load(self):
hostdict = paramiko.HostKeys('hostfile.temp') hostdict = paramiko.HostKeys('hostfile.temp')
self.assertEqual(2, len(hostdict)) self.assertEquals(2, len(hostdict))
self.assertEqual(1, len(list(hostdict.values())[0])) self.assertEquals(1, len(hostdict.values()[0]))
self.assertEqual(1, len(list(hostdict.values())[1])) self.assertEquals(1, len(hostdict.values()[1]))
fp = hexlify(hostdict['secure.example.com']['ssh-rsa'].get_fingerprint()).upper() fp = hexlify(hostdict['secure.example.com']['ssh-rsa'].get_fingerprint()).upper()
self.assertEqual(b'E6684DB30E109B67B70FF1DC5C7F1363', fp) self.assertEquals('E6684DB30E109B67B70FF1DC5C7F1363', fp)
def test_2_add(self): def test_2_add(self):
hostdict = paramiko.HostKeys('hostfile.temp') hostdict = paramiko.HostKeys('hostfile.temp')
hh = '|1|BMsIC6cUIP2zBuXR3t2LRcJYjzM=|hpkJMysjTk/+zzUUzxQEa2ieq6c=' hh = '|1|BMsIC6cUIP2zBuXR3t2LRcJYjzM=|hpkJMysjTk/+zzUUzxQEa2ieq6c='
key = paramiko.RSAKey(data=decodebytes(keyblob)) key = paramiko.RSAKey(data=base64.decodestring(keyblob))
hostdict.add(hh, 'ssh-rsa', key) hostdict.add(hh, 'ssh-rsa', key)
self.assertEqual(3, len(list(hostdict))) self.assertEquals(3, len(hostdict))
x = hostdict['foo.example.com'] x = hostdict['foo.example.com']
fp = hexlify(x['ssh-rsa'].get_fingerprint()).upper() fp = hexlify(x['ssh-rsa'].get_fingerprint()).upper()
self.assertEqual(b'7EC91BB336CB6D810B124B1353C32396', fp) self.assertEquals('7EC91BB336CB6D810B124B1353C32396', fp)
self.assertTrue(hostdict.check('foo.example.com', key)) self.assert_(hostdict.check('foo.example.com', key))
def test_3_dict(self): def test_3_dict(self):
hostdict = paramiko.HostKeys('hostfile.temp') hostdict = paramiko.HostKeys('hostfile.temp')
self.assertTrue('secure.example.com' in hostdict) self.assert_('secure.example.com' in hostdict)
self.assertTrue('not.example.com' not in hostdict) self.assert_('not.example.com' not in hostdict)
self.assertTrue('secure.example.com' in hostdict) self.assert_(hostdict.has_key('secure.example.com'))
self.assertTrue('not.example.com' not in hostdict) self.assert_(not hostdict.has_key('not.example.com'))
x = hostdict.get('secure.example.com', None) x = hostdict.get('secure.example.com', None)
self.assertTrue(x is not None) self.assert_(x is not None)
fp = hexlify(x['ssh-rsa'].get_fingerprint()).upper() fp = hexlify(x['ssh-rsa'].get_fingerprint()).upper()
self.assertEqual(b'E6684DB30E109B67B70FF1DC5C7F1363', fp) self.assertEquals('E6684DB30E109B67B70FF1DC5C7F1363', fp)
i = 0 i = 0
for key in hostdict: for key in hostdict:
i += 1 i += 1
self.assertEqual(2, i) self.assertEquals(2, i)
def test_4_dict_set(self): def test_4_dict_set(self):
hostdict = paramiko.HostKeys('hostfile.temp') hostdict = paramiko.HostKeys('hostfile.temp')
key = paramiko.RSAKey(data=decodebytes(keyblob)) key = paramiko.RSAKey(data=base64.decodestring(keyblob))
key_dss = paramiko.DSSKey(data=decodebytes(keyblob_dss)) key_dss = paramiko.DSSKey(data=base64.decodestring(keyblob_dss))
hostdict['secure.example.com'] = { hostdict['secure.example.com'] = {
'ssh-rsa': key, 'ssh-rsa': key,
'ssh-dss': key_dss 'ssh-dss': key_dss
@ -106,11 +107,11 @@ class HostKeysTest (unittest.TestCase):
hostdict['fake.example.com'] = {} hostdict['fake.example.com'] = {}
hostdict['fake.example.com']['ssh-rsa'] = key hostdict['fake.example.com']['ssh-rsa'] = key
self.assertEqual(3, len(hostdict)) self.assertEquals(3, len(hostdict))
self.assertEqual(2, len(list(hostdict.values())[0])) self.assertEquals(2, len(hostdict.values()[0]))
self.assertEqual(1, len(list(hostdict.values())[1])) self.assertEquals(1, len(hostdict.values()[1]))
self.assertEqual(1, len(list(hostdict.values())[2])) self.assertEquals(1, len(hostdict.values()[2]))
fp = hexlify(hostdict['secure.example.com']['ssh-rsa'].get_fingerprint()).upper() fp = hexlify(hostdict['secure.example.com']['ssh-rsa'].get_fingerprint()).upper()
self.assertEqual(b'7EC91BB336CB6D810B124B1353C32396', fp) self.assertEquals('7EC91BB336CB6D810B124B1353C32396', fp)
fp = hexlify(hostdict['secure.example.com']['ssh-dss'].get_fingerprint()).upper() fp = hexlify(hostdict['secure.example.com']['ssh-dss'].get_fingerprint()).upper()
self.assertEqual(b'4478F0B9A23CC5182009FF755BC1D26C', fp) self.assertEquals('4478F0B9A23CC5182009FF755BC1D26C', fp)

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -21,40 +21,38 @@ Some unit tests for the key exchange protocols.
""" """
from binascii import hexlify from binascii import hexlify
import os
import unittest import unittest
import paramiko.util import paramiko.util
from paramiko.kex_group1 import KexGroup1 from paramiko.kex_group1 import KexGroup1
from paramiko.kex_gex import KexGex from paramiko.kex_gex import KexGex
from paramiko import Message from paramiko import Message
from paramiko.common import byte_chr
import six
if six.PY3:
long = lambda x: int(x)
def dummy_urandom(n): class FakeRng (object):
return byte_chr(0xcc) * n def read(self, n):
return chr(0xcc) * n
class FakeKey (object): class FakeKey (object):
def __str__(self): def __str__(self):
return 'fake-key' return 'fake-key'
def sign_ssh_data(self, rng, H):
def asbytes(self): return 'fake-sig'
return b'fake-key'
def sign_ssh_data(self, H):
return b'fake-sig'
class FakeModulusPack (object): class FakeModulusPack (object):
P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF P = long(0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF)
G = 2 G = 2
def get_modulus(self, min, ask, max): def get_modulus(self, min, ask, max):
return self.G, self.P return self.G, self.P
class FakeTransport(object): class FakeTransport (object):
rng = FakeRng()
local_version = 'SSH-2.0-paramiko_1.0' local_version = 'SSH-2.0-paramiko_1.0'
remote_version = 'SSH-2.0-lame' remote_version = 'SSH-2.0-lame'
local_kex_init = 'local-kex-init' local_kex_init = 'local-kex-init'
@ -62,49 +60,41 @@ class FakeTransport(object):
def _send_message(self, m): def _send_message(self, m):
self._message = m self._message = m
def _expect_packet(self, *t): def _expect_packet(self, *t):
self._expect = t self._expect = t
def _set_K_H(self, K, H): def _set_K_H(self, K, H):
self._K = K self._K = K
self._H = H self._H = H
def _verify_key(self, host_key, sig): def _verify_key(self, host_key, sig):
self._verify = (host_key, sig) self._verify = (host_key, sig)
def _activate_outbound(self): def _activate_outbound(self):
self._activated = True self._activated = True
def _log(self, level, s): def _log(self, level, s):
pass pass
def get_server_key(self): def get_server_key(self):
return FakeKey() return FakeKey()
def _get_modulus_pack(self): def _get_modulus_pack(self):
return FakeModulusPack() return FakeModulusPack()
class KexTest (unittest.TestCase): class KexTest (unittest.TestCase):
K = 14730343317708716439807310032871972459448364195094179797249681733965528989482751523943515690110179031004049109375612685505881911274101441415545039654102474376472240501616988799699744135291070488314748284283496055223852115360852283821334858541043710301057312858051901453919067023103730011648890038847384890504 K = long(14730343317708716439807310032871972459448364195094179797249681733965528989482751523943515690110179031004049109375612685505881911274101441415545039654102474376472240501616988799699744135291070488314748284283496055223852115360852283821334858541043710301057312858051901453919067023103730011648890038847384890504)
def setUp(self): def setUp(self):
self._original_urandom = os.urandom pass
os.urandom = dummy_urandom
def tearDown(self): def tearDown(self):
os.urandom = self._original_urandom pass
def test_1_group1_client(self): def test_1_group1_client(self):
transport = FakeTransport() transport = FakeTransport()
transport.server_mode = False transport.server_mode = False
kex = KexGroup1(transport) kex = KexGroup1(transport)
kex.start_kex() kex.start_kex()
x = b'1E000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D4' x = '1E000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D4'
self.assertEqual(x, hexlify(transport._message.asbytes()).upper()) self.assertEquals(x, hexlify(str(transport._message)).upper())
self.assertEqual((paramiko.kex_group1._MSG_KEXDH_REPLY,), transport._expect) self.assertEquals((paramiko.kex_group1._MSG_KEXDH_REPLY,), transport._expect)
# fake "reply" # fake "reply"
msg = Message() msg = Message()
@ -113,47 +103,47 @@ class KexTest (unittest.TestCase):
msg.add_string('fake-sig') msg.add_string('fake-sig')
msg.rewind() msg.rewind()
kex.parse_next(paramiko.kex_group1._MSG_KEXDH_REPLY, msg) kex.parse_next(paramiko.kex_group1._MSG_KEXDH_REPLY, msg)
H = b'03079780F3D3AD0B3C6DB30C8D21685F367A86D2' H = '03079780F3D3AD0B3C6DB30C8D21685F367A86D2'
self.assertEqual(self.K, transport._K) self.assertEquals(self.K, transport._K)
self.assertEqual(H, hexlify(transport._H).upper()) self.assertEquals(H, hexlify(transport._H).upper())
self.assertEqual((b'fake-host-key', b'fake-sig'), transport._verify) self.assertEquals(('fake-host-key', 'fake-sig'), transport._verify)
self.assertTrue(transport._activated) self.assert_(transport._activated)
def test_2_group1_server(self): def test_2_group1_server(self):
transport = FakeTransport() transport = FakeTransport()
transport.server_mode = True transport.server_mode = True
kex = KexGroup1(transport) kex = KexGroup1(transport)
kex.start_kex() kex.start_kex()
self.assertEqual((paramiko.kex_group1._MSG_KEXDH_INIT,), transport._expect) self.assertEquals((paramiko.kex_group1._MSG_KEXDH_INIT,), transport._expect)
msg = Message() msg = Message()
msg.add_mpint(69) msg.add_mpint(69)
msg.rewind() msg.rewind()
kex.parse_next(paramiko.kex_group1._MSG_KEXDH_INIT, msg) kex.parse_next(paramiko.kex_group1._MSG_KEXDH_INIT, msg)
H = b'B16BF34DD10945EDE84E9C1EF24A14BFDC843389' H = 'B16BF34DD10945EDE84E9C1EF24A14BFDC843389'
x = b'1F0000000866616B652D6B6579000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D40000000866616B652D736967' x = '1F0000000866616B652D6B6579000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D40000000866616B652D736967'
self.assertEqual(self.K, transport._K) self.assertEquals(self.K, transport._K)
self.assertEqual(H, hexlify(transport._H).upper()) self.assertEquals(H, hexlify(transport._H).upper())
self.assertEqual(x, hexlify(transport._message.asbytes()).upper()) self.assertEquals(x, hexlify(str(transport._message)).upper())
self.assertTrue(transport._activated) self.assert_(transport._activated)
def test_3_gex_client(self): def test_3_gex_client(self):
transport = FakeTransport() transport = FakeTransport()
transport.server_mode = False transport.server_mode = False
kex = KexGex(transport) kex = KexGex(transport)
kex.start_kex() kex.start_kex()
x = b'22000004000000080000002000' x = '22000004000000080000002000'
self.assertEqual(x, hexlify(transport._message.asbytes()).upper()) self.assertEquals(x, hexlify(str(transport._message)).upper())
self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_GROUP,), transport._expect) self.assertEquals((paramiko.kex_gex._MSG_KEXDH_GEX_GROUP,), transport._expect)
msg = Message() msg = Message()
msg.add_mpint(FakeModulusPack.P) msg.add_mpint(FakeModulusPack.P)
msg.add_mpint(FakeModulusPack.G) msg.add_mpint(FakeModulusPack.G)
msg.rewind() msg.rewind()
kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_GROUP, msg) kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_GROUP, msg)
x = b'20000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D4' x = '20000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D4'
self.assertEqual(x, hexlify(transport._message.asbytes()).upper()) self.assertEquals(x, hexlify(str(transport._message)).upper())
self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_REPLY,), transport._expect) self.assertEquals((paramiko.kex_gex._MSG_KEXDH_GEX_REPLY,), transport._expect)
msg = Message() msg = Message()
msg.add_string('fake-host-key') msg.add_string('fake-host-key')
@ -161,29 +151,29 @@ class KexTest (unittest.TestCase):
msg.add_string('fake-sig') msg.add_string('fake-sig')
msg.rewind() msg.rewind()
kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_REPLY, msg) kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_REPLY, msg)
H = b'A265563F2FA87F1A89BF007EE90D58BE2E4A4BD0' H = 'A265563F2FA87F1A89BF007EE90D58BE2E4A4BD0'
self.assertEqual(self.K, transport._K) self.assertEquals(self.K, transport._K)
self.assertEqual(H, hexlify(transport._H).upper()) self.assertEquals(H, hexlify(transport._H).upper())
self.assertEqual((b'fake-host-key', b'fake-sig'), transport._verify) self.assertEquals(('fake-host-key', 'fake-sig'), transport._verify)
self.assertTrue(transport._activated) self.assert_(transport._activated)
def test_4_gex_old_client(self): def test_4_gex_old_client(self):
transport = FakeTransport() transport = FakeTransport()
transport.server_mode = False transport.server_mode = False
kex = KexGex(transport) kex = KexGex(transport)
kex.start_kex(_test_old_style=True) kex.start_kex(_test_old_style=True)
x = b'1E00000800' x = '1E00000800'
self.assertEqual(x, hexlify(transport._message.asbytes()).upper()) self.assertEquals(x, hexlify(str(transport._message)).upper())
self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_GROUP,), transport._expect) self.assertEquals((paramiko.kex_gex._MSG_KEXDH_GEX_GROUP,), transport._expect)
msg = Message() msg = Message()
msg.add_mpint(FakeModulusPack.P) msg.add_mpint(FakeModulusPack.P)
msg.add_mpint(FakeModulusPack.G) msg.add_mpint(FakeModulusPack.G)
msg.rewind() msg.rewind()
kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_GROUP, msg) kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_GROUP, msg)
x = b'20000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D4' x = '20000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D4'
self.assertEqual(x, hexlify(transport._message.asbytes()).upper()) self.assertEquals(x, hexlify(str(transport._message)).upper())
self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_REPLY,), transport._expect) self.assertEquals((paramiko.kex_gex._MSG_KEXDH_GEX_REPLY,), transport._expect)
msg = Message() msg = Message()
msg.add_string('fake-host-key') msg.add_string('fake-host-key')
@ -191,18 +181,18 @@ class KexTest (unittest.TestCase):
msg.add_string('fake-sig') msg.add_string('fake-sig')
msg.rewind() msg.rewind()
kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_REPLY, msg) kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_REPLY, msg)
H = b'807F87B269EF7AC5EC7E75676808776A27D5864C' H = '807F87B269EF7AC5EC7E75676808776A27D5864C'
self.assertEqual(self.K, transport._K) self.assertEquals(self.K, transport._K)
self.assertEqual(H, hexlify(transport._H).upper()) self.assertEquals(H, hexlify(transport._H).upper())
self.assertEqual((b'fake-host-key', b'fake-sig'), transport._verify) self.assertEquals(('fake-host-key', 'fake-sig'), transport._verify)
self.assertTrue(transport._activated) self.assert_(transport._activated)
def test_5_gex_server(self): def test_5_gex_server(self):
transport = FakeTransport() transport = FakeTransport()
transport.server_mode = True transport.server_mode = True
kex = KexGex(transport) kex = KexGex(transport)
kex.start_kex() kex.start_kex()
self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST, paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST_OLD), transport._expect) self.assertEquals((paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST, paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST_OLD), transport._expect)
msg = Message() msg = Message()
msg.add_int(1024) msg.add_int(1024)
@ -210,45 +200,45 @@ class KexTest (unittest.TestCase):
msg.add_int(4096) msg.add_int(4096)
msg.rewind() msg.rewind()
kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST, msg) kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST, msg)
x = b'1F0000008100FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF0000000102' x = '1F0000008100FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF0000000102'
self.assertEqual(x, hexlify(transport._message.asbytes()).upper()) self.assertEquals(x, hexlify(str(transport._message)).upper())
self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_INIT,), transport._expect) self.assertEquals((paramiko.kex_gex._MSG_KEXDH_GEX_INIT,), transport._expect)
msg = Message() msg = Message()
msg.add_mpint(12345) msg.add_mpint(12345)
msg.rewind() msg.rewind()
kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_INIT, msg) kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_INIT, msg)
K = 67592995013596137876033460028393339951879041140378510871612128162185209509220726296697886624612526735888348020498716482757677848959420073720160491114319163078862905400020959196386947926388406687288901564192071077389283980347784184487280885335302632305026248574716290537036069329724382811853044654824945750581 K = long(67592995013596137876033460028393339951879041140378510871612128162185209509220726296697886624612526735888348020498716482757677848959420073720160491114319163078862905400020959196386947926388406687288901564192071077389283980347784184487280885335302632305026248574716290537036069329724382811853044654824945750581)
H = b'CE754197C21BF3452863B4F44D0B3951F12516EF' H = 'CE754197C21BF3452863B4F44D0B3951F12516EF'
x = b'210000000866616B652D6B6579000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D40000000866616B652D736967' x = '210000000866616B652D6B6579000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D40000000866616B652D736967'
self.assertEqual(K, transport._K) self.assertEquals(K, transport._K)
self.assertEqual(H, hexlify(transport._H).upper()) self.assertEquals(H, hexlify(transport._H).upper())
self.assertEqual(x, hexlify(transport._message.asbytes()).upper()) self.assertEquals(x, hexlify(str(transport._message)).upper())
self.assertTrue(transport._activated) self.assert_(transport._activated)
def test_6_gex_server_with_old_client(self): def test_6_gex_server_with_old_client(self):
transport = FakeTransport() transport = FakeTransport()
transport.server_mode = True transport.server_mode = True
kex = KexGex(transport) kex = KexGex(transport)
kex.start_kex() kex.start_kex()
self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST, paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST_OLD), transport._expect) self.assertEquals((paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST, paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST_OLD), transport._expect)
msg = Message() msg = Message()
msg.add_int(2048) msg.add_int(2048)
msg.rewind() msg.rewind()
kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST_OLD, msg) kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST_OLD, msg)
x = b'1F0000008100FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF0000000102' x = '1F0000008100FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF0000000102'
self.assertEqual(x, hexlify(transport._message.asbytes()).upper()) self.assertEquals(x, hexlify(str(transport._message)).upper())
self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_INIT,), transport._expect) self.assertEquals((paramiko.kex_gex._MSG_KEXDH_GEX_INIT,), transport._expect)
msg = Message() msg = Message()
msg.add_mpint(12345) msg.add_mpint(12345)
msg.rewind() msg.rewind()
kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_INIT, msg) kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_INIT, msg)
K = 67592995013596137876033460028393339951879041140378510871612128162185209509220726296697886624612526735888348020498716482757677848959420073720160491114319163078862905400020959196386947926388406687288901564192071077389283980347784184487280885335302632305026248574716290537036069329724382811853044654824945750581 K = long(67592995013596137876033460028393339951879041140378510871612128162185209509220726296697886624612526735888348020498716482757677848959420073720160491114319163078862905400020959196386947926388406687288901564192071077389283980347784184487280885335302632305026248574716290537036069329724382811853044654824945750581)
H = b'B41A06B2E59043CEFC1AE16EC31F1E2D12EC455B' H = 'B41A06B2E59043CEFC1AE16EC31F1E2D12EC455B'
x = b'210000000866616B652D6B6579000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D40000000866616B652D736967' x = '210000000866616B652D6B6579000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D40000000866616B652D736967'
self.assertEqual(K, transport._K) self.assertEquals(K, transport._K)
self.assertEqual(H, hexlify(transport._H).upper()) self.assertEquals(H, hexlify(transport._H).upper())
self.assertEqual(x, hexlify(transport._message.asbytes()).upper()) self.assertEquals(x, hexlify(str(transport._message)).upper())
self.assertTrue(transport._activated) self.assert_(transport._activated)

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -20,17 +20,20 @@
Some unit tests for ssh protocol message blocks. Some unit tests for ssh protocol message blocks.
""" """
import six
if six.PY3:
long = lambda x: int(x)
import unittest import unittest
from paramiko.message import Message from paramiko.message import Message
from paramiko.common import byte_chr, zero_byte
class MessageTest (unittest.TestCase): class MessageTest (unittest.TestCase):
__a = b'\x00\x00\x00\x17\x07\x60\xe0\x90\x00\x00\x00\x01\x71\x00\x00\x00\x05\x68\x65\x6c\x6c\x6f\x00\x00\x03\xe8' + b'x' * 1000 __a = '\x00\x00\x00\x17\x07\x60\xe0\x90\x00\x00\x00\x01q\x00\x00\x00\x05hello\x00\x00\x03\xe8' + ('x' * 1000)
__b = b'\x01\x00\xf3\x00\x3f\x00\x00\x00\x10\x68\x75\x65\x79\x2c\x64\x65\x77\x65\x79\x2c\x6c\x6f\x75\x69\x65' __b = '\x01\x00\xf3\x00\x3f\x00\x00\x00\x10huey,dewey,louie'
__c = b'\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\xf5\xe4\xd3\xc2\xb1\x09\x00\x00\x00\x01\x11\x00\x00\x00\x07\x00\xf5\xe4\xd3\xc2\xb1\x09\x00\x00\x00\x06\x9a\x1b\x2c\x3d\x4e\xf7' __c = '\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\xf5\xe4\xd3\xc2\xb1\x09\x00\x00\x00\x01\x11\x00\x00\x00\x07\x00\xf5\xe4\xd3\xc2\xb1\x09\x00\x00\x00\x06\x9a\x1b\x2c\x3d\x4e\xf7'
__d = b'\x00\x00\x00\x05\xff\x00\x00\x00\x05\x11\x22\x33\x44\x55\xff\x00\x00\x00\x0a\x00\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x63\x61\x74\x00\x00\x00\x03\x61\x2c\x62' __d = '\x00\x00\x00\x05\x00\x00\x00\x05\x11\x22\x33\x44\x55\x01\x00\x00\x00\x03cat\x00\x00\x00\x03a,b'
def test_1_encode(self): def test_1_encode(self):
msg = Message() msg = Message()
@ -39,65 +42,63 @@ class MessageTest (unittest.TestCase):
msg.add_string('q') msg.add_string('q')
msg.add_string('hello') msg.add_string('hello')
msg.add_string('x' * 1000) msg.add_string('x' * 1000)
self.assertEqual(msg.asbytes(), self.__a) self.assertEquals(str(msg), self.__a)
msg = Message() msg = Message()
msg.add_boolean(True) msg.add_boolean(True)
msg.add_boolean(False) msg.add_boolean(False)
msg.add_byte(byte_chr(0xf3)) msg.add_byte('\xf3')
msg.add_bytes('\x00\x3f')
msg.add_bytes(zero_byte + byte_chr(0x3f))
msg.add_list(['huey', 'dewey', 'louie']) msg.add_list(['huey', 'dewey', 'louie'])
self.assertEqual(msg.asbytes(), self.__b) self.assertEquals(str(msg), self.__b)
msg = Message() msg = Message()
msg.add_int64(5) msg.add_int64(5)
msg.add_int64(0xf5e4d3c2b109) msg.add_int64(long(0xf5e4d3c2b109))
msg.add_mpint(17) msg.add_mpint(17)
msg.add_mpint(0xf5e4d3c2b109) msg.add_mpint(long(0xf5e4d3c2b109))
msg.add_mpint(-0x65e4d3c2b109) msg.add_mpint(long(-0x65e4d3c2b109))
self.assertEqual(msg.asbytes(), self.__c) self.assertEquals(str(msg), self.__c)
def test_2_decode(self): def test_2_decode(self):
msg = Message(self.__a) msg = Message(self.__a)
self.assertEqual(msg.get_int(), 23) self.assertEquals(msg.get_int(), 23)
self.assertEqual(msg.get_int(), 123789456) self.assertEquals(msg.get_int(), 123789456)
self.assertEqual(msg.get_text(), 'q') self.assertEquals(msg.get_string(), 'q')
self.assertEqual(msg.get_text(), 'hello') self.assertEquals(msg.get_string(), 'hello')
self.assertEqual(msg.get_text(), 'x' * 1000) self.assertEquals(msg.get_string(), 'x' * 1000)
msg = Message(self.__b) msg = Message(self.__b)
self.assertEqual(msg.get_boolean(), True) self.assertEquals(msg.get_boolean(), True)
self.assertEqual(msg.get_boolean(), False) self.assertEquals(msg.get_boolean(), False)
self.assertEqual(msg.get_byte(), byte_chr(0xf3)) self.assertEquals(msg.get_byte(), '\xf3')
self.assertEqual(msg.get_bytes(2), zero_byte + byte_chr(0x3f)) self.assertEquals(msg.get_bytes(2), '\x00\x3f')
self.assertEqual(msg.get_list(), ['huey', 'dewey', 'louie']) self.assertEquals(msg.get_list(), ['huey', 'dewey', 'louie'])
msg = Message(self.__c) msg = Message(self.__c)
self.assertEqual(msg.get_int64(), 5) self.assertEquals(msg.get_int64(), 5)
self.assertEqual(msg.get_int64(), 0xf5e4d3c2b109) self.assertEquals(msg.get_int64(), long(0xf5e4d3c2b109))
self.assertEqual(msg.get_mpint(), 17) self.assertEquals(msg.get_mpint(), 17)
self.assertEqual(msg.get_mpint(), 0xf5e4d3c2b109) self.assertEquals(msg.get_mpint(), long(0xf5e4d3c2b109))
self.assertEqual(msg.get_mpint(), -0x65e4d3c2b109) self.assertEquals(msg.get_mpint(), long(-0x65e4d3c2b109))
def test_3_add(self): def test_3_add(self):
msg = Message() msg = Message()
msg.add(5) msg.add(5)
msg.add(0x1122334455) msg.add(long(0x1122334455))
msg.add(0xf00000000000000000)
msg.add(True) msg.add(True)
msg.add('cat') msg.add('cat')
msg.add(['a', 'b']) msg.add(['a', 'b'])
self.assertEqual(msg.asbytes(), self.__d) self.assertEquals(str(msg), self.__d)
def test_4_misc(self): def test_4_misc(self):
msg = Message(self.__d) msg = Message(self.__d)
self.assertEqual(msg.get_int(), 5) self.assertEquals(msg.get_int(), 5)
self.assertEqual(msg.get_int(), 0x1122334455) self.assertEquals(msg.get_mpint(), long(0x1122334455))
self.assertEqual(msg.get_int(), 0xf00000000000000000) self.assertEquals(msg.get_so_far(), self.__d[:13])
self.assertEqual(msg.get_so_far(), self.__d[:29]) self.assertEquals(msg.get_remainder(), self.__d[13:])
self.assertEqual(msg.get_remainder(), self.__d[29:])
msg.rewind() msg.rewind()
self.assertEqual(msg.get_int(), 5) self.assertEquals(msg.get_int(), 5)
self.assertEqual(msg.get_so_far(), self.__d[:4]) self.assertEquals(msg.get_so_far(), self.__d[:4])
self.assertEqual(msg.get_remainder(), self.__d[4:]) self.assertEquals(msg.get_remainder(), self.__d[4:])

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -21,56 +21,50 @@ Some unit tests for the ssh2 protocol in Transport.
""" """
import unittest import unittest
from hashlib import sha1 from loop import LoopSocket
from tests.loop import LoopSocket
from Crypto.Cipher import AES from Crypto.Cipher import AES
from Crypto.Hash import SHA, HMAC
from paramiko import Message, Packetizer, util from paramiko import Message, Packetizer, util
from paramiko.common import byte_chr, zero_byte
x55 = byte_chr(0x55)
x1f = byte_chr(0x1f)
class PacketizerTest (unittest.TestCase): class PacketizerTest (unittest.TestCase):
def test_1_write(self): def test_1_write (self):
rsock = LoopSocket() rsock = LoopSocket()
wsock = LoopSocket() wsock = LoopSocket()
rsock.link(wsock) rsock.link(wsock)
p = Packetizer(wsock) p = Packetizer(wsock)
p.set_log(util.get_logger('paramiko.transport')) p.set_log(util.get_logger('paramiko.transport'))
p.set_hexdump(True) p.set_hexdump(True)
cipher = AES.new(zero_byte * 16, AES.MODE_CBC, x55 * 16) cipher = AES.new('\x00' * 16, AES.MODE_CBC, '\x55' * 16)
p.set_outbound_cipher(cipher, 16, sha1, 12, x1f * 20) p.set_outbound_cipher(cipher, 16, SHA, 12, '\x1f' * 20)
# message has to be at least 16 bytes long, so we'll have at least one # 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 # block of data encrypted that contains zero random padding bytes
m = Message() m = Message()
m.add_byte(byte_chr(100)) m.add_byte(chr(100))
m.add_int(100) m.add_int(100)
m.add_int(1) m.add_int(1)
m.add_int(900) m.add_int(900)
p.send_message(m) p.send_message(m)
data = rsock.recv(100) data = rsock.recv(100)
# 32 + 12 bytes of MAC = 44 # 32 + 12 bytes of MAC = 44
self.assertEqual(44, len(data)) self.assertEquals(44, len(data))
self.assertEqual(b'\x43\x91\x97\xbd\x5b\x50\xac\x25\x87\xc2\xc4\x6b\xc7\xe9\x38\xc0', data[:16]) self.assertEquals('\x43\x91\x97\xbd\x5b\x50\xac\x25\x87\xc2\xc4\x6b\xc7\xe9\x38\xc0', data[:16])
def test_2_read(self): def test_2_read (self):
rsock = LoopSocket() rsock = LoopSocket()
wsock = LoopSocket() wsock = LoopSocket()
rsock.link(wsock) rsock.link(wsock)
p = Packetizer(rsock) p = Packetizer(rsock)
p.set_log(util.get_logger('paramiko.transport')) p.set_log(util.get_logger('paramiko.transport'))
p.set_hexdump(True) p.set_hexdump(True)
cipher = AES.new(zero_byte * 16, AES.MODE_CBC, x55 * 16) cipher = AES.new('\x00' * 16, AES.MODE_CBC, '\x55' * 16)
p.set_inbound_cipher(cipher, 16, sha1, 12, x1f * 20) p.set_inbound_cipher(cipher, 16, SHA, 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')
wsock.send('C\x91\x97\xbd[P\xac%\x87\xc2\xc4k\xc7\xe98\xc0' + \
'\x90\xd2\x16V\rqsa8|L=\xfb\x97}\xe2n\x03\xb1\xa0\xc2\x1c\xd6AAL\xb4Y')
cmd, m = p.read_message() cmd, m = p.read_message()
self.assertEqual(100, cmd) self.assertEquals(100, cmd)
self.assertEqual(100, m.get_int()) self.assertEquals(100, m.get_int())
self.assertEqual(1, m.get_int()) self.assertEquals(1, m.get_int())
self.assertEqual(900, m.get_int()) self.assertEquals(900, m.get_int())

View File

@ -7,7 +7,7 @@
# Software Foundation; either version 2.1 of the License, or (at your option) # Software Foundation; either version 2.1 of the License, or (at your option)
# any later version. # any later version.
# #
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details. # details.
@ -20,23 +20,20 @@
Some unit tests for public/private key objects. Some unit tests for public/private key objects.
""" """
from binascii import hexlify, unhexlify
try:
from StringIO import StringIO
except ImportError:
from io import StringIO
import unittest import unittest
from binascii import hexlify from paramiko import RSAKey, DSSKey, Message, util
from hashlib import md5 from paramiko.common import rng
from paramiko import RSAKey, DSSKey, ECDSAKey, Message, util
from paramiko.py3compat import StringIO, byte_chr, b, bytes
from tests.util import test_path
# from openssh's ssh-keygen # from openssh's ssh-keygen
PUB_RSA = 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA049W6geFpmsljTwfvI1UmKWWJPNFI74+vNKTk4dmzkQY2yAMs6FhlvhlI8ysU4oj71ZsRYMecHbBbxdN79+JRFVYTKaLqjwGENeTd+yv4q+V2PvZv3fLnzApI3l7EJCqhWwJUHJ1jAkZzqDx0tyOL4uoZpww3nmE0kb3y21tH4c=' PUB_RSA = 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA049W6geFpmsljTwfvI1UmKWWJPNFI74+vNKTk4dmzkQY2yAMs6FhlvhlI8ysU4oj71ZsRYMecHbBbxdN79+JRFVYTKaLqjwGENeTd+yv4q+V2PvZv3fLnzApI3l7EJCqhWwJUHJ1jAkZzqDx0tyOL4uoZpww3nmE0kb3y21tH4c='
PUB_DSS = 'ssh-dss AAAAB3NzaC1kc3MAAACBAOeBpgNnfRzr/twmAQRu2XwWAp3CFtrVnug6s6fgwj/oLjYbVtjAy6pl/h0EKCWx2rf1IetyNsTxWrniA9I6HeDj65X1FyDkg6g8tvCnaNB8Xp/UUhuzHuGsMIipRxBxw9LF608EqZcj1E3ytktoW5B5OcjrkEoz3xG7C+rpIjYvAAAAFQDwz4UnmsGiSNu5iqjn3uTzwUpshwAAAIEAkxfFeY8P2wZpDjX0MimZl5wkoFQDL25cPzGBuB4OnB8NoUk/yjAHIIpEShw8V+LzouMK5CTJQo5+Ngw3qIch/WgRmMHy4kBq1SsXMjQCte1So6HBMvBPIW5SiMTmjCfZZiw4AYHK+B/JaOwaG9yRg2Ejg4Ok10+XFDxlqZo8Y+wAAACARmR7CCPjodxASvRbIyzaVpZoJ/Z6x7dAumV+ysrV1BVYd0lYukmnjO1kKBWApqpH1ve9XDQYN8zgxM4b16L21kpoWQnZtXrY3GZ4/it9kUgyB7+NwacIBlXa8cMDL7Q/69o0d54U0X/NeX5QxuYR6OMJlrkQB7oiW/P/1mwjQgE=' PUB_DSS = 'ssh-dss AAAAB3NzaC1kc3MAAACBAOeBpgNnfRzr/twmAQRu2XwWAp3CFtrVnug6s6fgwj/oLjYbVtjAy6pl/h0EKCWx2rf1IetyNsTxWrniA9I6HeDj65X1FyDkg6g8tvCnaNB8Xp/UUhuzHuGsMIipRxBxw9LF608EqZcj1E3ytktoW5B5OcjrkEoz3xG7C+rpIjYvAAAAFQDwz4UnmsGiSNu5iqjn3uTzwUpshwAAAIEAkxfFeY8P2wZpDjX0MimZl5wkoFQDL25cPzGBuB4OnB8NoUk/yjAHIIpEShw8V+LzouMK5CTJQo5+Ngw3qIch/WgRmMHy4kBq1SsXMjQCte1So6HBMvBPIW5SiMTmjCfZZiw4AYHK+B/JaOwaG9yRg2Ejg4Ok10+XFDxlqZo8Y+wAAACARmR7CCPjodxASvRbIyzaVpZoJ/Z6x7dAumV+ysrV1BVYd0lYukmnjO1kKBWApqpH1ve9XDQYN8zgxM4b16L21kpoWQnZtXrY3GZ4/it9kUgyB7+NwacIBlXa8cMDL7Q/69o0d54U0X/NeX5QxuYR6OMJlrkQB7oiW/P/1mwjQgE='
PUB_ECDSA = 'ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJSPZm3ZWkvk/Zx8WP+fZRZ5/NBBHnGQwR6uIC6XHGPDIHuWUzIjAwA0bzqkOUffEsbLe+uQgKl5kbc/L8KA/eo='
FINGER_RSA = '1024 60:73:38:44:cb:51:86:65:7f:de:da:a2:2b:5a:57:d5' FINGER_RSA = '1024 60:73:38:44:cb:51:86:65:7f:de:da:a2:2b:5a:57:d5'
FINGER_DSS = '1024 44:78:f0:b9:a2:3c:c5:18:20:09:ff:75:5b:c1:d2:6c' FINGER_DSS = '1024 44:78:f0:b9:a2:3c:c5:18:20:09:ff:75:5b:c1:d2:6c'
FINGER_ECDSA = '256 25:19:eb:55:e6:a1:47:ff:4f:38:d2:75:6f:a5:d5:60'
SIGNED_RSA = '20:d7:8a:31:21:cb:f7:92:12:f2:a4:89:37:f5:78:af:e6:16:b6:25:b9:97:3d:a2:cd:5f:ca:20:21:73:4c:ad:34:73:8f:20:77:28:e2:94:15:08:d8:91:40:7a:85:83:bf:18:37:95:dc:54:1a:9b:88:29:6c:73:ca:38:b4:04:f1:56:b9:f2:42:9d:52:1b:29:29:b4:4f:fd:c9:2d:af:47:d2:40:76:30:f3:63:45:0c:d9:1d:43:86:0f:1c:70:e2:93:12:34:f3:ac:c5:0a:2f:14:50:66:59:f1:88:ee:c1:4a:e9:d1:9c:4e:46:f0:0e:47:6f:38:74:f1:44:a8' SIGNED_RSA = '20:d7:8a:31:21:cb:f7:92:12:f2:a4:89:37:f5:78:af:e6:16:b6:25:b9:97:3d:a2:cd:5f:ca:20:21:73:4c:ad:34:73:8f:20:77:28:e2:94:15:08:d8:91:40:7a:85:83:bf:18:37:95:dc:54:1a:9b:88:29:6c:73:ca:38:b4:04:f1:56:b9:f2:42:9d:52:1b:29:29:b4:4f:fd:c9:2d:af:47:d2:40:76:30:f3:63:45:0c:d9:1d:43:86:0f:1c:70:e2:93:12:34:f3:ac:c5:0a:2f:14:50:66:59:f1:88:ee:c1:4a:e9:d1:9c:4e:46:f0:0e:47:6f:38:74:f1:44:a8'
RSA_PRIVATE_OUT = """\ RSA_PRIVATE_OUT = """\
@ -72,16 +69,6 @@ QPSch9pT9XHqn+1rZ4bK+QGA
-----END DSA PRIVATE KEY----- -----END DSA PRIVATE KEY-----
""" """
ECDSA_PRIVATE_OUT = """\
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIKB6ty3yVyKEnfF/zprx0qwC76MsMlHY4HXCnqho2eKioAoGCCqGSM49
AwEHoUQDQgAElI9mbdlaS+T9nHxY/59lFnn80EEecZDBHq4gLpccY8Mge5ZTMiMD
ADRvOqQ5R98Sxst765CAqXmRtz8vwoD96g==
-----END EC PRIVATE KEY-----
"""
x1234 = b'\x01\x02\x03\x04'
class KeyTest (unittest.TestCase): class KeyTest (unittest.TestCase):
@ -92,164 +79,115 @@ class KeyTest (unittest.TestCase):
pass pass
def test_1_generate_key_bytes(self): def test_1_generate_key_bytes(self):
key = util.generate_key_bytes(md5, x1234, 'happy birthday', 30) from Crypto.Hash import MD5
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' key = util.generate_key_bytes(MD5, '\x01\x02\x03\x04', 'happy birthday', 30)
self.assertEqual(exp, key) exp = unhexlify('61E1F272F4C1C4561586BD322498C0E924672780F47BB37DDA7D54019E64')
self.assertEquals(exp, key)
def test_2_load_rsa(self): def test_2_load_rsa(self):
key = RSAKey.from_private_key_file(test_path('test_rsa.key')) key = RSAKey.from_private_key_file('tests/test_rsa.key')
self.assertEqual('ssh-rsa', key.get_name()) self.assertEquals('ssh-rsa', key.get_name())
exp_rsa = b(FINGER_RSA.split()[1].replace(':', '')) exp_rsa = FINGER_RSA.split()[1].replace(':', '')
my_rsa = hexlify(key.get_fingerprint()) my_rsa = hexlify(key.get_fingerprint())
self.assertEqual(exp_rsa, my_rsa) self.assertEquals(exp_rsa, my_rsa)
self.assertEqual(PUB_RSA.split()[1], key.get_base64()) self.assertEquals(PUB_RSA.split()[1], key.get_base64())
self.assertEqual(1024, key.get_bits()) self.assertEquals(1024, key.get_bits())
s = StringIO() s = StringIO()
key.write_private_key(s) key.write_private_key(s)
self.assertEqual(RSA_PRIVATE_OUT, s.getvalue()) self.assertEquals(RSA_PRIVATE_OUT, s.getvalue())
s.seek(0) s.seek(0)
key2 = RSAKey.from_private_key(s) key2 = RSAKey.from_private_key(s)
self.assertEqual(key, key2) self.assertEquals(key, key2)
def test_3_load_rsa_password(self): def test_3_load_rsa_password(self):
key = RSAKey.from_private_key_file(test_path('test_rsa_password.key'), 'television') key = RSAKey.from_private_key_file('tests/test_rsa_password.key', 'television')
self.assertEqual('ssh-rsa', key.get_name()) self.assertEquals('ssh-rsa', key.get_name())
exp_rsa = b(FINGER_RSA.split()[1].replace(':', '')) exp_rsa = FINGER_RSA.split()[1].replace(':', '')
my_rsa = hexlify(key.get_fingerprint()) my_rsa = hexlify(key.get_fingerprint())
self.assertEqual(exp_rsa, my_rsa) self.assertEquals(exp_rsa, my_rsa)
self.assertEqual(PUB_RSA.split()[1], key.get_base64()) self.assertEquals(PUB_RSA.split()[1], key.get_base64())
self.assertEqual(1024, key.get_bits()) self.assertEquals(1024, key.get_bits())
def test_4_load_dss(self): def test_4_load_dss(self):
key = DSSKey.from_private_key_file(test_path('test_dss.key')) key = DSSKey.from_private_key_file('tests/test_dss.key')
self.assertEqual('ssh-dss', key.get_name()) self.assertEquals('ssh-dss', key.get_name())
exp_dss = b(FINGER_DSS.split()[1].replace(':', '')) exp_dss = FINGER_DSS.split()[1].replace(':', '')
my_dss = hexlify(key.get_fingerprint()) my_dss = hexlify(key.get_fingerprint())
self.assertEqual(exp_dss, my_dss) self.assertEquals(exp_dss, my_dss)
self.assertEqual(PUB_DSS.split()[1], key.get_base64()) self.assertEquals(PUB_DSS.split()[1], key.get_base64())
self.assertEqual(1024, key.get_bits()) self.assertEquals(1024, key.get_bits())
s = StringIO() s = StringIO()
key.write_private_key(s) key.write_private_key(s)
self.assertEqual(DSS_PRIVATE_OUT, s.getvalue()) self.assertEquals(DSS_PRIVATE_OUT, s.getvalue())
s.seek(0) s.seek(0)
key2 = DSSKey.from_private_key(s) key2 = DSSKey.from_private_key(s)
self.assertEqual(key, key2) self.assertEquals(key, key2)
def test_5_load_dss_password(self): def test_5_load_dss_password(self):
key = DSSKey.from_private_key_file(test_path('test_dss_password.key'), 'television') key = DSSKey.from_private_key_file('tests/test_dss_password.key', 'television')
self.assertEqual('ssh-dss', key.get_name()) self.assertEquals('ssh-dss', key.get_name())
exp_dss = b(FINGER_DSS.split()[1].replace(':', '')) exp_dss = FINGER_DSS.split()[1].replace(':', '')
my_dss = hexlify(key.get_fingerprint()) my_dss = hexlify(key.get_fingerprint())
self.assertEqual(exp_dss, my_dss) self.assertEquals(exp_dss, my_dss)
self.assertEqual(PUB_DSS.split()[1], key.get_base64()) self.assertEquals(PUB_DSS.split()[1], key.get_base64())
self.assertEqual(1024, key.get_bits()) self.assertEquals(1024, key.get_bits())
def test_6_compare_rsa(self): def test_6_compare_rsa(self):
# verify that the private & public keys compare equal # verify that the private & public keys compare equal
key = RSAKey.from_private_key_file(test_path('test_rsa.key')) key = RSAKey.from_private_key_file('tests/test_rsa.key')
self.assertEqual(key, key) self.assertEquals(key, key)
pub = RSAKey(data=key.asbytes()) pub = RSAKey(data=str(key))
self.assertTrue(key.can_sign()) self.assert_(key.can_sign())
self.assertTrue(not pub.can_sign()) self.assert_(not pub.can_sign())
self.assertEqual(key, pub) self.assertEquals(key, pub)
def test_7_compare_dss(self): def test_7_compare_dss(self):
# verify that the private & public keys compare equal # verify that the private & public keys compare equal
key = DSSKey.from_private_key_file(test_path('test_dss.key')) key = DSSKey.from_private_key_file('tests/test_dss.key')
self.assertEqual(key, key) self.assertEquals(key, key)
pub = DSSKey(data=key.asbytes()) pub = DSSKey(data=str(key))
self.assertTrue(key.can_sign()) self.assert_(key.can_sign())
self.assertTrue(not pub.can_sign()) self.assert_(not pub.can_sign())
self.assertEqual(key, pub) self.assertEquals(key, pub)
def test_8_sign_rsa(self): def test_8_sign_rsa(self):
# verify that the rsa private key can sign and verify # verify that the rsa private key can sign and verify
key = RSAKey.from_private_key_file(test_path('test_rsa.key')) key = RSAKey.from_private_key_file('tests/test_rsa.key')
msg = key.sign_ssh_data(b'ice weasels') msg = key.sign_ssh_data(rng, 'ice weasels')
self.assertTrue(type(msg) is Message) self.assert_(type(msg) is Message)
msg.rewind() msg.rewind()
self.assertEqual('ssh-rsa', msg.get_text()) self.assertEquals('ssh-rsa', msg.get_string())
sig = bytes().join([byte_chr(int(x, 16)) for x in SIGNED_RSA.split(':')]) sig = ''.join([chr(int(x, 16)) for x in SIGNED_RSA.split(':')])
self.assertEqual(sig, msg.get_binary()) self.assertEquals(sig, msg.get_string())
msg.rewind() msg.rewind()
pub = RSAKey(data=key.asbytes()) pub = RSAKey(data=str(key))
self.assertTrue(pub.verify_ssh_sig(b'ice weasels', msg)) self.assert_(pub.verify_ssh_sig('ice weasels', msg))
def test_9_sign_dss(self): def test_9_sign_dss(self):
# verify that the dss private key can sign and verify # verify that the dss private key can sign and verify
key = DSSKey.from_private_key_file(test_path('test_dss.key')) key = DSSKey.from_private_key_file('tests/test_dss.key')
msg = key.sign_ssh_data(b'ice weasels') msg = key.sign_ssh_data(rng, 'ice weasels')
self.assertTrue(type(msg) is Message) self.assert_(type(msg) is Message)
msg.rewind() msg.rewind()
self.assertEqual('ssh-dss', msg.get_text()) self.assertEquals('ssh-dss', msg.get_string())
# can't do the same test as we do for RSA, because DSS signatures # can't do the same test as we do for RSA, because DSS signatures
# are usually different each time. but we can test verification # are usually different each time. but we can test verification
# anyway so it's ok. # anyway so it's ok.
self.assertEqual(40, len(msg.get_binary())) self.assertEquals(40, len(msg.get_string()))
msg.rewind() msg.rewind()
pub = DSSKey(data=key.asbytes()) pub = DSSKey(data=str(key))
self.assertTrue(pub.verify_ssh_sig(b'ice weasels', msg)) self.assert_(pub.verify_ssh_sig('ice weasels', msg))
def test_A_generate_rsa(self): def test_A_generate_rsa(self):
key = RSAKey.generate(1024) key = RSAKey.generate(1024)
msg = key.sign_ssh_data(b'jerri blank') msg = key.sign_ssh_data(rng, 'jerri blank')
msg.rewind() msg.rewind()
self.assertTrue(key.verify_ssh_sig(b'jerri blank', msg)) self.assert_(key.verify_ssh_sig('jerri blank', msg))
def test_B_generate_dss(self): def test_B_generate_dss(self):
key = DSSKey.generate(1024) key = DSSKey.generate(1024)
msg = key.sign_ssh_data(b'jerri blank') msg = key.sign_ssh_data(rng, 'jerri blank')
msg.rewind() msg.rewind()
self.assertTrue(key.verify_ssh_sig(b'jerri blank', msg)) self.assert_(key.verify_ssh_sig('jerri blank', msg))
def test_10_load_ecdsa(self):
key = ECDSAKey.from_private_key_file(test_path('test_ecdsa.key'))
self.assertEqual('ecdsa-sha2-nistp256', key.get_name())
exp_ecdsa = b(FINGER_ECDSA.split()[1].replace(':', ''))
my_ecdsa = hexlify(key.get_fingerprint())
self.assertEqual(exp_ecdsa, my_ecdsa)
self.assertEqual(PUB_ECDSA.split()[1], key.get_base64())
self.assertEqual(256, key.get_bits())
s = StringIO()
key.write_private_key(s)
self.assertEqual(ECDSA_PRIVATE_OUT, s.getvalue())
s.seek(0)
key2 = ECDSAKey.from_private_key(s)
self.assertEqual(key, key2)
def test_11_load_ecdsa_password(self):
key = ECDSAKey.from_private_key_file(test_path('test_ecdsa_password.key'), b'television')
self.assertEqual('ecdsa-sha2-nistp256', key.get_name())
exp_ecdsa = b(FINGER_ECDSA.split()[1].replace(':', ''))
my_ecdsa = hexlify(key.get_fingerprint())
self.assertEqual(exp_ecdsa, my_ecdsa)
self.assertEqual(PUB_ECDSA.split()[1], key.get_base64())
self.assertEqual(256, key.get_bits())
def test_12_compare_ecdsa(self):
# verify that the private & public keys compare equal
key = ECDSAKey.from_private_key_file(test_path('test_ecdsa.key'))
self.assertEqual(key, key)
pub = ECDSAKey(data=key.asbytes())
self.assertTrue(key.can_sign())
self.assertTrue(not pub.can_sign())
self.assertEqual(key, pub)
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(b'ice weasels')
self.assertTrue(type(msg) is Message)
msg.rewind()
self.assertEqual('ecdsa-sha2-nistp256', msg.get_text())
# ECDSA signatures, like DSS signatures, tend to be different
# each time, so we can't compare against a "known correct"
# signature.
# Even the length of the signature can change.
msg.rewind()
pub = ECDSAKey(data=key.asbytes())
self.assertTrue(pub.verify_ssh_sig(b'ice weasels', msg))

Some files were not shown because too many files have changed in this diff Show More