diff options
author | Robey Pointer <robey@lag.net> | 2003-12-30 07:18:20 +0000 |
---|---|---|
committer | Robey Pointer <robey@lag.net> | 2003-12-30 07:18:20 +0000 |
commit | 48c7d888a22a6810a32f6d25cdd6b561803166cd (patch) | |
tree | 33e22ab7a6f56ef6b569c674817127bb1108f7dd | |
parent | 36d6d95dc6db1f47d187b87ab1c1a10c1d30558f (diff) |
[project @ Arch-1:robey@lag.net--2003-public%secsh--dev--1.0--patch-17]
lots more documentation, and added Transport.connect()
renamed demo_host_key to demo_rsa_key. moved changelog to a separate file,
and indicated that future changelog entries should be fetched from tla.
tried to clean up "__all__" in a way that makes epydoc still work.
added lots more documentation, and renamed many methods and vars to hide
them as private non-exported API.
Transport's ModulusPack is now a static member, so it only has to be loaded
once, and can then be used by any future Transport object.
added Transport.connect(), which tries to wrap all the SSH2 negotiation and
authentication into one method. you should be able to create a Transport,
call connect(), and then create channels.
-rw-r--r-- | ChangeLog | 42 | ||||
-rw-r--r-- | Makefile | 7 | ||||
-rw-r--r-- | NOTES | 13 | ||||
-rw-r--r-- | README | 43 | ||||
-rwxr-xr-x | demo.py | 1 | ||||
-rw-r--r-- | demo_rsa_key (renamed from demo_host_key) | 0 | ||||
-rwxr-xr-x | demo_server.py | 3 | ||||
-rwxr-xr-x | demo_simple.py | 136 | ||||
-rw-r--r-- | paramiko.py | 24 | ||||
-rw-r--r-- | paramiko/__init__.py | 37 | ||||
-rw-r--r-- | paramiko/auth_transport.py | 42 | ||||
-rw-r--r-- | paramiko/channel.py | 553 | ||||
-rw-r--r-- | paramiko/dsskey.py | 14 | ||||
-rw-r--r-- | paramiko/kex_gex.py | 28 | ||||
-rw-r--r-- | paramiko/kex_group1.py | 16 | ||||
-rw-r--r-- | paramiko/pkey.py | 112 | ||||
-rw-r--r-- | paramiko/rsakey.py | 23 | ||||
-rw-r--r-- | paramiko/transport.py | 353 | ||||
-rw-r--r-- | setup.py | 2 |
19 files changed, 989 insertions, 460 deletions
diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 00000000..c151d251 --- /dev/null +++ b/ChangeLog @@ -0,0 +1,42 @@ + +2003-08-24: + * implemented the other hashes: all 4 from the draft are working now + * added 'aes128-cbc' and '3des-cbc' cipher support + * fixed channel eof/close semantics +2003-09-12: version "aerodactyl" + * implemented group-exchange kex ("kex-gex") + * implemented RSA/DSA private key auth +2003-09-13: + * fixed inflate_long and deflate_long to handle negatives, even though + they're never used in the current ssh protocol +2003-09-14: + * fixed session_id handling: re-keying works now + * added the ability for a Channel to have a fileno() for select/poll + purposes, although this will cause worse window performance if the + client app isn't careful +2003-09-16: version "bulbasaur" + * fixed pipe (fileno) method to be nonblocking and it seems to work now + * fixed silly bug that caused large blocks to be truncated +2003-10-08: + * patch to fix Channel.invoke_subsystem and add Channel.exec_command + [vaclav dvorak] + * patch to add Channel.sendall [vaclav dvorak] + * patch to add Channel.shutdown [vaclav dvorak] + * patch to add Channel.makefile and a ChannelFile class which emulates + a python file object [vaclav dvorak] +2003-10-26: + * thread creation no longer happens during construction -- use the new + method "start_client(event)" to get things rolling + * re-keying now takes place after 1GB of data or 1 billion packets + (these limits can be easily changed per-session if needed) +2003-11-06: + * added a demo server and host key +2003-11-09: + * lots of changes to server mode + * ChannelFile supports universal newline mode; fixed readline + * fixed a bug with parsing the remote banner +2003-11-10: version "charmander" + * renamed SSHException -> SecshException + * cleaned up server mode and the demo server + +*** for all subsequent changes, please see 'tla changelog'. @@ -1,6 +1,6 @@ # releases: # aerodactyl (13sep03) -# bulbasaur +# bulbasaur (18sep03) # charmander (10nov03) RELEASE=charmander @@ -8,8 +8,11 @@ RELEASE=charmander release: python ./setup.py sdist --formats=zip +docs: + epydoc -o docs/ paramiko + # places where the version number is stored: # # setup.py -# secsh.py +# __init__.py # README @@ -15,19 +15,6 @@ SSHOutputStream --> ssh2 chan --> ssh2 transport --> SOS [no thread] exported API... -from BaseTransport: - start_client - start_server - add_server_key - get_server_key - close - get_remote_server_key -* is_active - open_session - open_channel - renegotiate_keys - check_channel_request - from Transport: * is_authenticated auth_key @@ -130,49 +130,6 @@ are still running (and you'll have to kill -9 from another shell window). [fixme: add info about server mode] -*** CHANGELOG - -2003-08-24: - * implemented the other hashes: all 4 from the draft are working now - * added 'aes128-cbc' and '3des-cbc' cipher support - * fixed channel eof/close semantics -2003-09-12: version "aerodactyl" - * implemented group-exchange kex ("kex-gex") - * implemented RSA/DSA private key auth -2003-09-13: - * fixed inflate_long and deflate_long to handle negatives, even though - they're never used in the current ssh protocol -2003-09-14: - * fixed session_id handling: re-keying works now - * added the ability for a Channel to have a fileno() for select/poll - purposes, although this will cause worse window performance if the - client app isn't careful -2003-09-16: version "bulbasaur" - * fixed pipe (fileno) method to be nonblocking and it seems to work now - * fixed silly bug that caused large blocks to be truncated -2003-10-08: - * patch to fix Channel.invoke_subsystem and add Channel.exec_command - [vaclav dvorak] - * patch to add Channel.sendall [vaclav dvorak] - * patch to add Channel.shutdown [vaclav dvorak] - * patch to add Channel.makefile and a ChannelFile class which emulates - a python file object [vaclav dvorak] -2003-10-26: - * thread creation no longer happens during construction -- use the new - method "start_client(event)" to get things rolling - * re-keying now takes place after 1GB of data or 1 billion packets - (these limits can be easily changed per-session if needed) -2003-11-06: - * added a demo server and host key -2003-11-09: - * lots of changes to server mode - * ChannelFile supports universal newline mode; fixed readline - * fixed a bug with parsing the remote banner -2003-11-10: version "charmander" - * renamed SSHException -> SecshException - * cleaned up server mode and the demo server - - *** MISSING LINKS * ctr forms of ciphers are missing (blowfish-ctr, aes128-ctr, aes256-ctr) @@ -66,7 +66,6 @@ except Exception, e: try: event = threading.Event() t = paramiko.Transport(sock) - t.ultra_debug = 0 t.start_client(event) # print repr(t) event.wait(15) diff --git a/demo_host_key b/demo_rsa_key index f50e9c53..f50e9c53 100644 --- a/demo_host_key +++ b/demo_rsa_key diff --git a/demo_server.py b/demo_server.py index e04c8027..7fd25ad3 100755 --- a/demo_server.py +++ b/demo_server.py @@ -13,10 +13,11 @@ if len(l.handlers) == 0: l.addHandler(lh) #host_key = paramiko.RSAKey() -#host_key.read_private_key_file('demo_host_key') +#host_key.read_private_key_file('demo_rsa_key') host_key = paramiko.DSSKey() host_key.read_private_key_file('demo_dss_key') + print 'Read key: ' + paramiko.hexify(host_key.get_fingerprint()) diff --git a/demo_simple.py b/demo_simple.py new file mode 100755 index 00000000..0bd877c2 --- /dev/null +++ b/demo_simple.py @@ -0,0 +1,136 @@ +#!/usr/bin/python + +import sys, os, base64, getpass, socket, logging, traceback, termios, tty, select +import paramiko + + +##### utility functions + +def load_host_keys(): + filename = os.environ['HOME'] + '/.ssh/known_hosts' + keys = {} + try: + f = open(filename, 'r') + except Exception, e: + print '*** Unable to open host keys file (%s)' % filename + return + for line in f: + keylist = line.split(' ') + if len(keylist) != 3: + continue + hostlist, keytype, key = keylist + hosts = hostlist.split(',') + for host in hosts: + if not keys.has_key(host): + keys[host] = {} + keys[host][keytype] = base64.decodestring(key) + f.close() + return keys + + +# setup logging +l = logging.getLogger("paramiko") +l.setLevel(logging.DEBUG) +if len(l.handlers) == 0: + f = open('demo.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) + +# get hostname +username = '' +if len(sys.argv) > 1: + hostname = sys.argv[1] + if hostname.find('@') >= 0: + username, hostname = hostname.split('@') +else: + hostname = raw_input('Hostname: ') +if len(hostname) == 0: + print '*** Hostname required.' + sys.exit(1) +port = 22 +if hostname.find(':') >= 0: + hostname, portstr = hostname.split(':') + port = int(portstr) + + +# get username +if username == '': + default_username = getpass.getuser() + username = raw_input('Username [%s]: ' % default_username) + if len(username) == 0: + username = default_username +password = getpass.getpass('Password for %s@%s: ' % (username, hostname)) + + +# get host key, if we know one +hostkeytype = None +hostkey = None +hkeys = load_host_keys() +if hkeys.has_key(hostname): + hostkeytype = hkeys[hostname].keys()[0] + hostkey = hkeys[hostname][hostkeytype] + print 'Using host key of type %s' % hostkeytype + + +# now connect +try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((hostname, port)) +except Exception, e: + print '*** Connect failed: ' + str(e) + traceback.print_exc() + sys.exit(1) + + +# finally, use paramiko Transport to negotiate SSH2 across the connection +try: + t = paramiko.Transport(sock) + t.connect(username=username, password=password, hostkeytype=hostkeytype, hostkey=hostkey) + chan = t.open_session() + chan.get_pty() + chan.invoke_shell() + print '*** Here we go!' + print + + try: + oldtty = termios.tcgetattr(sys.stdin) + tty.setraw(sys.stdin.fileno()) + tty.setcbreak(sys.stdin.fileno()) + chan.settimeout(0.0) + + while 1: + r, w, e = select.select([chan, sys.stdin], [], []) + if chan in r: + try: + x = chan.recv(1024) + if len(x) == 0: + print '\r\n*** EOF\r\n', + break + sys.stdout.write(x) + sys.stdout.flush() + except socket.timeout: + pass + if sys.stdin in r: + # FIXME: reading 1 byte at a time is incredibly dumb. + x = sys.stdin.read(1) + if len(x) == 0: + print + print '*** Bye.\r\n', + break + chan.send(x) + + finally: + termios.tcsetattr(sys.stdin, termios.TCSADRAIN, oldtty) + + chan.close() + t.close() + +except Exception, e: + print '*** Caught exception: ' + str(e.__class__) + ': ' + str(e) + traceback.print_exc() + try: + t.close() + except: + pass + sys.exit(1) diff --git a/paramiko.py b/paramiko.py deleted file mode 100644 index cc5fbfaa..00000000 --- a/paramiko.py +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/python - -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.') - -class SSHException(Exception): - pass - - -from auth_transport import Transport -from channel import Channel -from rsakey import RSAKey -from dsskey import DSSKey - -from util import hexify - - -__author__ = "Robey Pointer <robey@lag.net>" -__date__ = "10 Nov 2003" -__version__ = "0.1-charmander" -__credits__ = "Huzzah!" - diff --git a/paramiko/__init__.py b/paramiko/__init__.py index 0e96a92a..81d41edb 100644 --- a/paramiko/__init__.py +++ b/paramiko/__init__.py @@ -11,10 +11,35 @@ __version__ = "0.1-charmander" __credits__ = "Huzzah!" -from auth_transport import Transport -from channel import Channel -from rsakey import RSAKey -from dsskey import DSSKey -from util import hexify +import ssh_exception, transport, auth_transport, channel, rsakey, dsskey, util -#__all__ = [ 'Transport', 'Channel', 'RSAKey', 'DSSKey', 'hexify' ] +class SSHException (ssh_exception.SSHException): + pass + +class Transport (auth_transport.Transport): + """ + An SSH Transport attaches to a stream (usually a socket), negotiates an + encrypted session, authenticates, and then creates stream tunnels, called + L{Channel}s, across the session. Multiple channels can be multiplexed + across a single session (and often are, in the case of port forwardings). + """ + pass + +class Channel (channel.Channel): + """ + A secure tunnel across an SSH L{Transport}. A Channel is meant to behave + like a socket, and has an API that should be indistinguishable from the + python socket API. + """ + pass + +class RSAKey (rsakey.RSAKey): + pass + +class DSSKey (dsskey.DSSKey): + pass + + +__all__ = [ 'Transport', 'Channel', 'RSAKey', 'DSSKey', 'transport', + 'auth_transport', 'channel', 'rsakey', 'ddskey', 'util', + 'SSHException' ] diff --git a/paramiko/auth_transport.py b/paramiko/auth_transport.py index c39c4630..34f11744 100644 --- a/paramiko/auth_transport.py +++ b/paramiko/auth_transport.py @@ -1,18 +1,18 @@ #!/usr/bin/python from transport import BaseTransport -from transport import MSG_SERVICE_REQUEST, MSG_SERVICE_ACCEPT, MSG_USERAUTH_REQUEST, MSG_USERAUTH_FAILURE, \ - MSG_USERAUTH_SUCCESS, MSG_USERAUTH_BANNER +from transport import _MSG_SERVICE_REQUEST, _MSG_SERVICE_ACCEPT, _MSG_USERAUTH_REQUEST, _MSG_USERAUTH_FAILURE, \ + _MSG_USERAUTH_SUCCESS, _MSG_USERAUTH_BANNER from message import Message from ssh_exception import SSHException from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL -DISCONNECT_SERVICE_NOT_AVAILABLE, DISCONNECT_AUTH_CANCELLED_BY_USER, \ - DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE = 7, 13, 14 +_DISCONNECT_SERVICE_NOT_AVAILABLE, _DISCONNECT_AUTH_CANCELLED_BY_USER, \ + _DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE = 7, 13, 14 -class Transport(BaseTransport): +class Transport (BaseTransport): "BaseTransport with the auth framework hooked up" AUTH_SUCCESSFUL, AUTH_PARTIALLY_SUCCESSFUL, AUTH_FAILED = range(3) @@ -55,7 +55,7 @@ class Transport(BaseTransport): def _request_auth(self): m = Message() - m.add_byte(chr(MSG_SERVICE_REQUEST)) + m.add_byte(chr(_MSG_SERVICE_REQUEST)) m.add_string('ssh-userauth') self._send_message(m) @@ -90,8 +90,8 @@ class Transport(BaseTransport): def disconnect_service_not_available(self): m = Message() - m.add_byte(chr(MSG_DISCONNECT)) - m.add_int(DISCONNECT_SERVICE_NOT_AVAILABLE) + m.add_byte(chr(_MSG_DISCONNECT)) + m.add_int(_DISCONNECT_SERVICE_NOT_AVAILABLE) m.add_string('Service not available') m.add_string('en') self._send_message(m) @@ -99,8 +99,8 @@ class Transport(BaseTransport): def disconnect_no_more_auth(self): m = Message() - m.add_byte(chr(MSG_DISCONNECT)) - m.add_int(DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE) + m.add_byte(chr(_MSG_DISCONNECT)) + m.add_int(_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE) m.add_string('No more auth methods available') m.add_string('en') self._send_message(m) @@ -111,7 +111,7 @@ class Transport(BaseTransport): if self.server_mode and (service == 'ssh-userauth'): # accepted m = Message() - m.add_byte(chr(MSG_SERVICE_ACCEPT)) + m.add_byte(chr(_MSG_SERVICE_ACCEPT)) m.add_string(service) self._send_message(m) return @@ -123,7 +123,7 @@ class Transport(BaseTransport): if service == 'ssh-userauth': self._log(DEBUG, 'userauth is OK') m = Message() - m.add_byte(chr(MSG_USERAUTH_REQUEST)) + m.add_byte(chr(_MSG_USERAUTH_REQUEST)) m.add_string(self.username) m.add_string('ssh-connection') m.add_string(self.auth_method) @@ -161,7 +161,7 @@ class Transport(BaseTransport): if not self.server_mode: # er, uh... what? m = Message() - m.add_byte(chr(MSG_USERAUTH_FAILURE)) + m.add_byte(chr(_MSG_USERAUTH_FAILURE)) m.add_string('none') m.add_boolean(0) self._send_message(m) @@ -202,11 +202,11 @@ class Transport(BaseTransport): m = Message() if result == self.AUTH_SUCCESSFUL: self._log(DEBUG, 'Auth granted.') - m.add_byte(chr(MSG_USERAUTH_SUCCESS)) + m.add_byte(chr(_MSG_USERAUTH_SUCCESS)) self.auth_complete = 1 else: self._log(DEBUG, 'Auth rejected.') - m.add_byte(chr(MSG_USERAUTH_FAILURE)) + m.add_byte(chr(_MSG_USERAUTH_FAILURE)) m.add_string(self.get_allowed_auths(username)) if result == self.AUTH_PARTIALLY_SUCCESSFUL: m.add_boolean(1) @@ -245,11 +245,11 @@ class Transport(BaseTransport): _handler_table = BaseTransport._handler_table.copy() _handler_table.update({ - MSG_SERVICE_REQUEST: parse_service_request, - MSG_SERVICE_ACCEPT: parse_service_accept, - MSG_USERAUTH_REQUEST: parse_userauth_request, - MSG_USERAUTH_SUCCESS: parse_userauth_success, - MSG_USERAUTH_FAILURE: parse_userauth_failure, - MSG_USERAUTH_BANNER: parse_userauth_banner, + _MSG_SERVICE_REQUEST: parse_service_request, + _MSG_SERVICE_ACCEPT: parse_service_accept, + _MSG_USERAUTH_REQUEST: parse_userauth_request, + _MSG_USERAUTH_SUCCESS: parse_userauth_success, + _MSG_USERAUTH_FAILURE: parse_userauth_failure, + _MSG_USERAUTH_BANNER: parse_userauth_banner, }) diff --git a/paramiko/channel.py b/paramiko/channel.py index 2ac0866b..78d8ef2c 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -1,14 +1,14 @@ from message import Message from ssh_exception import SSHException -from transport import MSG_CHANNEL_REQUEST, MSG_CHANNEL_CLOSE, MSG_CHANNEL_WINDOW_ADJUST, MSG_CHANNEL_DATA, \ - MSG_CHANNEL_EOF, MSG_CHANNEL_SUCCESS, MSG_CHANNEL_FAILURE +from transport import _MSG_CHANNEL_REQUEST, _MSG_CHANNEL_CLOSE, _MSG_CHANNEL_WINDOW_ADJUST, _MSG_CHANNEL_DATA, \ + _MSG_CHANNEL_EOF, _MSG_CHANNEL_SUCCESS, _MSG_CHANNEL_FAILURE import time, threading, logging, socket, os from logging import DEBUG # this is ugly, and won't work on windows -def set_nonblocking(fd): +def _set_nonblocking(fd): import fcntl fcntl.fcntl(fd, fcntl.F_SETFL, os.O_NONBLOCK) @@ -17,7 +17,7 @@ class Channel(object): """ Abstraction for an SSH2 channel. """ - + def __init__(self, chanid): self.chanid = chanid self.transport = None @@ -35,6 +35,11 @@ class Channel(object): self.pipe_rfd = self.pipe_wfd = None def __repr__(self): + """ + Returns a string representation of this object, for debugging. + + @rtype: string + """ out = '<paramiko.Channel %d' % self.chanid if self.closed: out += ' (closed)' @@ -50,146 +55,23 @@ class Channel(object): out += '>' return out - def _set_transport(self, transport): - self.transport = transport - - def _log(self, level, msg): - self.logger.log(level, msg) - - def set_window(self, window_size, max_packet_size): - self.in_window_size = window_size - self.in_max_packet_size = max_packet_size - # threshold of bytes we receive before we bother to send a window update - self.in_window_threshold = window_size // 10 - self.in_window_sofar = 0 - - def set_remote_channel(self, chanid, window_size, max_packet_size): - self.remote_chanid = chanid - self.out_window_size = window_size - self.out_max_packet_size = max_packet_size - self.active = 1 - - def request_success(self, m): - self._log(DEBUG, 'Sesch channel %d request ok' % self.chanid) - return - - def request_failed(self, m): - self.close() - - def feed(self, m): - s = m.get_string() - try: - self.lock.acquire() - self._log(DEBUG, 'fed %d bytes' % len(s)) - if self.pipe_wfd != None: - self.feed_pipe(s) - else: - self.in_buffer += s - self.in_buffer_cv.notifyAll() - self._log(DEBUG, '(out from feed)') - finally: - self.lock.release() - - def window_adjust(self, m): - nbytes = m.get_int() - try: - self.lock.acquire() - self._log(DEBUG, 'window up %d' % nbytes) - self.out_window_size += nbytes - self.out_buffer_cv.notifyAll() - finally: - self.lock.release() - - def check_pty_request(self, term, width, height, pixelwidth, pixelheight, modes): - "override me! return True if a pty of the given dimensions (for shell access, usually) can be provided" - return False - - def check_shell_request(self): - "override me! return True if shell access will be provided" - return False - - def check_subsystem_request(self, name): - "override me! return True if the given subsystem can be provided" - return False - - def check_window_change_request(self, width, height, pixelwidth, pixelheight): - "override me! return True if the pty was resized" - return False - - def handle_request(self, m): - key = m.get_string() - want_reply = m.get_boolean() - ok = False - if key == 'exit-status': - self.exit_status = m.get_int() - ok = True - elif key == 'xon-xoff': - # ignore - ok = True - elif key == 'pty-req': - term = m.get_string() - width = m.get_int() - height = m.get_int() - pixelwidth = m.get_int() - pixelheight = m.get_int() - modes = m.get_string() - ok = self.check_pty_request(term, width, height, pixelwidth, pixelheight, modes) - elif key == 'shell': - ok = self.check_shell_request() - elif key == 'subsystem': - name = m.get_string() - ok = self.check_subsystem_request(name) - elif key == 'window-change': - width = m.get_int() - height = m.get_int() - pixelwidth = m.get_int() - pixelheight = m.get_int() - ok = self.check_window_change_request(width, height, pixelwidth, pixelheight) - else: - self._log(DEBUG, 'Unhandled channel request "%s"' % key) - ok = False - if want_reply: - m = Message() - if ok: - m.add_byte(chr(MSG_CHANNEL_SUCCESS)) - else: - m.add_byte(chr(MSG_CHANNEL_FAILURE)) - m.add_int(self.remote_chanid) - self.transport._send_message(m) - - def handle_eof(self, m): - try: - self.lock.acquire() - if not self.eof_received: - self.eof_received = 1 - self.in_buffer_cv.notifyAll() - if self.pipe_wfd != None: - os.close(self.pipe_wfd) - self.pipe_wfd = None - finally: - self.lock.release() - self._log(DEBUG, 'EOF received') - - def handle_close(self, m): - self.close() - try: - self.lock.acquire() - self.in_buffer_cv.notifyAll() - self.out_buffer_cv.notifyAll() - if self.pipe_wfd != None: - os.close(self.pipe_wfd) - self.pipe_wfd = None - finally: - self.lock.release() - - - # API for external use - def get_pty(self, term='vt100', width=80, height=24): + """ + Request a pseudo-terminal from the server. This is usually used right + after creating a client channel, to ask the server to provide some + basic terminal semantics for the next command you execute. + + @param term: the terminal type to emulate (for example, C{'vt100'}). + @type term: string + @param width: width (in characters) of the terminal screen + @type width: int + @param height: height (in characters) of the terminal screen + @type height: int + """ if self.closed or self.eof_received or self.eof_sent or not self.active: raise SSHException('Channel is not open') m = Message() - m.add_byte(chr(MSG_CHANNEL_REQUEST)) + m.add_byte(chr(_MSG_CHANNEL_REQUEST)) m.add_int(self.remote_chanid) m.add_string('pty-req') m.add_boolean(0) @@ -205,7 +87,7 @@ class Channel(object): if self.closed or self.eof_received or self.eof_sent or not self.active: raise SSHException('Channel is not open') m = Message() - m.add_byte(chr(MSG_CHANNEL_REQUEST)) + m.add_byte(chr(_MSG_CHANNEL_REQUEST)) m.add_int(self.remote_chanid) m.add_string('shell') m.add_boolean(1) @@ -215,7 +97,7 @@ class Channel(object): if self.closed or self.eof_received or self.eof_sent or not self.active: raise SSHException('Channel is not open') m = Message() - m.add_byte(chr(MSG_CHANNEL_REQUEST)) + m.add_byte(chr(_MSG_CHANNEL_REQUEST)) m.add_int(self.remote_chanid) m.add_string('exec') m.add_boolean(1) @@ -226,7 +108,7 @@ class Channel(object): if self.closed or self.eof_received or self.eof_sent or not self.active: raise SSHException('Channel is not open') m = Message() - m.add_byte(chr(MSG_CHANNEL_REQUEST)) + m.add_byte(chr(_MSG_CHANNEL_REQUEST)) m.add_int(self.remote_chanid) m.add_string('subsystem') m.add_boolean(1) @@ -234,10 +116,19 @@ class Channel(object): self.transport._send_message(m) def resize_pty(self, width=80, height=24): + """ + Resize the pseudo-terminal. This can be used to change the width and + height of the terminal emulation created in a previous L{get_pty} call. + + @param width: new width (in characters) of the terminal screen + @type width: int + @param height: new height (in characters) of the terminal screen + @type height: int + """ if self.closed or self.eof_received or self.eof_sent or not self.active: raise SSHException('Channel is not open') m = Message() - m.add_byte(chr(MSG_CHANNEL_REQUEST)) + m.add_byte(chr(_MSG_CHANNEL_REQUEST)) m.add_int(self.remote_chanid) m.add_string('window-change') m.add_boolean(0) @@ -247,48 +138,103 @@ class Channel(object): self.transport._send_message(m) def get_transport(self): + """ + Return the L{Transport} associated with this channel. + + @return: the L{Transport} that was used to create this channel. + @rtype: L{Transport} + """ return self.transport def set_name(self, name): + """ + Set a name for this channel. Currently it's only used to set the name + of the log level used for debugging. The name can be fetched with the + L{get_name} method. + + @param name: new channel name + @type name: string + """ self.name = name self.logger = logging.getLogger('paramiko.chan.' + name) def get_name(self): - return self.name + """ + Get the name of this channel that was previously set by L{set_name}. - def send_eof(self): - if self.eof_sent: - return - m = Message() - m.add_byte(chr(MSG_CHANNEL_EOF)) - m.add_int(self.remote_chanid) - self.transport._send_message(m) - self.eof_sent = 1 - self._log(DEBUG, 'EOF sent') - return + @return: the name of this channel + @rtype: string + """ + return self.name + + ### socket API - # socket equivalency methods... def settimeout(self, timeout): + """ + Set a timeout on blocking read/write operations. The C{timeout} + argument can be a nonnegative float expressing seconds, or C{None}. If + a float is given, subsequent channel read/write operations will raise + a timeout exception if the timeout period value has elapsed before the + operation has completed. Setting a timeout of C{None} disables + timeouts on socket operations. + + C{chan.settimeout(0.0)} is equivalent to C{chan.setblocking(0)}; + C{chan.settimeout(None)} is equivalent to C{chan.setblocking(1)}. + + @param timeout: seconds to wait for a pending read/write operation + before raising C{socket.timeout}, or C{None} for no timeout. + @type timeout: float + """ self.timeout = timeout def gettimeout(self): + """ + Returns the timeout in seconds (as a float) associated with socket + operations, or C{None} if no timeout is set. This reflects the last + call to L{setblocking} or L{settimeout}. + + @return: timeout in seconds, or C{None}. + @rtype: float + """ return self.timeout def setblocking(self, blocking): + """ + Set blocking or non-blocking mode of the channel: if C{blocking} is 0, + the channel is set to non-blocking mode; otherwise it's set to blocking + mode. Initially all channels are in blocking mode. + + In non-blocking mode, if a L{recv} call doesn't find any data, or if a + L{send} call can't immediately dispose of the data, an error exception + is raised. In blocking mode, the calls block until they can proceed. + + C{chan.setblocking(0)} is equivalent to C{chan.settimeout(0)}; + C{chan.setblocking(1)} is equivalent to C{chan.settimeout(None)}. + + @param blocking: 0 to set non-blocking mode; non-0 to set blocking + mode. + @type blocking: int + """ if blocking: self.settimeout(None) else: self.settimeout(0.0) def close(self): + """ + Close the channel. All future read/write operations on the channel + will fail. The remote end will receive no more data (after queued data + is flushed). Channels are automatically closed when they are garbage- + collected, or when their L{Transport} is closed. + """ try: self.lock.acquire() if self.active and not self.closed: - self.send_eof() + self._send_eof() m = Message() - m.add_byte(chr(MSG_CHANNEL_CLOSE)) + m.add_byte(chr(_MSG_CHANNEL_CLOSE)) m.add_int(self.remote_chanid) self.transport._send_message(m) self.closed = 1 @@ -297,22 +243,44 @@ class Channel(object): self.lock.release() def recv_ready(self): - "doesn't work if you've called fileno()" + """ + Returns true if data is ready to be read from this channel. + + @return: C{True} if a L{recv} call on this channel would immediately + return at least one byte; C{False} otherwise. + @rtype: boolean + + @note: This method doesn't work if you've called L{fileno}. + """ try: self.lock.acquire() if len(self.in_buffer) == 0: - return 0 - return 1 + return False + return True finally: self.lock.release() def recv(self, nbytes): + """ + Receive data from the channel. The return value is a string + representing the data received. The maximum amount of data to be + received at once is specified by C{nbytes}. If a string of length zero + is returned, the channel stream has closed. + + @param nbytes: maximum number of bytes to read. + @type nbytes: int + @return: data + @rtype: string + + @raise socket.timeout: if no data is ready before the timeout set by + L{settimeout}. + """ out = '' try: self.lock.acquire() if self.pipe_rfd != None: # use the pipe - return self.read_pipe(nbytes) + return self._read_pipe(nbytes) if len(self.in_buffer) == 0: if self.closed or self.eof_received: return out @@ -335,12 +303,27 @@ class Channel(object): else: out = self.in_buffer[:nbytes] self.in_buffer = self.in_buffer[nbytes:] - self.check_add_window(len(out)) + self._check_add_window(len(out)) finally: self.lock.release() return out def send(self, s): + """ + Send data to the channel. Returns the number of bytes sent, or 0 if + the channel stream is closed. Applications are responsible for + checking that all data has been sent: if only some of the data was + transmitted, the application needs to attempt delivery of the remaining + data. + + @param s: data to send. + @type s: string + @return: number of bytes actually sent. + @rtype: int + + @raise socket.timeout: if no data could be sent before the timeout set + by L{settimeout}. + """ size = 0 if self.closed or self.eof_sent: return size @@ -368,7 +351,7 @@ class Channel(object): if self.out_max_packet_size < size: size = self.out_max_packet_size m = Message() - m.add_byte(chr(MSG_CHANNEL_DATA)) + m.add_byte(chr(_MSG_CHANNEL_DATA)) m.add_int(self.remote_chanid) m.add_string(s[:size]) self.transport._send_message(m) @@ -378,6 +361,23 @@ class Channel(object): return size def sendall(self, s): + """ + Send data to the channel, without allowing partial results. Unlike + L{send}, this method continues to send data from the given string until + either all data has been sent or an error occurs. Nothing is returned. + + @param s: data to send. + @type s: string + + @raise socket.timeout: if sending stalled for longer than the timeout + set by L{settimeout}. + @raise socket.error: if an error occured before the entire string was + sent. + + @note: If the channel is closed while only part of the data hase been + sent, there is no way to determine how much data (if any) was sent. + This is irritating, but identically follows python's API. + """ while s: if self.closed: # this doesn't seem useful, but it is the documented behavior of Socket @@ -387,20 +387,37 @@ class Channel(object): return None def makefile(self, *params): + """ + Return a file-like object associated with this channel, without the + non-portable side effects of L{fileno}. The optional C{mode} and + C{bufsize} arguments are interpreted the same way as by the built-in + C{file()} function in python. + + @return: object which can be used for python file I/O. + @rtype: L{ChannelFile} + """ return ChannelFile(*([self] + list(params))) def fileno(self): """ - returns an OS-level fd which can be used for polling and reading (but - NOT for writing). this is primarily to allow python's \"select\" module - to work. the first time this function is called, a pipe is created to - simulate real OS-level fd behavior. because of this, two actual fds are - created: one to return and one to feed. this may be inefficient if you - plan to use many fds. + Returns an OS-level file descriptor which can be used for polling and + reading (but I{not} for writing). This is primaily to allow python's + C{select} module to work. + + The first time C{fileno} is called on a channel, a pipe is created to + simulate real OS-level file descriptor (FD) behavior. Because of this, + two actual FDs are created -- this may be inefficient if you plan to + use many channels. - the channel's receive window will be updated as data comes in, not as - you read it, so if you fail to poll the channel often enough, it may - block ALL channels across the transport. + @return: a small integer file descriptor + @rtype: int + + @warning: This method causes several aspects of the channel to change + behavior. It is always more efficient to avoid using this method. + + @bug: This does not work on Windows. The problem is that pipes are + used to simulate an open FD, but I haven't figured out how to make + pipes enter non-blocking mode on Windows yet. """ try: self.lock.acquire() @@ -408,27 +425,189 @@ class Channel(object): return self.pipe_rfd # create the pipe and feed in any existing data self.pipe_rfd, self.pipe_wfd = os.pipe() - set_nonblocking(self.pipe_wfd) - set_nonblocking(self.pipe_rfd) + _set_nonblocking(self.pipe_wfd) + _set_nonblocking(self.pipe_rfd) if len(self.in_buffer) > 0: x = self.in_buffer self.in_buffer = '' - self.feed_pipe(x) + self._feed_pipe(x) return self.pipe_rfd finally: self.lock.release() def shutdown(self, how): + """ + Shut down one or both halves of the connection. If C{how} is 0, + further receives are disallowed. If C{how} is 1, further sends + are disallowed. If C{how} is 2, further sends and receives are + disallowed. This closes the stream in one or both directions. + + @param how: 0 (stop receiving), 1 (stop sending), or 2 (stop + receiving and sending). + @type how: int + """ if (how == 0) or (how == 2): # feign "read" shutdown self.eof_received = 1 if (how == 1) or (how == 2): - self.send_eof() + self._send_eof() + + + ### overrides + + + def check_pty_request(self, term, width, height, pixelwidth, pixelheight, modes): + "override me! return True if a pty of the given dimensions (for shell access, usually) can be provided" + return False + + def check_shell_request(self): + "override me! return True if shell access will be provided" + return False + def check_subsystem_request(self, name): + "override me! return True if the given subsystem can be provided" + return False - # internal use... + def check_window_change_request(self, width, height, pixelwidth, pixelheight): + "override me! return True if the pty was resized" + return False + + + ### calls from Transport + + + def _set_transport(self, transport): + self.transport = transport + + def _set_window(self, window_size, max_packet_size): + self.in_window_size = window_size + self.in_max_packet_size = max_packet_size + # threshold of bytes we receive before we bother to send a window update + self.in_window_threshold = window_size // 10 + self.in_window_sofar = 0 + + def _set_remote_channel(self, chanid, window_size, max_packet_size): + self.remote_chanid = chanid + self.out_window_size = window_size + self.out_max_packet_size = max_packet_size + self.active = 1 + + def _request_success(self, m): + self._log(DEBUG, 'Sesch channel %d request ok' % self.chanid) + return + + def _request_failed(self, m): + self.close() + + def _feed(self, m): + s = m.get_string() + try: + self.lock.acquire() + self._log(DEBUG, 'fed %d bytes' % len(s)) + if self.pipe_wfd != None: + self._feed_pipe(s) + else: + self.in_buffer += s + self.in_buffer_cv.notifyAll() + self._log(DEBUG, '(out from feed)') + finally: + self.lock.release() + + def _window_adjust(self, m): + nbytes = m.get_int() + try: + self.lock.acquire() + self._log(DEBUG, 'window up %d' % nbytes) + self.out_window_size += nbytes + self.out_buffer_cv.notifyAll() + finally: + self.lock.release() + + def _handle_request(self, m): + key = m.get_string() + want_reply = m.get_boolean() + ok = False + if key == 'exit-status': + self.exit_status = m.get_int() + ok = True + elif key == 'xon-xoff': + # ignore + ok = True + elif key == 'pty-req': + term = m.get_string() + width = m.get_int() + height = m.get_int() + pixelwidth = m.get_int() + pixelheight = m.get_int() + modes = m.get_string() + ok = self.check_pty_request(term, width, height, pixelwidth, pixelheight, modes) + elif key == 'shell': + ok = self.check_shell_request() + elif key == 'subsystem': + name = m.get_string() + ok = self.check_subsystem_request(name) + elif key == 'window-change': + width = m.get_int() + height = m.get_int() + pixelwidth = m.get_int() + pixelheight = m.get_int() + ok = self.check_window_change_request(width, height, pixelwidth, pixelheight) + else: + self._log(DEBUG, 'Unhandled channel request "%s"' % key) + ok = False + if want_reply: + m = Message() + if ok: + m.add_byte(chr(_MSG_CHANNEL_SUCCESS)) + else: + m.add_byte(chr(_MSG_CHANNEL_FAILURE)) + m.add_int(self.remote_chanid) + self.transport._send_message(m) + + def _handle_eof(self, m): + try: + self.lock.acquire() + if not self.eof_received: + self.eof_received = 1 + self.in_buffer_cv.notifyAll() + if self.pipe_wfd != None: + os.close(self.pipe_wfd) + self.pipe_wfd = None + finally: + self.lock.release() + self._log(DEBUG, 'EOF received') + + def _handle_close(self, m): + self.close() + try: + self.lock.acquire() + self.in_buffer_cv.notifyAll() + self.out_buffer_cv.notifyAll() + if self.pipe_wfd != None: + os.close(self.pipe_wfd) + self.pipe_wfd = None + finally: + self.lock.release() + + + ### internals... + + + def _log(self, level, msg): + self.logger.log(level, msg) + + def _send_eof(self): + if self.eof_sent: + return + m = Message() + m.add_byte(chr(_MSG_CHANNEL_EOF)) + m.add_int(self.remote_chanid) + self.transport._send_message(m) + self.eof_sent = 1 + self._log(DEBUG, 'EOF sent') + return - def feed_pipe(self, data): + def _feed_pipe(self, data): "you are already holding the lock" if len(self.in_buffer) > 0: self.in_buffer += data @@ -439,7 +618,7 @@ class Channel(object): # at least on linux, this will never happen, as the writes are # considered atomic... but just in case. self.in_buffer = data[n:] - self.check_add_window(n) + self._check_add_window(n) self.in_buffer_cv.notifyAll() return except OSError, e: @@ -451,7 +630,7 @@ class Channel(object): try: os.write(self.pipe_wfd, x) self.in_buffer = data - self.check_add_window(1) + self._check_add_window(1) self.in_buffer_cv.notifyAll() return except OSError, e: @@ -460,12 +639,12 @@ class Channel(object): self.in_buffer = data self.in_buffer_cv.notifyAll() - def read_pipe(self, nbytes): + def _read_pipe(self, nbytes): "you are already holding the lock" try: x = os.read(self.pipe_rfd, nbytes) if len(x) > 0: - self.push_pipe(len(x)) + self._push_pipe(len(x)) return x except OSError, e: pass @@ -487,13 +666,13 @@ class Channel(object): try: x = os.read(self.pipe_rfd, nbytes) if len(x) > 0: - self.push_pipe(len(x)) + self._push_pipe(len(x)) return x except OSError, e: pass pass - def push_pipe(self, nbytes): + def _push_pipe(self, nbytes): # successfully read N bytes from the pipe, now re-feed the pipe if necessary # (assumption: the pipe can hold as many bytes as were read out) if len(self.in_buffer) == 0: @@ -512,7 +691,7 @@ class Channel(object): self.closed = 1 self.transport._unlink_channel(self.chanid) - def check_add_window(self, n): + def _check_add_window(self, n): # already holding the lock! if self.closed or self.eof_received or not self.active: return @@ -521,7 +700,7 @@ class Channel(object): if self.in_window_sofar > self.in_window_threshold: self._log(DEBUG, 'addwindow send %d' % self.in_window_sofar) m = Message() - m.add_byte(chr(MSG_CHANNEL_WINDOW_ADJUST)) + m.add_byte(chr(_MSG_CHANNEL_WINDOW_ADJUST)) m.add_int(self.remote_chanid) m.add_int(self.in_window_sofar) self.transport._send_message(m) diff --git a/paramiko/dsskey.py b/paramiko/dsskey.py index 35d8a8d1..3eca8589 100644 --- a/paramiko/dsskey.py +++ b/paramiko/dsskey.py @@ -3,16 +3,16 @@ import base64 from ssh_exception import SSHException from message import Message -from transport import MSG_USERAUTH_REQUEST +from transport import _MSG_USERAUTH_REQUEST from util import inflate_long, deflate_long from Crypto.PublicKey import DSA -from Crypto.Hash import SHA, MD5 +from Crypto.Hash import SHA from ber import BER +from pkey import PKey from util import format_binary - -class DSSKey(object): +class DSSKey (PKey): def __init__(self, msg=None): self.valid = 0 @@ -39,9 +39,6 @@ class DSSKey(object): def get_name(self): return 'ssh-dss' - def get_fingerprint(self): - return MD5.new(str(self)).digest() - def verify_ssh_sig(self, data, msg): if not self.valid: return 0 @@ -78,7 +75,6 @@ class DSSKey(object): return str(m) def read_private_key_file(self, filename): - "throws a file exception, or SSHException (on invalid key, or base64 decoding exception" # private key file contains: # DSAPrivateKey = { version = 0, p, q, g, y, x } self.valid = 0 @@ -102,7 +98,7 @@ class DSSKey(object): def sign_ssh_session(self, randpool, sid, username): m = Message() m.add_string(sid) - m.add_byte(chr(MSG_USERAUTH_REQUEST)) + m.add_byte(chr(_MSG_USERAUTH_REQUEST)) m.add_string(username) m.add_string('ssh-connection') m.add_string('publickey') diff --git a/paramiko/kex_gex.py b/paramiko/kex_gex.py index 5e02bab7..f14c7864 100644 --- a/paramiko/kex_gex.py +++ b/paramiko/kex_gex.py @@ -7,12 +7,12 @@ from message import Message from util import inflate_long, deflate_long, bit_length from ssh_exception import SSHException -from transport import MSG_NEWKEYS +from transport import _MSG_NEWKEYS from Crypto.Hash import SHA from Crypto.Util import number from logging import DEBUG -MSG_KEXDH_GEX_GROUP, MSG_KEXDH_GEX_INIT, MSG_KEXDH_GEX_REPLY, MSG_KEXDH_GEX_REQUEST = range(31, 35) +_MSG_KEXDH_GEX_GROUP, _MSG_KEXDH_GEX_INIT, _MSG_KEXDH_GEX_REPLY, _MSG_KEXDH_GEX_REQUEST = range(31, 35) class KexGex(object): @@ -27,27 +27,27 @@ class KexGex(object): def start_kex(self): if self.transport.server_mode: - self.transport._expect_packet(MSG_KEXDH_GEX_REQUEST) + self.transport._expect_packet(_MSG_KEXDH_GEX_REQUEST) return # request a bit range: we accept (min_bits) to (max_bits), but prefer # (preferred_bits). according to the spec, we shouldn't pull the # minimum up above 1024. m = Message() - m.add_byte(chr(MSG_KEXDH_GEX_REQUEST)) + m.add_byte(chr(_MSG_KEXDH_GEX_REQUEST)) m.add_int(self.min_bits) m.add_int(self.preferred_bits) m.add_int(self.max_bits) self.transport._send_message(m) - self.transport._expect_packet(MSG_KEXDH_GEX_GROUP) + self.transport._expect_packet(_MSG_KEXDH_GEX_GROUP) def parse_next(self, ptype, m): - if ptype == MSG_KEXDH_GEX_REQUEST: + if ptype == _MSG_KEXDH_GEX_REQUEST: return self._parse_kexdh_gex_request(m) - elif ptype == MSG_KEXDH_GEX_GROUP: + elif ptype == _MSG_KEXDH_GEX_GROUP: return self._parse_kexdh_gex_group(m) - elif ptype == MSG_KEXDH_GEX_INIT: + elif ptype == _MSG_KEXDH_GEX_INIT: return self._parse_kexdh_gex_init(m) - elif ptype == MSG_KEXDH_GEX_REPLY: + elif ptype == _MSG_KEXDH_GEX_REPLY: return self._parse_kexdh_gex_reply(m) raise SSHException('KexGex asked to handle packet type %d' % ptype) @@ -96,11 +96,11 @@ class KexGex(object): raise SSHException('Can\'t do server-side gex with no modulus pack') self.g, self.p = pack.get_modulus(min, preferred, max) m = Message() - m.add_byte(chr(MSG_KEXDH_GEX_GROUP)) + m.add_byte(chr(_MSG_KEXDH_GEX_GROUP)) m.add_mpint(self.p) m.add_mpint(self.g) self.transport._send_message(m) - self.transport._expect_packet(MSG_KEXDH_GEX_INIT) + self.transport._expect_packet(_MSG_KEXDH_GEX_INIT) def _parse_kexdh_gex_group(self, m): self.p = m.get_mpint() @@ -114,10 +114,10 @@ class KexGex(object): # now compute e = g^x mod p self.e = pow(self.g, self.x, self.p) m = Message() - m.add_byte(chr(MSG_KEXDH_GEX_INIT)) + m.add_byte(chr(_MSG_KEXDH_GEX_INIT)) m.add_mpint(self.e) self.transport._send_message(m) - self.transport._expect_packet(MSG_KEXDH_GEX_REPLY) + self.transport._expect_packet(_MSG_KEXDH_GEX_REPLY) def _parse_kexdh_gex_init(self, m): self.e = m.get_mpint() @@ -142,7 +142,7 @@ class KexGex(object): sig = self.transport.get_server_key().sign_ssh_data(self.transport.randpool, H) # send reply m = Message() - m.add_byte(chr(MSG_KEXDH_GEX_REPLY)) + m.add_byte(chr(_MSG_KEXDH_GEX_REPLY)) m.add_string(key) m.add_mpint(self.f) m.add_string(sig) diff --git a/paramiko/kex_group1.py b/paramiko/kex_group1.py index de1a2546..31123269 100644 --- a/paramiko/kex_group1.py +++ b/paramiko/kex_group1.py @@ -6,11 +6,11 @@ from message import Message, inflate_long from ssh_exception import SSHException -from transport import MSG_NEWKEYS +from transport import _MSG_NEWKEYS from Crypto.Hash import SHA from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL -MSG_KEXDH_INIT, MSG_KEXDH_REPLY = range(30, 32) +_MSG_KEXDH_INIT, _MSG_KEXDH_REPLY = range(30, 32) # draft-ietf-secsh-transport-09.txt, page 17 P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFFL @@ -44,20 +44,20 @@ class KexGroup1(object): if self.transport.server_mode: # compute f = g^x mod p, but don't send it yet self.f = pow(G, self.x, P) - self.transport._expect_packet(MSG_KEXDH_INIT) + self.transport._expect_packet(_MSG_KEXDH_INIT) return # compute e = g^x mod p (where g=2), and send it self.e = pow(G, self.x, P) m = Message() - m.add_byte(chr(MSG_KEXDH_INIT)) + m.add_byte(chr(_MSG_KEXDH_INIT)) m.add_mpint(self.e) self.transport._send_message(m) - self.transport._expect_packet(MSG_KEXDH_REPLY) + self.transport._expect_packet(_MSG_KEXDH_REPLY) def parse_next(self, ptype, m): - if self.transport.server_mode and (ptype == MSG_KEXDH_INIT): + if self.transport.server_mode and (ptype == _MSG_KEXDH_INIT): return self.parse_kexdh_init(m) - elif not self.transport.server_mode and (ptype == MSG_KEXDH_REPLY): + elif not self.transport.server_mode and (ptype == _MSG_KEXDH_REPLY): return self.parse_kexdh_reply(m) raise SSHException('KexGroup1 asked to handle packet type %d' % ptype) @@ -94,7 +94,7 @@ class KexGroup1(object): sig = self.transport.get_server_key().sign_ssh_data(self.transport.randpool, H) # send reply m = Message() - m.add_byte(chr(MSG_KEXDH_REPLY)) + m.add_byte(chr(_MSG_KEXDH_REPLY)) m.add_string(key) m.add_mpint(self.f) m.add_string(sig) diff --git a/paramiko/pkey.py b/paramiko/pkey.py new file mode 100644 index 00000000..c4c7599d --- /dev/null +++ b/paramiko/pkey.py @@ -0,0 +1,112 @@ + +from Crypto.Hash import MD5 +from message import Message + +class PKey (object): + """ + Base class for public keys. + """ + + def __init__(self, msg=None): + """ + Create a new instance of this public key type. If C{msg} is not + C{None}, the key's public part(s) will be filled in from the + message. + + @param msg: an optional SSH L{Message} containing a public key of this + type. + @type msg: L{Message} + """ + pass + + def __str__(self): + """ + Return a string of an SSH L{Message} made up of the public part(s) of + this key. + + @return: string representation of an SSH key message. + @rtype: string + """ + return '' + + def get_name(self): + """ + Return the name of this private key implementation. + + @return: name of this private key type, in SSH terminology (for + example, C{"ssh-rsa"}). + @rtype: string + """ + return '' + + def get_fingerprint(self): + """ + Return an MD5 fingerprint of the public part of this key. Nothing + secret is revealed. + + @return: a 16-byte string (binary) of the MD5 fingerprint, in SSH + format. + @rtype: string + """ + return MD5.new(str(self)).digest() + + def verify_ssh_sig(self, data, msg): + """ + Given a blob of data, and an SSH message representing a signature of + that data, verify that it was signed with this key. + + @param data: the data that was signed. + @type data: string + @param msg: an SSH signature message + @type msg: L{Message} + @return: C{True} if the signature verifies correctly; C{False} + otherwise. + @rtype: boolean + """ + return False + + def sign_ssh_data(self, randpool, data): + """ + Sign a blob of data with this private key, and return a string + representing an SSH signature message. + + @bug: It would be cleaner for this method to return a L{Message} + object, so it would be complementary to L{verify_ssh_sig}. FIXME. + + @param randpool: a secure random number generator. + @type randpool: L{Crypto.Util.randpool.RandomPool} + @param data: the data to sign. + @type data: string + @return: string representation of an SSH signature message. + @rtype: string + """ + return '' + + def read_private_key_file(self, filename): + """ + Read private key contents from a file into this object. + + @param filename: name of the file to read. + @type filename: string + + @raise IOError: if there was an error reading the file. + @raise SSHException: if the key file is invalid + @raise binascii.Error: on base64 decoding error + """ + pass + + def sign_ssh_session(self, randpool, sid, username): + """ + Sign an SSH authentication request. + + @bug: Same as L{sign_ssh_data} + + @param randpool: a secure random number generator. + @type randpool: L{Crypto.Util.randpool.RandomPool} + @param sid: the session ID given by the server + @type sid: string + @param username: the username to use in the authentication request + @type username: string + @return: string representation of an SSH signature message. + @rtype: string + """ diff --git a/paramiko/rsakey.py b/paramiko/rsakey.py index 74502aa2..e0935d5d 100644 --- a/paramiko/rsakey.py +++ b/paramiko/rsakey.py @@ -1,14 +1,15 @@ #!/usr/bin/python from message import Message -from transport import MSG_USERAUTH_REQUEST +from transport import _MSG_USERAUTH_REQUEST from Crypto.PublicKey import RSA from Crypto.Hash import SHA from ber import BER from util import format_binary, inflate_long, deflate_long +from pkey import PKey import base64 -class RSAKey(object): +class RSAKey (PKey): def __init__(self, msg=None): self.valid = 0 @@ -31,10 +32,7 @@ class RSAKey(object): def get_name(self): return 'ssh-rsa' - def get_fingerprint(self): - return MD5.new(str(self)).digest() - - 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, using PKCS1's \"emsa-pkcs1-v1_5\" encoding. totally bizarre. @@ -45,26 +43,25 @@ class RSAKey(object): def verify_ssh_sig(self, data, msg): if (not self.valid) or (msg.get_string() != 'ssh-rsa'): - return 0 + return False sig = inflate_long(msg.get_string(), 1) # verify the signature by SHA'ing the data and encrypting it using the # public key. some wackiness ensues where we "pkcs1imify" the 20-byte # hash into a string as long as the RSA key. - hash = inflate_long(self.pkcs1imify(SHA.new(data).digest()), 1) + hash = inflate_long(self._pkcs1imify(SHA.new(data).digest()), 1) rsa = RSA.construct((long(self.n), long(self.e))) return rsa.verify(hash, (sig,)) def sign_ssh_data(self, randpool, data): hash = SHA.new(data).digest() rsa = RSA.construct((long(self.n), long(self.e), long(self.d))) - sig = deflate_long(rsa.sign(self.pkcs1imify(hash), '')[0], 0) + sig = deflate_long(rsa.sign(self._pkcs1imify(hash), '')[0], 0) m = Message() m.add_string('ssh-rsa') m.add_string(sig) return str(m) def read_private_key_file(self, filename): - "throws a file exception, or SSHException (on invalid key), or base64 decoding exception" # private key file contains: # RSAPrivateKey = { version = 0, n, e, d, p, q, d mod p-1, d mod q-1, q**-1 mod p } self.valid = 0 @@ -72,11 +69,11 @@ class RSAKey(object): lines = f.readlines() f.close() if lines[0].strip() != '-----BEGIN RSA PRIVATE KEY-----': - raise SSHException('not a valid DSA private key file') + raise SSHException('not a valid RSA private key file') data = base64.decodestring(''.join(lines[1:-1])) keylist = BER(data).decode() if (type(keylist) != type([])) or (len(keylist) < 4) or (keylist[0] != 0): - raise SSHException('not a valid DSA private key file (bad ber encoding)') + raise SSHException('not a valid RSA private key file (bad ber encoding)') self.n = keylist[1] self.e = keylist[2] self.d = keylist[3] @@ -89,7 +86,7 @@ class RSAKey(object): def sign_ssh_session(self, randpool, sid, username): m = Message() m.add_string(sid) - m.add_byte(chr(MSG_USERAUTH_REQUEST)) + m.add_byte(chr(_MSG_USERAUTH_REQUEST)) m.add_string(username) m.add_string('ssh-connection') m.add_string('publickey') diff --git a/paramiko/transport.py b/paramiko/transport.py index f982d78f..1a42fb76 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -1,15 +1,15 @@ #!/usr/bin/python -MSG_DISCONNECT, MSG_IGNORE, MSG_UNIMPLEMENTED, MSG_DEBUG, MSG_SERVICE_REQUEST, \ - MSG_SERVICE_ACCEPT = range(1, 7) -MSG_KEXINIT, MSG_NEWKEYS = range(20, 22) -MSG_USERAUTH_REQUEST, MSG_USERAUTH_FAILURE, MSG_USERAUTH_SUCCESS, \ - MSG_USERAUTH_BANNER = range(50, 54) -MSG_USERAUTH_PK_OK = 60 -MSG_CHANNEL_OPEN, MSG_CHANNEL_OPEN_SUCCESS, MSG_CHANNEL_OPEN_FAILURE, \ - MSG_CHANNEL_WINDOW_ADJUST, MSG_CHANNEL_DATA, MSG_CHANNEL_EXTENDED_DATA, \ - MSG_CHANNEL_EOF, MSG_CHANNEL_CLOSE, MSG_CHANNEL_REQUEST, \ - MSG_CHANNEL_SUCCESS, MSG_CHANNEL_FAILURE = range(90, 101) +_MSG_DISCONNECT, _MSG_IGNORE, _MSG_UNIMPLEMENTED, _MSG_DEBUG, _MSG_SERVICE_REQUEST, \ + _MSG_SERVICE_ACCEPT = range(1, 7) +_MSG_KEXINIT, _MSG_NEWKEYS = range(20, 22) +_MSG_USERAUTH_REQUEST, _MSG_USERAUTH_FAILURE, _MSG_USERAUTH_SUCCESS, \ + _MSG_USERAUTH_BANNER = range(50, 54) +_MSG_USERAUTH_PK_OK = 60 +_MSG_CHANNEL_OPEN, _MSG_CHANNEL_OPEN_SUCCESS, _MSG_CHANNEL_OPEN_FAILURE, \ + _MSG_CHANNEL_WINDOW_ADJUST, _MSG_CHANNEL_DATA, _MSG_CHANNEL_EXTENDED_DATA, \ + _MSG_CHANNEL_EOF, _MSG_CHANNEL_CLOSE, _MSG_CHANNEL_REQUEST, \ + _MSG_CHANNEL_SUCCESS, _MSG_CHANNEL_FAILURE = range(90, 101) import sys, os, string, threading, socket, logging, struct from ssh_exception import SSHException @@ -36,7 +36,7 @@ from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL # channel request failed reasons: -CONNECTION_FAILED_CODE = { +_CONNECTION_FAILED_CODE = { 1: 'Administratively prohibited', 2: 'Connect failed', 3: 'Unknown channel type', @@ -54,29 +54,14 @@ except: randpool.randomize() - -class BaseTransport(threading.Thread): - ''' - An SSH Transport attaches to a stream (usually a socket), negotiates an - encrypted session, authenticates, and then creates stream tunnels, called - "channels", across the session. Multiple channels can be multiplexed - across a single session (and often are, in the case of port forwardings). - - Transport expects to receive a "socket-like object" to talk to the SSH - server. This means it has a method "settimeout" which sets a timeout for - read/write calls, and a method "send()" to write bytes and "recv()" to - read bytes. "recv" returns from 1 to n bytes, or 0 if the stream has been - closed. EOFError may also be raised on a closed stream. (A return value - of 0 is converted to an EOFError internally.) "send(s)" writes from 1 to - len(s) bytes, and returns the number of bytes written, or returns 0 if the - stream has been closed. As with instream, EOFError may be raised instead - of returning 0. - - FIXME: Describe events here. - ''' - - PROTO_ID = '2.0' - CLIENT_ID = 'pyssh_1.1' +class BaseTransport (threading.Thread): + """ + Handles protocol negotiation, key exchange, encryption, and the creation + of channels across an SSH session. Basically everything but authentication + is done here. + """ + _PROTO_ID = '2.0' + _CLIENT_ID = 'pyssh_1.1' preferred_ciphers = [ 'aes128-cbc', 'blowfish-cbc', 'aes256-cbc', '3des-cbc' ] preferred_macs = [ 'hmac-sha1', 'hmac-md5', 'hmac-sha1-96', 'hmac-md5-96' ] @@ -108,13 +93,34 @@ class BaseTransport(threading.Thread): OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED, OPEN_FAILED_CONNECT_FAILED, OPEN_FAILED_UNKNOWN_CHANNEL_TYPE, \ OPEN_FAILED_RESOURCE_SHORTAGE = range(1, 5) + _modulus_pack = None + def __init__(self, sock): + """ + Create a new SSH session over an existing socket, or socket-like + object. This only creates the Transport object; it doesn't begin the + SSH session yet. Use L{connect} or L{start_client} to begin a client + session, or L{start_server} to begin a server session. + + 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. + - C{recv(int)}: Reads from 1 to C{int} bytes and returns them as a + string. Returns 0 or raises C{EOFError} if the stream has been + closed. + + @param sock: a socket or socket-like object to create the session over. + @type sock: socket + """ threading.Thread.__init__(self, target=self._run) self.randpool = randpool self.sock = sock self.sock.settimeout(0.1) # negotiated crypto parameters - self.local_version = 'SSH-' + self.PROTO_ID + '-' + self.CLIENT_ID + self.local_version = 'SSH-' + self._PROTO_ID + '-' + self._CLIENT_ID self.remote_version = '' self.block_size_out = self.block_size_in = 8 self.local_mac_len = self.remote_mac_len = 0 @@ -136,7 +142,7 @@ class BaseTransport(threading.Thread): self.window_size = 65536 self.max_packet_size = 2048 self.ultra_debug = 0 - self.modulus_pack = None + self.saved_exception = None # used for noticing when to re-key: self.received_bytes = 0 self.received_packets = 0 @@ -159,6 +165,17 @@ class BaseTransport(threading.Thread): self.start() def add_server_key(self, key): + """ + Add a host key to the list of keys used for server mode. When behaving + as a server, the host key is used to sign certain packets during the + SSH2 negotiation, so that the client can trust that we are who we say + we are. Because this is used for signing, the key must contain private + key info, not just the public half. + + @param key: the host key to add, usually an L{RSAKey <rsakey.RSAKey>} or + L{DSSKey <dsskey.DSSKey>}. + @type key: L{PKey <pkey.PKey>} + """ self.server_key_dict[key.get_name()] = key def get_server_key(self): @@ -167,7 +184,7 @@ class BaseTransport(threading.Thread): except KeyError: return None - def load_server_moduli(self, filename=None): + def load_server_moduli(filename=None): """ I{(optional)} Load a file of prime moduli for use in doing group-exchange key @@ -195,26 +212,32 @@ class BaseTransport(threading.Thread): @note: This has no effect when used in client mode. """ - self.modulus_pack = ModulusPack(self.randpool) + BaseTransport._modulus_pack = ModulusPack(randpool) # places to look for the openssh "moduli" file file_list = [ '/etc/ssh/moduli', '/usr/local/etc/moduli' ] if filename is not None: file_list.insert(0, filename) for fn in file_list: try: - self.modulus_pack.read_file(fn) + BaseTransport._modulus_pack.read_file(fn) return True except IOError: pass # none succeeded - self.modulus_pack = None + BaseTransport._modulus_pack = None return False + load_server_moduli = staticmethod(load_server_moduli) def _get_modulus_pack(self): "used by KexGex to find primes for group exchange" - return self.modulus_pack + return self._modulus_pack def __repr__(self): + """ + Returns a string representation of this object, for debugging. + + @rtype: string + """ if not self.active: return '<paramiko.BaseTransport (unconnected)>' out = '<paramiko.BaseTransport' @@ -285,7 +308,7 @@ class BaseTransport(threading.Thread): chanid = self.channel_counter self.channel_counter += 1 m = Message() - m.add_byte(chr(MSG_CHANNEL_OPEN)) + m.add_byte(chr(_MSG_CHANNEL_OPEN)) m.add_string(kind) m.add_int(chanid) m.add_int(self.window_size) @@ -293,7 +316,7 @@ class BaseTransport(threading.Thread): self.channels[chanid] = chan = Channel(chanid) self.channel_events[chanid] = event = threading.Event() chan._set_transport(self) - chan.set_window(self.window_size, self.max_packet_size) + chan._set_window(self.window_size, self.max_packet_size) self._send_message(m) finally: self.lock.release() @@ -310,7 +333,141 @@ class BaseTransport(threading.Thread): finally: self.lock.release() return chan + + def renegotiate_keys(self): + """ + Force this session to switch to new keys. Normally this is done + automatically after the session hits a certain number of packets or + bytes sent or received, but this method gives you the option of forcing + new keys whenever you want. Negotiating new keys causes a pause in + traffic both ways as the two sides swap keys and do computations. This + method returns when the session has switched to new keys, or the + session has died mid-negotiation. + + @return: True if the renegotiation was successful, and the link is + using new keys; False if the session dropped during renegotiation. + @rtype: boolean + """ + self.completion_event = threading.Event() + self._send_kex_init() + while 1: + self.completion_event.wait(0.1); + if not self.active: + return False + if self.completion_event.isSet(): + break + return True + + def check_channel_request(self, kind, chanid): + "override me! return object descended from Channel to allow, or None to reject" + return None + + def accept(self, timeout=None): + try: + self.lock.acquire() + if len(self.server_accepts) > 0: + chan = self.server_accepts.pop(0) + else: + self.server_accept_cv.wait(timeout) + if len(self.server_accepts) > 0: + chan = self.server_accepts.pop(0) + else: + # timeout + chan = None + finally: + self.lock.release() + return chan + + def connect(self, hostkeytype=None, hostkey=None, username='', password=None, pkey=None): + """ + Negotiate an SSH2 session, and optionally verify the server's host key + and authenticate using a password or private key. This is a shortcut + for L{start_client}, L{get_remote_server_key}, and + L{Transport.auth_password} or L{Transport.auth_key}. Use those methods + if you want more control. + + You can use this method immediately after creating a Transport to + negotiate encryption with a server. If it fails, an exception will be + thrown. On success, the method will return cleanly, and an encrypted + session exists. You may immediately call L{open_channel} or + L{open_session} to get a L{Channel} object, which is used for data + transfer. + + @note: If you fail to supply a password or private key, this method may + succeed, but a subsequent L{open_channel} or L{open_session} call may + fail because you haven't authenticated yet. + + @param hostkeytype: the type of host key expected from the server + (usually C{"ssh-rsa"} or C{"ssh-dss"}), or C{None} if you don't want + to do host key verification. + @type hostkeytype: string + @param hostkey: the host key expected from the server, or C{None} if + you don't want to do host key verification. + @type hostkey: string + @param username: the username to authenticate as. + @type username: string + @param password: a password to use for authentication, if you want to + use password authentication; otherwise C{None}. + @type password: string + @param pkey: a private key to use for authentication, if you want to + use private key authentication; otherwise C{None}. + @type pkey: L{PKey<pkey.PKey>} + + @raise SSHException: if the SSH2 negotiation fails, the host key + supplied by the server is incorrect, or authentication fails. + """ + if hostkeytype is not None: + self.preferred_keys = [ hostkeytype ] + event = threading.Event() + self.start_client(event) + while 1: + event.wait(0.1) + if not self.active: + e = self.saved_exception + self.saved_exception = None + if e is not None: + raise e + raise SSHException('Negotiation failed.') + if event.isSet(): + break + + # check host key if we were given one + if (hostkeytype is not None) and (hostkey is not None): + type, key = self.get_remote_server_key() + if (type != hostkeytype) or (key != hostkey): + print repr(type) + ' - ' + repr(hostkeytype) + print repr(key) + ' - ' + repr(hostkey) + raise SSHException('Bad host key from server') + self._log(DEBUG, 'Host key verified (%s)' % hostkeytype) + + if (pkey is not None) or (password is not None): + event.clear() + if password is not None: + self._log(DEBUG, 'Attempting password auth...') + self.auth_password(username, password, event) + else: + self._log(DEBUG, 'Attempting password auth...') + self.auth_key(username, pkey, event) + while 1: + event.wait(0.1) + if not self.active: + e = self.saved_exception + self.saved_exception = None + if e is not None: + raise e + raise SSHException('Authentication failed.') + if event.isSet(): + break + if not self.is_authenticated(): + raise SSHException('Authentication failed.') + + return + + + ### internals... + + def _unlink_channel(self, chanid): "used by a Channel to remove itself from the active channel list" try: @@ -405,7 +562,8 @@ class BaseTransport(threading.Thread): padding = ord(packet[0]) payload = packet[1:packet_size - padding + 1] randpool.add_event(packet[packet_size - padding + 1]) - #self._log(DEBUG, 'Got payload (%d bytes, %d padding)' % (packet_size, padding)) + if self.ultra_debug: + self._log(DEBUG, 'Got payload (%d bytes, %d padding)' % (packet_size, padding)) msg = Message(payload[1:]) msg.seqno = self.sequence_number_in self.sequence_number_in = (self.sequence_number_in + 1) & 0xffffffffL @@ -482,17 +640,17 @@ class BaseTransport(threading.Thread): self._write_all(self.local_version + '\r\n') self._check_banner() self._send_kex_init() - self.expected_packet = MSG_KEXINIT + self.expected_packet = _MSG_KEXINIT while self.active: ptype, m = self._read_message() - if ptype == MSG_IGNORE: + if ptype == _MSG_IGNORE: continue - elif ptype == MSG_DISCONNECT: + elif ptype == _MSG_DISCONNECT: self._parse_disconnect(m) self.active = False break - elif ptype == MSG_DEBUG: + elif ptype == _MSG_DEBUG: self._parse_debug(m) continue if self.expected_packet != 0: @@ -512,53 +670,34 @@ class BaseTransport(threading.Thread): else: self._log(WARNING, 'Oops, unhandled type %d' % ptype) msg = Message() - msg.add_byte(chr(MSG_UNIMPLEMENTED)) + msg.add_byte(chr(_MSG_UNIMPLEMENTED)) msg.add_int(m.seqno) self._send_message(msg) except SSHException, e: self._log(DEBUG, 'Exception: ' + str(e)) self._log(DEBUG, tb_strings()) + self.saved_exception = e except EOFError, e: self._log(DEBUG, 'EOF') self._log(DEBUG, tb_strings()) + self.saved_exception = e except Exception, e: self._log(DEBUG, 'Unknown exception: ' + str(e)) self._log(DEBUG, tb_strings()) + self.saved_exception = e if self.active: self.active = False if self.completion_event != None: self.completion_event.set() if self.auth_event != None: self.auth_event.set() - for e in self.channel_events.values(): - e.set() + for event in self.channel_events.values(): + event.set() self.sock.close() - ### protocol stages - def renegotiate_keys(self): - """ - Force this session to switch to new keys. Normally this is done - automatically after the session hits a certain number of packets or - bytes sent or received, but this method gives you the option of forcing - new keys whenever you want. Negotiating new keys causes a pause in - traffic both ways as the two sides swap keys and do computations. This - method returns when the session has switched to new keys, or the - session has died mid-negotiation. + ### protocol stages - @return: True if the renegotiation was successful, and the link is - using new keys; False if the session dropped during renegotiation. - @rtype: boolean - """ - self.completion_event = threading.Event() - self._send_kex_init() - while 1: - self.completion_event.wait(0.1); - if not self.active: - return False - if self.completion_event.isSet(): - break - return True def _negotiate_keys(self, m): # throws SSHException on anything unusual @@ -615,7 +754,7 @@ class BaseTransport(threading.Thread): available_server_keys = self.preferred_keys m = Message() - m.add_byte(chr(MSG_KEXINIT)) + m.add_byte(chr(_MSG_KEXINIT)) m.add_bytes(randpool.get_bytes(16)) m.add(','.join(self.preferred_kex)) m.add(','.join(available_server_keys)) @@ -726,7 +865,7 @@ class BaseTransport(threading.Thread): # actually some extra bytes (one NUL byte in openssh's case) added to # the end of the packet but not parsed. turns out we need to throw # away those bytes because they aren't part of the hash. - self.remote_kex_init = chr(MSG_KEXINIT) + m.get_so_far() + self.remote_kex_init = chr(_MSG_KEXINIT) + m.get_so_far() def _activate_inbound(self): "switch on newly negotiated encryption parameters for inbound traffic" @@ -750,7 +889,7 @@ class BaseTransport(threading.Thread): def _activate_outbound(self): "switch on newly negotiated encryption parameters for outbound traffic" m = Message() - m.add_byte(chr(MSG_NEWKEYS)) + m.add_byte(chr(_MSG_NEWKEYS)) self._send_message(m) self.block_size_out = self._cipher_info[self.local_cipher]['block-size'] if self.server_mode: @@ -769,7 +908,7 @@ class BaseTransport(threading.Thread): else: self.mac_key_out = self._compute_key('E', self.local_mac_engine.digest_size) # we always expect to receive NEWKEYS now - self.expected_packet = MSG_NEWKEYS + self.expected_packet = _MSG_NEWKEYS def _parse_newkeys(self, m): self._log(DEBUG, 'Switch to new keys ...') @@ -801,7 +940,7 @@ class BaseTransport(threading.Thread): try: self.lock.acquire() chan = self.channels[chanid] - chan.set_remote_channel(server_chanid, server_window_size, server_max_packet_size) + chan._set_remote_channel(server_chanid, server_window_size, server_max_packet_size) self._log(INFO, 'Secsh channel %d opened.' % chanid) if self.channel_events.has_key(chanid): self.channel_events[chanid].set() @@ -815,8 +954,8 @@ class BaseTransport(threading.Thread): reason = m.get_int() reason_str = m.get_string() lang = m.get_string() - if CONNECTION_FAILED_CODE.has_key(reason): - reason_text = CONNECTION_FAILED_CODE[reason] + if _CONNECTION_FAILED_CODE.has_key(reason): + reason_text = _CONNECTION_FAILED_CODE[reason] else: reason_text = '(unknown code)' self._log(INFO, 'Secsh channel %d open FAILED: %s: %s' % (chanid, reason_str, reason_text)) @@ -831,10 +970,6 @@ class BaseTransport(threading.Thread): self.lock.release() return - def check_channel_request(self, kind, chanid): - "override me! return object descended from Channel to allow, or None to reject" - return None - def _parse_channel_open(self, m): kind = m.get_string() chanid = m.get_int() @@ -862,7 +997,7 @@ class BaseTransport(threading.Thread): reason = self.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED if reject: msg = Message() - msg.add_byte(chr(MSG_CHANNEL_OPEN_FAILURE)) + msg.add_byte(chr(_MSG_CHANNEL_OPEN_FAILURE)) msg.add_int(chanid) msg.add_int(reason) msg.add_string('') @@ -873,12 +1008,12 @@ class BaseTransport(threading.Thread): self.lock.acquire() self.channels[my_chanid] = chan chan._set_transport(self) - chan.set_window(self.window_size, self.max_packet_size) - chan.set_remote_channel(chanid, initial_window_size, max_packet_size) + chan._set_window(self.window_size, self.max_packet_size) + chan._set_remote_channel(chanid, initial_window_size, max_packet_size) finally: self.lock.release() m = Message() - m.add_byte(chr(MSG_CHANNEL_OPEN_SUCCESS)) + m.add_byte(chr(_MSG_CHANNEL_OPEN_SUCCESS)) m.add_int(chanid) m.add_int(my_chanid) m.add_int(self.window_size) @@ -892,22 +1027,6 @@ class BaseTransport(threading.Thread): finally: self.lock.release() - def accept(self, timeout=None): - try: - self.lock.acquire() - if len(self.server_accepts) > 0: - chan = self.server_accepts.pop(0) - else: - self.server_accept_cv.wait(timeout) - if len(self.server_accepts) > 0: - chan = self.server_accepts.pop(0) - else: - # timeout - chan = None - finally: - self.lock.release() - return chan - def _parse_debug(self, m): always_display = m.get_boolean() msg = m.get_string() @@ -915,19 +1034,19 @@ class BaseTransport(threading.Thread): self._log(DEBUG, 'Debug msg: ' + safe_string(msg)) _handler_table = { - MSG_NEWKEYS: _parse_newkeys, - MSG_CHANNEL_OPEN_SUCCESS: _parse_channel_open_success, - MSG_CHANNEL_OPEN_FAILURE: _parse_channel_open_failure, - MSG_CHANNEL_OPEN: _parse_channel_open, - MSG_KEXINIT: _negotiate_keys, + _MSG_NEWKEYS: _parse_newkeys, + _MSG_CHANNEL_OPEN_SUCCESS: _parse_channel_open_success, + _MSG_CHANNEL_OPEN_FAILURE: _parse_channel_open_failure, + _MSG_CHANNEL_OPEN: _parse_channel_open, + _MSG_KEXINIT: _negotiate_keys, } _channel_handler_table = { - MSG_CHANNEL_SUCCESS: Channel.request_success, - MSG_CHANNEL_FAILURE: Channel.request_failed, - MSG_CHANNEL_DATA: Channel.feed, - MSG_CHANNEL_WINDOW_ADJUST: Channel.window_adjust, - MSG_CHANNEL_REQUEST: Channel.handle_request, - MSG_CHANNEL_EOF: Channel.handle_eof, - MSG_CHANNEL_CLOSE: Channel.handle_close, + _MSG_CHANNEL_SUCCESS: Channel._request_success, + _MSG_CHANNEL_FAILURE: Channel._request_failed, + _MSG_CHANNEL_DATA: Channel._feed, + _MSG_CHANNEL_WINDOW_ADJUST: Channel._window_adjust, + _MSG_CHANNEL_REQUEST: Channel._handle_request, + _MSG_CHANNEL_EOF: Channel._handle_eof, + _MSG_CHANNEL_CLOSE: Channel._handle_close, } @@ -3,7 +3,7 @@ from distutils.core import setup longdesc = ''' This is a library for making SSH2 connections (client or server). Emphasis is on using SSH2 as an alternative to SSL for making secure -connections between pyton scripts. All major ciphers and hash methods +connections between python scripts. All major ciphers and hash methods are supported. (Previous name: secsh) |