diff --git a/README b/README index b7284cc..ab9b210 100644 --- a/README +++ b/README @@ -9,7 +9,7 @@ http://www.lag.net/~robey/paramiko/ *** WHAT "paramiko" is a combination of the esperanto words for "paranoid" and "friend". -it's a module for python 2.3 that implements the SSH2 protocol for secure +it's a module for python 2.2+ that implements the SSH2 protocol for secure (encrypted and authenticated) connections to remote machines. unlike SSL (aka TLS), SSH2 protocol does not require heirarchical certificates signed by a powerful central authority. you may know SSH2 as the protocol that replaced @@ -27,6 +27,7 @@ should have come with this archive. *** REQUIREMENTS python 2.3 + (python 2.2 may work with some pain) pyCrypt PyCrypt compiled for Win32 can be downloaded from the HashTar homepage: @@ -46,6 +47,13 @@ it changes behavior in some fundamental ways, and these ways require posix. so don't call "fileno()" on a Channel on Windows. this is detailed in the documentation for the "fileno" method. +python 2.2 may work, thanks to some patches from Roger Binns. things to watch +out for: +* sockets in 2.2 don't support timeouts, so the 'select' module is imported + to do polling. this may not work on windows. (works fine on osx.) +* there is no logging, period. +you really should upgrade to python 2.3. laziness is no excuse! + *** DEMO @@ -81,7 +89,7 @@ which actually motivated me to write more documentation than i ever would have before. there are also unit tests here: - $ python2 ./test.py + $ python ./test.py which will verify that some of the core components are working correctly. not much is tested yet, but it's a start. the tests for SFTP are probably the best and easiest examples of how to use the SFTP class. diff --git a/demo.py b/demo.py index c9dd501..dfb7231 100755 --- a/demo.py +++ b/demo.py @@ -1,6 +1,6 @@ #!/usr/bin/python -import sys, os, socket, threading, getpass, logging, time, base64, select, termios, tty, traceback +import sys, os, socket, threading, getpass, time, base64, select, termios, tty, traceback import paramiko diff --git a/demo_server.py b/demo_server.py index 6447d4a..5d6cbb5 100755 --- a/demo_server.py +++ b/demo_server.py @@ -1,16 +1,10 @@ #!/usr/bin/python -import sys, os, socket, threading, logging, traceback, base64 +import sys, os, socket, threading, traceback, base64 import paramiko # setup logging -l = logging.getLogger("paramiko") -l.setLevel(logging.DEBUG) -if len(l.handlers) == 0: - f = open('demo_server.log', 'w') - lh = logging.StreamHandler(f) - lh.setFormatter(logging.Formatter('%(levelname)-.3s [%(asctime)s] %(name)s: %(message)s', '%Y%m%d:%H%M%S')) - l.addHandler(lh) +paramiko.util.log_to_file('demo_server.log') #host_key = paramiko.RSAKey() #host_key.read_private_key_file('demo_rsa_key') diff --git a/demo_simple.py b/demo_simple.py index 6a216b2..0bc46bf 100755 --- a/demo_simple.py +++ b/demo_simple.py @@ -1,6 +1,6 @@ #!/usr/bin/python -import sys, os, base64, getpass, socket, logging, traceback, termios, tty, select +import sys, os, base64, getpass, socket, traceback, termios, tty, select import paramiko diff --git a/paramiko/__init__.py b/paramiko/__init__.py index 80cae55..4e5f4aa 100644 --- a/paramiko/__init__.py +++ b/paramiko/__init__.py @@ -56,8 +56,8 @@ Website: U{http://www.lag.net/~robey/paramiko/} import sys -if (sys.version_info[0] < 2) or ((sys.version_info[0] == 2) and (sys.version_info[1] < 3)): - raise RuntimeError('You need python 2.3 for this module.') +if sys.version_info < (2, 2): + raise RuntimeError('You need python 2.2 for this module.') __author__ = "Robey Pointer " diff --git a/paramiko/auth_transport.py b/paramiko/auth_transport.py index ccb7149..b8468f0 100644 --- a/paramiko/auth_transport.py +++ b/paramiko/auth_transport.py @@ -24,10 +24,10 @@ This separation keeps either class file from being too unwieldy. """ from common import * +import util from transport import BaseTransport from message import Message from ssh_exception import SSHException -from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL class Transport (BaseTransport): diff --git a/paramiko/channel.py b/paramiko/channel.py index cf2ff63..44672d9 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -22,14 +22,14 @@ Abstraction for an SSH2 channel. """ +import time, threading, socket, os + from common import * +import util from message import Message from ssh_exception import SSHException from file import BufferedFile -import time, threading, logging, socket, os -from logging import DEBUG - # this is ugly, and won't work on windows def _set_nonblocking(fd): diff --git a/paramiko/common.py b/paramiko/common.py index 4aa2c34..e5aaaf8 100644 --- a/paramiko/common.py +++ b/paramiko/common.py @@ -59,3 +59,18 @@ except: randpool.randomize() + +import sys +if sys.version_info < (2, 3): + import logging22 as logging + import select + PY22 = True +else: + import logging + PY22 = False + +DEBUG = logging.DEBUG +INFO = logging.INFO +WARNING = logging.WARNING +ERROR = logging.ERROR +CRITICAL = logging.CRITICAL diff --git a/paramiko/kex_gex.py b/paramiko/kex_gex.py index c7b07a3..7db93d6 100644 --- a/paramiko/kex_gex.py +++ b/paramiko/kex_gex.py @@ -26,11 +26,10 @@ client side, and a B{lot} more on the server side. from Crypto.Hash import SHA from Crypto.Util import number -from logging import DEBUG from common import * from message import Message -from util import inflate_long, deflate_long, bit_length +import util from ssh_exception import SSHException @@ -76,7 +75,7 @@ class KexGex (object): def _generate_x(self): # generate an "x" (1 < x < (p-1)/2). q = (self.p - 1) // 2 - qnorm = deflate_long(q, 0) + qnorm = util.deflate_long(q, 0) qhbyte = ord(qnorm[0]) bytes = len(qnorm) qmask = 0xff @@ -87,7 +86,7 @@ class KexGex (object): self.transport.randpool.stir() x_bytes = self.transport.randpool.get_bytes(bytes) x_bytes = chr(ord(x_bytes[0]) & qmask) + x_bytes[1:] - x = inflate_long(x_bytes, 1) + x = util.inflate_long(x_bytes, 1) if (x > 1) and (x < q): break self.x = x @@ -128,7 +127,7 @@ class KexGex (object): self.p = m.get_mpint() self.g = m.get_mpint() # reject if p's bit length < 1024 or > 8192 - bitlen = bit_length(self.p) + bitlen = util.bit_length(self.p) if (bitlen < 1024) or (bitlen > 8192): raise SSHException('Server-generated gex p (don\'t ask) is out of range (%d bits)' % bitlen) self.transport._log(DEBUG, 'Got server p (%d bits)' % bitlen) diff --git a/paramiko/kex_group1.py b/paramiko/kex_group1.py index 77857f8..c528f29 100644 --- a/paramiko/kex_group1.py +++ b/paramiko/kex_group1.py @@ -24,10 +24,10 @@ Standard SSH key exchange ("kex" if you wanna sound cool). Diffie-Hellman of """ from Crypto.Hash import SHA -from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL from common import * -from message import Message, inflate_long +import util +from message import Message from ssh_exception import SSHException _MSG_KEXDH_INIT, _MSG_KEXDH_REPLY = range(30, 32) @@ -57,7 +57,7 @@ class KexGroup1(object): if (x_bytes[:8] != '\x7F\xFF\xFF\xFF\xFF\xFF\xFF\xFF') and \ (x_bytes[:8] != '\x00\x00\x00\x00\x00\x00\x00\x00'): break - self.x = inflate_long(x_bytes) + self.x = util.inflate_long(x_bytes) def start_kex(self): self.generate_x() diff --git a/paramiko/logging22.py b/paramiko/logging22.py new file mode 100644 index 0000000..b59aacf --- /dev/null +++ b/paramiko/logging22.py @@ -0,0 +1,62 @@ +#!/usr/bin/python + +# Copyright (C) 2003-2004 Robey Pointer +# +# 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 Foobar; 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 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() diff --git a/paramiko/message.py b/paramiko/message.py index bb79d60..0ce0c70 100644 --- a/paramiko/message.py +++ b/paramiko/message.py @@ -23,7 +23,7 @@ Implementation of an SSH2 "message". """ import string, types, struct -from util import inflate_long, deflate_long +import util class Message (object): @@ -158,7 +158,7 @@ class Message (object): @return: an arbitrary-length integer. @rtype: long """ - return inflate_long(self.get_string()) + return util.inflate_long(self.get_string()) def get_string(self): """ @@ -219,7 +219,7 @@ class Message (object): def add_mpint(self, z): "this only works on positive numbers" - self.add_string(deflate_long(z)) + self.add_string(util.deflate_long(z)) return self def add_string(self, s): diff --git a/paramiko/sftp.py b/paramiko/sftp.py index 928a134..9a26298 100644 --- a/paramiko/sftp.py +++ b/paramiko/sftp.py @@ -18,11 +18,11 @@ # along with Foobar; if not, write to the Free Software Foundation, Inc., # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. -import struct, logging, socket -from util import format_binary, tb_strings +import struct, socket +from common import * +import util from channel import Channel from message import Message -from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL from file import BufferedFile CMD_INIT, CMD_VERSION, CMD_OPEN, CMD_CLOSE, CMD_READ, CMD_WRITE, CMD_LSTAT, CMD_FSTAT, CMD_SETSTAT, \ @@ -511,14 +511,14 @@ class SFTP (object): def _send_packet(self, t, packet): out = struct.pack('>I', len(packet) + 1) + chr(t) + packet if self.ultra_debug: - self._log(DEBUG, format_binary(out, 'OUT: ')) + self._log(DEBUG, util.format_binary(out, 'OUT: ')) self._write_all(out) def _read_packet(self): size = struct.unpack('>I', self._read_all(4))[0] data = self._read_all(size) if self.ultra_debug: - self._log(DEBUG, format_binary(data, 'IN: ')); + self._log(DEBUG, util.format_binary(data, 'IN: ')); if size > 0: return ord(data[0]), data[1:] return 0, '' diff --git a/paramiko/transport.py b/paramiko/transport.py index 391f2a0..bc3338c 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -22,13 +22,13 @@ L{BaseTransport} handles the core SSH2 protocol. """ -import sys, os, string, threading, socket, logging, struct +import sys, os, string, threading, socket, struct from common import * from ssh_exception import SSHException from message import Message from channel import Channel -from util import format_binary, safe_string, inflate_long, deflate_long, tb_strings +import util from rsakey import RSAKey from dsskey import DSSKey from kex_group1 import KexGroup1 @@ -43,8 +43,6 @@ from primes import ModulusPack from Crypto.Cipher import Blowfish, AES, DES3 from Crypto.Hash import SHA, MD5, HMAC -from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL - # for thread cleanup _active_threads = [] @@ -105,7 +103,6 @@ class BaseTransport (threading.Thread): If the object is not actually a socket, it must have the following methods: - - C{settimeout(float)}: Sets a timeout for read & write calls. - C{send(string)}: Writes from 1 to C{len(string)} bytes, and returns an int representing the number of bytes written. Returns 0 or raises C{EOFError} if the stream has been closed. @@ -139,7 +136,11 @@ class BaseTransport (threading.Thread): threading.Thread.__init__(self, target=self._run) self.randpool = randpool self.sock = sock - self.sock.settimeout(0.1) + # Python < 2.3 doesn't have the settimeout method - RogerB + try: + self.sock.settimeout(0.1) + except AttributeError: + pass # negotiated crypto parameters self.local_version = 'SSH-' + self._PROTO_ID + '-' + self._CLIENT_ID self.remote_version = '' @@ -689,7 +690,24 @@ class BaseTransport (threading.Thread): finally: self.lock.release() + def _py22_read_all(self, n): + out = '' + while n > 0: + r, w, e = select.select([self.sock], [], [], 0.1) + if self.sock not in r: + if not self.active: + raise EOFError() + else: + x = self.sock.recv(n) + if len(x) == 0: + raise EOFError() + out += x + n -= len(x) + return out + def _read_all(self, n): + if PY22: + return self._py22_read_all(n) out = '' while n > 0: try: @@ -728,7 +746,7 @@ class BaseTransport (threading.Thread): # encrypt this sucka packet = self._build_packet(str(data)) if self.ultra_debug: - self._log(DEBUG, format_binary(packet, 'OUT: ')) + self._log(DEBUG, util.format_binary(packet, 'OUT: ')) if self.engine_out != None: out = self.engine_out.encrypt(packet) else: @@ -751,7 +769,7 @@ class BaseTransport (threading.Thread): if self.engine_in != None: header = self.engine_in.decrypt(header) if self.ultra_debug: - self._log(DEBUG, format_binary(header, 'IN: ')); + self._log(DEBUG, util.format_binary(header, 'IN: ')); packet_size = struct.unpack('>I', header[:4])[0] # leftover contains decrypted bytes from the first block (after the length field) leftover = header[4:] @@ -763,7 +781,7 @@ class BaseTransport (threading.Thread): if self.engine_in != None: packet = self.engine_in.decrypt(packet) if self.ultra_debug: - self._log(DEBUG, format_binary(packet, 'IN: ')); + self._log(DEBUG, util.format_binary(packet, 'IN: ')); packet = leftover + packet if self.remote_mac_len > 0: mac = post_packet[:self.remote_mac_len] @@ -891,15 +909,15 @@ class BaseTransport (threading.Thread): self._send_message(msg) except SSHException, e: self._log(DEBUG, 'Exception: ' + str(e)) - self._log(DEBUG, tb_strings()) + self._log(DEBUG, util.tb_strings()) self.saved_exception = e except EOFError, e: self._log(DEBUG, 'EOF') - self._log(DEBUG, tb_strings()) + self._log(DEBUG, util.tb_strings()) self.saved_exception = e except Exception, e: self._log(DEBUG, 'Unknown exception: ' + str(e)) - self._log(DEBUG, tb_strings()) + self._log(DEBUG, util.tb_strings()) self.saved_exception = e _active_threads.remove(self) if self.active: @@ -1276,7 +1294,7 @@ class BaseTransport (threading.Thread): always_display = m.get_boolean() msg = m.get_string() lang = m.get_string() - self._log(DEBUG, 'Debug msg: ' + safe_string(msg)) + self._log(DEBUG, 'Debug msg: ' + util.safe_string(msg)) _handler_table = { MSG_NEWKEYS: _parse_newkeys, diff --git a/paramiko/util.py b/paramiko/util.py index 57c9b98..191f845 100644 --- a/paramiko/util.py +++ b/paramiko/util.py @@ -18,11 +18,25 @@ # along with Foobar; if not, write to the Free Software Foundation, Inc., # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +from __future__ import generators + """ Useful functions used by the rest of paramiko. """ -import sys, struct, traceback, logging +import sys, struct, traceback +from common import * + +# 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): "turns a normalized byte string into a long-int (adapted from Crypto.Util.number)" @@ -174,7 +188,7 @@ def mod_inverse(x, m): u2 += m return u2 -def log_to_file(filename, level=logging.DEBUG): +def log_to_file(filename, level=DEBUG): "send paramiko logs to a logfile, if they're not already going somewhere" l = logging.getLogger("paramiko") if len(l.handlers) > 0: diff --git a/tests/test_sftp.py b/tests/test_sftp.py index 798853f..5085d7a 100755 --- a/tests/test_sftp.py +++ b/tests/test_sftp.py @@ -31,9 +31,10 @@ import sys, os HOST = os.environ.get('TEST_HOST', 'localhost') USER = os.environ.get('TEST_USER', os.environ.get('USER', 'nobody')) PKEY = os.environ.get('TEST_PKEY', os.path.join(os.environ.get('HOME', '/'), '.ssh/id_rsa')) +PKEY_PASSWD = os.environ.get('TEST_PKEY_PASSWD', None) FOLDER = os.environ.get('TEST_FOLDER', 'temp-testing') -import paramiko, logging, unittest +import paramiko, unittest ARTICLE = ''' Insulin sensitivity and liver insulin receptor structure in ducks from two @@ -64,16 +65,21 @@ decreased compared with chicken. # setup logging -l = logging.getLogger('paramiko') -l.setLevel(logging.DEBUG) -if len(l.handlers) == 0: - f = open('test.log', 'w') - lh = logging.StreamHandler(f) - lh.setFormatter(logging.Formatter('%(levelname)-.3s [%(asctime)s] %(name)s: %(message)s', '%Y%m%d:%H%M%S')) - l.addHandler(lh) +paramiko.util.log_to_file('test.log') + t = paramiko.Transport(HOST) -key = paramiko.RSAKey() -key.read_private_key_file(PKEY) +try: + key = paramiko.RSAKey.from_private_key_file(PKEY, PKEY_PASSWD) +except paramiko.PasswordRequiredException: + sys.stderr.write('\n\nparamiko.RSAKey.from_private_key_file REQUIRES PASSWORD.\n') + sys.stderr.write('You have two options:\n') + sys.stderr.write('* Change environment variable TEST_PKEY to point to a different\n') + sys.stderr.write(' (non-password-protected) private key file.\n') + sys.stderr.write('* Set environment variable TEST_PKEY_PASSWD to the password needed\n') + sys.stderr.write(' to unlock this private key.\n') + sys.stderr.write('\n') + sys.exit(1) + try: t.connect(username=USER, pkey=key) except paramiko.SSHException: