From 945a41dd3d2cf7f3d37012c588d8eb07bcc296b2 Mon Sep 17 00:00:00 2001 From: Robey Pointer Date: Tue, 6 Apr 2004 08:16:02 +0000 Subject: [PATCH] [project @ Arch-1:robey@lag.net--2003-public%secsh--dev--1.0--patch-42] support py22, more or less add roger binns' patches for supporting python 2.2. i hedged a bit on the logging stuff and just added some trickery to let logging be stubbed out for python 2.2. this changed a lot of import statements but i managed to avoid hacking at any of the existing logging. socket timeouts are required for the threads to notice when they've been deactivated. worked around it by using the 'select' module on py22. also fixed the sftp unit tests to cope with a password-protected private key. --- README | 12 ++++++-- demo.py | 2 +- demo_server.py | 10 ++---- demo_simple.py | 2 +- paramiko/__init__.py | 4 +-- paramiko/auth_transport.py | 2 +- paramiko/channel.py | 6 ++-- paramiko/common.py | 15 +++++++++ paramiko/kex_gex.py | 9 +++--- paramiko/kex_group1.py | 6 ++-- paramiko/logging22.py | 62 ++++++++++++++++++++++++++++++++++++++ paramiko/message.py | 6 ++-- paramiko/sftp.py | 10 +++--- paramiko/transport.py | 44 +++++++++++++++++++-------- paramiko/util.py | 18 +++++++++-- tests/test_sftp.py | 26 ++++++++++------ 16 files changed, 175 insertions(+), 59 deletions(-) create mode 100644 paramiko/logging22.py 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: