Compare commits
16 Commits
master
...
python3-su
Author | SHA1 | Date |
---|---|---|
Dorian | bec140c933 | |
Dorian | 3cb91a0e9e | |
Dorian | 9ffd9efb20 | |
Dorian | 3c33c763a7 | |
Dorian | 387f243c1d | |
Dorian | 2c538bd5fe | |
Dorian | 5a9fc81ad9 | |
Dorian | cc2dd9c0f4 | |
Dorian | 1022eec17a | |
Dorian | b94fce4df9 | |
Dorian | b2f74c1291 | |
Dorian | a77054d632 | |
Dorian | 3bad2a13be | |
Dorian | 86fe372a2c | |
Dorian | b1e235d820 | |
Dorian | 0847ac780b |
|
@ -5,6 +5,4 @@ dist/
|
||||||
paramiko.egg-info/
|
paramiko.egg-info/
|
||||||
test.log
|
test.log
|
||||||
docs/
|
docs/
|
||||||
!sites/docs
|
.idea/
|
||||||
_build
|
|
||||||
.coverage
|
|
22
.travis.yml
22
.travis.yml
|
@ -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
|
|
||||||
|
|
|
@ -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
79
NEWS
|
@ -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
13
README
|
@ -5,7 +5,7 @@ paramiko
|
||||||
|
|
||||||
:Paramiko: Python SSH module
|
:Paramiko: Python SSH module
|
||||||
:Copyright: Copyright (c) 2003-2009 Robey Pointer <robeypointer@gmail.com>
|
:Copyright: Copyright (c) 2003-2009 Robey Pointer <robeypointer@gmail.com>
|
||||||
:Copyright: Copyright (c) 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::
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
try:
|
|
||||||
import interactive
|
import interactive
|
||||||
except ImportError:
|
|
||||||
from . import interactive
|
try:
|
||||||
|
input = raw_input
|
||||||
|
except NameError:
|
||||||
|
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()
|
||||||
|
|
|
@ -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)))
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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,25 +93,21 @@ 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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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()
|
|
@ -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,6 +88,17 @@ 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
|
||||||
|
|
|
@ -8,16 +8,7 @@ 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
|
||||||
|
@ -44,7 +35,7 @@ def format_system_message(errno):
|
||||||
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,
|
||||||
|
@ -56,7 +47,7 @@ def format_system_message(errno):
|
||||||
# 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
|
||||||
|
@ -90,6 +81,9 @@ def handle_nonzero_success(result):
|
||||||
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,
|
||||||
|
@ -124,7 +118,7 @@ class MemoryMap(object):
|
||||||
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")
|
||||||
|
@ -136,18 +130,15 @@ class MemoryMap(object):
|
||||||
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
|
||||||
|
|
||||||
|
@ -182,7 +173,7 @@ class SECURITY_DESCRIPTOR(ctypes.Structure):
|
||||||
PACL Dacl;
|
PACL Dacl;
|
||||||
} SECURITY_DESCRIPTOR;
|
} SECURITY_DESCRIPTOR;
|
||||||
"""
|
"""
|
||||||
SECURITY_DESCRIPTOR_CONTROL = USHORT
|
SECURITY_DESCRIPTOR_CONTROL = ctypes.wintypes.USHORT
|
||||||
REVISION = 1
|
REVISION = 1
|
||||||
|
|
||||||
_fields_ = [
|
_fields_ = [
|
||||||
|
|
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
|
|
@ -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,7 +405,7 @@ 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
|
||||||
|
@ -413,6 +413,7 @@ class AuthHandler (object):
|
||||||
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,
|
||||||
MSG_SERVICE_ACCEPT: _parse_service_accept,
|
MSG_SERVICE_ACCEPT: _parse_service_accept,
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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...
|
||||||
|
@ -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)
|
||||||
|
|
|
@ -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
|
@ -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,7 +336,7 @@ 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
|
||||||
|
@ -274,62 +344,66 @@ class SSHClient (object):
|
||||||
|
|
||||||
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())))
|
|
||||||
|
|
|
@ -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,8 +19,6 @@
|
||||||
"""
|
"""
|
||||||
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)
|
||||||
|
@ -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 = {
|
||||||
|
@ -126,42 +95,29 @@ 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
|
|
||||||
else:
|
|
||||||
cr_byte_value = 13
|
|
||||||
linefeed_byte_value = 10
|
|
||||||
|
|
||||||
|
import sys
|
||||||
def asbytes(s):
|
if sys.version_info < (2, 3):
|
||||||
if not isinstance(s, bytes_types):
|
|
||||||
if isinstance(s, string_types):
|
|
||||||
s = b(s)
|
|
||||||
else:
|
|
||||||
try:
|
try:
|
||||||
s = s.asbytes()
|
import logging
|
||||||
except Exception:
|
except:
|
||||||
raise Exception('Unknown type')
|
import logging22 as logging
|
||||||
return s
|
import select
|
||||||
|
PY22 = True
|
||||||
|
|
||||||
|
import socket
|
||||||
|
if not hasattr(socket, 'timeout'):
|
||||||
|
class timeout(socket.error): pass
|
||||||
|
socket.timeout = timeout
|
||||||
|
del timeout
|
||||||
|
else:
|
||||||
|
import logging
|
||||||
|
PY22 = False
|
||||||
|
|
||||||
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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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())
|
||||||
|
@ -97,13 +95,13 @@ class DSSKey (PKey):
|
||||||
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))
|
||||||
|
@ -146,7 +144,7 @@ class DSSKey (PKey):
|
||||||
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,20 +157,24 @@ 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)
|
||||||
|
@ -186,8 +188,8 @@ class DSSKey (PKey):
|
||||||
# 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]
|
||||||
|
|
|
@ -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
|
|
191
paramiko/file.py
191
paramiko/file.py
|
@ -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,26 +92,10 @@ 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
|
|
||||||
`~exceptions.StopIteration` when EOF is hit. Unlike Python file
|
|
||||||
objects, it's okay to mix calls to `next` and `readline`.
|
|
||||||
|
|
||||||
:raises StopIteration: when the end of the file is reached.
|
|
||||||
|
|
||||||
:return: a line (`str`) read from the file.
|
|
||||||
"""
|
|
||||||
line = self.readline()
|
|
||||||
if not line:
|
|
||||||
raise StopIteration
|
|
||||||
return line
|
|
||||||
else:
|
|
||||||
def __next__(self):
|
|
||||||
"""
|
"""
|
||||||
Returns the next line from the input, or raises L{StopIteration} when
|
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
|
EOF is hit. Unlike python file objects, it's okay to mix calls to
|
||||||
|
@ -120,20 +113,15 @@ class BufferedFile (object):
|
||||||
|
|
||||||
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:
|
||||||
|
@ -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,)
|
||||||
|
|
|
@ -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,20 +153,21 @@ 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] == '#'):
|
||||||
|
@ -104,55 +180,46 @@ class HostKeys (MutableMapping):
|
||||||
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)
|
|
||||||
|
|
|
@ -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,16 +203,16 @@ 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()
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
@ -74,8 +71,10 @@ class KexGroup1(object):
|
||||||
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()
|
||||||
|
|
|
@ -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()
|
|
@ -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,33 +215,22 @@ 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
|
|
||||||
|
|
||||||
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
|
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(max_byte)
|
|
||||||
self.add_string(util.deflate_long(n))
|
|
||||||
else:
|
|
||||||
self.packet.write(struct.pack('>I', n))
|
self.packet.write(struct.pack('>I', n))
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
@ -261,7 +238,8 @@ class Message (object):
|
||||||
"""
|
"""
|
||||||
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_boolean(i)
|
return self.add_string(i)
|
||||||
elif isinstance(i, integer_types):
|
|
||||||
|
elif type(i) in six.integer_types:
|
||||||
|
|
||||||
|
if type(i) is int and not six.PY3:
|
||||||
return self.add_int(i)
|
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)
|
||||||
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::
|
@param seq: the sequence of items
|
||||||
Longs are encoded non-deterministically. Don't use this method.
|
@type seq: sequence
|
||||||
|
|
||||||
:param seq: the sequence of items
|
@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)
|
||||||
|
|
|
@ -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):
|
||||||
|
@ -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:
|
||||||
|
@ -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,20 +311,20 @@ 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)' %
|
||||||
|
@ -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,9 +366,9 @@ 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:
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
@ -64,7 +63,7 @@ class PosixPipe (object):
|
||||||
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
|
||||||
|
@ -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
|
||||||
|
|
252
paramiko/pkey.py
252
paramiko/pkey.py
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -44,35 +45,36 @@ class PKey (object):
|
||||||
'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,39 +336,44 @@ 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)])
|
||||||
|
|
|
@ -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"
|
||||||
|
hbyte_mask = pow(2, bits % 8) - 1
|
||||||
|
while True:
|
||||||
|
# loop catches the case where we increment n into a higher bit-range
|
||||||
|
x = rng.read((bits+7) // 8)
|
||||||
|
if hbyte_mask > 0:
|
||||||
|
x = 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)
|
bits = util.bit_length(n-1)
|
||||||
byte_count = (bits + 7) // 8
|
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,10 +112,10 @@ 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] == '#'):
|
||||||
|
@ -104,15 +124,17 @@ class ModulusPack (object):
|
||||||
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]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
|
@ -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:
|
||||||
|
|
|
@ -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,23 +89,23 @@ 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,))
|
||||||
|
|
||||||
|
@ -123,7 +120,7 @@ class RSAKey (PKey):
|
||||||
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)
|
||||||
|
@ -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,16 +149,19 @@ 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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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,15 +20,16 @@ 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)
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -122,7 +125,7 @@ class BaseSFTP (object):
|
||||||
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
|
||||||
|
@ -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, ''
|
||||||
|
|
|
@ -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))
|
|
||||||
|
|
|
@ -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):
|
class SFTPClient (BaseSFTP):
|
||||||
"""
|
"""
|
||||||
SFTP client object.
|
SFTP client object. C{SFTPClient} is used to open an sftp session across
|
||||||
|
an open ssh L{Transport} and do remote file operations.
|
||||||
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
|
|
||||||
msg = msg.replace('%','%%')
|
|
||||||
super(SFTPClient, self)._log(level, "[chan %s] " + msg, *([ self.sock.get_name() ] + list(args)))
|
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
|
||||||
|
|
|
@ -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))
|
||||||
|
|
||||||
|
@ -440,8 +438,10 @@ class SFTPFile (BufferedFile):
|
||||||
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
|
||||||
del self._prefetch_extents[num]
|
if len(self._prefetch_reads) == 0:
|
||||||
if len(self._prefetch_extents) == 0:
|
|
||||||
self._prefetch_done = True
|
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
|
||||||
|
|
|
@ -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,15 +33,16 @@ 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
|
||||||
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
@ -45,8 +46,9 @@ class SFTPServerInterface (object):
|
||||||
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
|
||||||
|
|
|
@ -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,19 +52,18 @@ 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
182
paramiko/util.py
182
paramiko/util.py
|
@ -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,37 +115,31 @@ 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:
|
|
||||||
norm = deflate_long(n, False)
|
|
||||||
hbyte = byte_ord(norm[0])
|
|
||||||
if hbyte == 0:
|
if hbyte == 0:
|
||||||
return 1
|
return 1
|
||||||
bitlen = len(norm) * 8
|
bitlen = len(norm) * 8
|
||||||
|
@ -138,34 +148,36 @@ def bit_length(n):
|
||||||
bitlen -= 1
|
bitlen -= 1
|
||||||
return bitlen
|
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
|
|
||||||
|
|
|
@ -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
|
||||||
|
@ -70,7 +63,6 @@ class COPYDATASTRUCT(ctypes.Structure):
|
||||||
('data_loc', ctypes.c_void_p),
|
('data_loc', ctypes.c_void_p),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def _query_pageant(msg):
|
def _query_pageant(msg):
|
||||||
"""
|
"""
|
||||||
Communication with the Pageant process is done through a shared
|
Communication with the Pageant process is done through a shared
|
||||||
|
@ -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,7 +98,6 @@ 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
|
||||||
|
|
27
setup.py
27
setup.py
|
@ -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",
|
||||||
long_description = longdesc,
|
|
||||||
author = "Jeff Forcier",
|
author = "Jeff Forcier",
|
||||||
author_email = "jeff@bitprophet.org",
|
author_email = "jeff@bitprophet.org",
|
||||||
url = "https://github.com/paramiko/paramiko/",
|
url = "https://github.com/paramiko/paramiko/",
|
||||||
packages = [ 'paramiko' ],
|
packages = [ 'paramiko' ],
|
||||||
license = 'LGPL',
|
license = 'LGPL',
|
||||||
platforms = 'Posix; MacOS X; Windows',
|
platforms = 'Posix; MacOS X; Windows',
|
||||||
classifiers = [
|
classifiers = [ 'Development Status :: 5 - Production/Stable',
|
||||||
'Development Status :: 5 - Production/Stable',
|
|
||||||
'Intended Audience :: Developers',
|
'Intended Audience :: Developers',
|
||||||
'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)',
|
'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)',
|
||||||
'Operating System :: OS Independent',
|
'Operating System :: OS Independent',
|
||||||
'Topic :: Internet',
|
'Topic :: Internet',
|
||||||
'Topic :: Security :: Cryptography',
|
'Topic :: Security :: Cryptography' ],
|
||||||
'Programming Language :: Python',
|
long_description = longdesc,
|
||||||
'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
|
**kw
|
||||||
)
|
)
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
SSH Agents
|
|
||||||
==========
|
|
||||||
|
|
||||||
.. automodule:: paramiko.agent
|
|
||||||
:inherited-members:
|
|
||||||
:no-special-members:
|
|
|
@ -1,4 +0,0 @@
|
||||||
Buffered pipes
|
|
||||||
==============
|
|
||||||
|
|
||||||
.. automodule:: paramiko.buffered_pipe
|
|
|
@ -1,4 +0,0 @@
|
||||||
Channel
|
|
||||||
=======
|
|
||||||
|
|
||||||
.. automodule:: paramiko.channel
|
|
|
@ -1,5 +0,0 @@
|
||||||
Client
|
|
||||||
======
|
|
||||||
|
|
||||||
.. automodule:: paramiko.client
|
|
||||||
:member-order: bysource
|
|
|
@ -1,5 +0,0 @@
|
||||||
Configuration
|
|
||||||
=============
|
|
||||||
|
|
||||||
.. automodule:: paramiko.config
|
|
||||||
:member-order: bysource
|
|
|
@ -1,4 +0,0 @@
|
||||||
Buffered files
|
|
||||||
==============
|
|
||||||
|
|
||||||
.. automodule:: paramiko.file
|
|
|
@ -1,5 +0,0 @@
|
||||||
Host keys / ``known_hosts`` files
|
|
||||||
=================================
|
|
||||||
|
|
||||||
.. automodule:: paramiko.hostkeys
|
|
||||||
:member-order: bysource
|
|
|
@ -1,6 +0,0 @@
|
||||||
Key handling
|
|
||||||
============
|
|
||||||
|
|
||||||
.. automodule:: paramiko.pkey
|
|
||||||
.. automodule:: paramiko.dsskey
|
|
||||||
.. automodule:: paramiko.rsakey
|
|
|
@ -1,4 +0,0 @@
|
||||||
Message
|
|
||||||
=======
|
|
||||||
|
|
||||||
.. automodule:: paramiko.message
|
|
|
@ -1,4 +0,0 @@
|
||||||
Packetizer
|
|
||||||
==========
|
|
||||||
|
|
||||||
.. automodule:: paramiko.packet
|
|
|
@ -1,4 +0,0 @@
|
||||||
Cross-platform pipe implementations
|
|
||||||
===================================
|
|
||||||
|
|
||||||
.. automodule:: paramiko.pipe
|
|
|
@ -1,4 +0,0 @@
|
||||||
``ProxyCommand`` support
|
|
||||||
========================
|
|
||||||
|
|
||||||
.. automodule:: paramiko.proxy
|
|
|
@ -1,5 +0,0 @@
|
||||||
Server implementation
|
|
||||||
=====================
|
|
||||||
|
|
||||||
.. automodule:: paramiko.server
|
|
||||||
:member-order: bysource
|
|
|
@ -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
|
|
|
@ -1,4 +0,0 @@
|
||||||
Exceptions
|
|
||||||
==========
|
|
||||||
|
|
||||||
.. automodule:: paramiko.ssh_exception
|
|
|
@ -1,5 +0,0 @@
|
||||||
Transport
|
|
||||||
=========
|
|
||||||
|
|
||||||
.. automodule:: paramiko.transport
|
|
||||||
:member-order: bysource
|
|
|
@ -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',
|
|
||||||
}
|
|
|
@ -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
|
|
|
@ -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'
|
|
|
@ -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>
|
|
|
@ -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.
|
|
|
@ -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',
|
|
||||||
}
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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:\>
|
|
49
tasks.py
49
tasks.py
|
@ -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)
|
|
33
test.py
33
test.py
|
@ -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')
|
||||||
|
@ -108,15 +109,12 @@ def main():
|
||||||
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))
|
||||||
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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):
|
||||||
|
@ -96,9 +95,9 @@ class StubSFTPServer (SFTPServerInterface):
|
||||||
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):
|
||||||
|
|
|
@ -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,20 +25,17 @@ 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':
|
||||||
|
@ -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))
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
-----BEGIN EC PRIVATE KEY-----
|
|
||||||
MHcCAQEEIKB6ty3yVyKEnfF/zprx0qwC76MsMlHY4HXCnqho2eKioAoGCCqGSM49
|
|
||||||
AwEHoUQDQgAElI9mbdlaS+T9nHxY/59lFnn80EEecZDBHq4gLpccY8Mge5ZTMiMD
|
|
||||||
ADRvOqQ5R98Sxst765CAqXmRtz8vwoD96g==
|
|
||||||
-----END EC PRIVATE KEY-----
|
|
|
@ -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-----
|
|
|
@ -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()
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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:])
|
||||||
|
|
||||||
|
|
|
@ -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,18 +21,10 @@ 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):
|
||||||
|
|
||||||
|
@ -43,21 +35,21 @@ class PacketizerTest (unittest.TestCase):
|
||||||
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()
|
||||||
|
@ -66,11 +58,13 @@ class PacketizerTest (unittest.TestCase):
|
||||||
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())
|
||||||
|
|
|
@ -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
Loading…
Reference in New Issue