diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..c151d25 --- /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'. diff --git a/Makefile b/Makefile index a2b28ae..c4d3873 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/NOTES b/NOTES index 7fbcc73..8a72a91 100644 --- a/NOTES +++ b/NOTES @@ -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 diff --git a/README b/README index 693d5b0..a67b889 100644 --- a/README +++ b/README @@ -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) diff --git a/demo.py b/demo.py index 875c204..963f7d8 100755 --- a/demo.py +++ b/demo.py @@ -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 similarity index 100% rename from demo_host_key rename to demo_rsa_key diff --git a/demo_server.py b/demo_server.py index e04c802..7fd25ad 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 0000000..0bd877c --- /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 cc5fbfa..0000000 --- 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 " -__date__ = "10 Nov 2003" -__version__ = "0.1-charmander" -__credits__ = "Huzzah!" - diff --git a/paramiko/__init__.py b/paramiko/__init__.py index 0e96a92..81d41ed 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 c39c463..34f1174 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 2ac0866..78d8ef2 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 = ' 0: + x = self.in_buffer + self.in_buffer = '' + self._feed_pipe(x) + return self.pipe_rfd 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 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() + + + ### 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" @@ -116,7 +472,58 @@ class Channel(object): "override me! return True if the pty was resized" return False - def handle_request(self, m): + + ### 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 @@ -151,13 +558,13 @@ class Channel(object): if want_reply: m = Message() if ok: - m.add_byte(chr(MSG_CHANNEL_SUCCESS)) + m.add_byte(chr(_MSG_CHANNEL_SUCCESS)) else: - m.add_byte(chr(MSG_CHANNEL_FAILURE)) + m.add_byte(chr(_MSG_CHANNEL_FAILURE)) m.add_int(self.remote_chanid) self.transport._send_message(m) - def handle_eof(self, m): + def _handle_eof(self, m): try: self.lock.acquire() if not self.eof_received: @@ -170,7 +577,7 @@ class Channel(object): self.lock.release() self._log(DEBUG, 'EOF received') - def handle_close(self, m): + def _handle_close(self, m): self.close() try: self.lock.acquire() @@ -183,252 +590,24 @@ class Channel(object): self.lock.release() - # API for external use + ### internals... - def get_pty(self, term='vt100', width=80, height=24): - 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_int(self.remote_chanid) - m.add_string('pty-req') - m.add_boolean(0) - m.add_string(term) - m.add_int(width) - m.add_int(height) - # pixel height, width (usually useless) - m.add_int(0).add_int(0) - m.add_string('') - self.transport._send_message(m) - def invoke_shell(self): - 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_int(self.remote_chanid) - m.add_string('shell') - m.add_boolean(1) - self.transport._send_message(m) + def _log(self, level, msg): + self.logger.log(level, msg) - def exec_command(self, command): - 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_int(self.remote_chanid) - m.add_string('exec') - m.add_boolean(1) - m.add_string(command) - self.transport._send_message(m) - - def invoke_subsystem(self, subsystem): - 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_int(self.remote_chanid) - m.add_string('subsystem') - m.add_boolean(1) - m.add_string(subsystem) - self.transport._send_message(m) - - def resize_pty(self, width=80, height=24): - 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_int(self.remote_chanid) - m.add_string('window-change') - m.add_boolean(0) - m.add_int(width) - m.add_int(height) - m.add_int(0).add_int(0) - self.transport._send_message(m) - - def get_transport(self): - return self.transport - - def set_name(self, name): - self.name = name - self.logger = logging.getLogger('paramiko.chan.' + name) - - def get_name(self): - return self.name - - def send_eof(self): + def _send_eof(self): if self.eof_sent: return m = Message() - m.add_byte(chr(MSG_CHANNEL_EOF)) + 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 - - # socket equivalency methods... - - def settimeout(self, timeout): - self.timeout = timeout - - def gettimeout(self): - return self.timeout - - def setblocking(self, blocking): - if blocking: - self.settimeout(None) - else: - self.settimeout(0.0) - - def close(self): - try: - self.lock.acquire() - if self.active and not self.closed: - self.send_eof() - m = Message() - m.add_byte(chr(MSG_CHANNEL_CLOSE)) - m.add_int(self.remote_chanid) - self.transport._send_message(m) - self.closed = 1 - self.transport._unlink_channel(self.chanid) - finally: - self.lock.release() - - def recv_ready(self): - "doesn't work if you've called fileno()" - try: - self.lock.acquire() - if len(self.in_buffer) == 0: - return 0 - return 1 - finally: - self.lock.release() - - def recv(self, nbytes): - out = '' - try: - self.lock.acquire() - if self.pipe_rfd != None: - # use the pipe - return self.read_pipe(nbytes) - if len(self.in_buffer) == 0: - if self.closed or self.eof_received: - return out - # should we block? - if self.timeout == 0.0: - raise socket.timeout() - # loop here in case we get woken up but a different thread has grabbed everything in the buffer - timeout = self.timeout - while (len(self.in_buffer) == 0) and not self.closed and not self.eof_received: - then = time.time() - self.in_buffer_cv.wait(timeout) - if timeout != None: - timeout -= time.time() - then - if timeout <= 0.0: - raise socket.timeout() - # something in the buffer and we have the lock - if len(self.in_buffer) <= nbytes: - out = self.in_buffer - self.in_buffer = '' - else: - out = self.in_buffer[:nbytes] - self.in_buffer = self.in_buffer[nbytes:] - self.check_add_window(len(out)) - finally: - self.lock.release() - return out - - def send(self, s): - size = 0 - if self.closed or self.eof_sent: - return size - try: - self.lock.acquire() - if self.out_window_size == 0: - # should we block? - if self.timeout == 0.0: - raise socket.timeout() - # loop here in case we get woken up but a different thread has filled the buffer - timeout = self.timeout - while self.out_window_size == 0: - then = time.time() - self.out_buffer_cv.wait(timeout) - if timeout != None: - timeout -= time.time() - then - if timeout <= 0.0: - raise socket.timeout() - # we have some window to squeeze into - if self.closed: - return 0 - size = len(s) - if self.out_window_size < size: - size = self.out_window_size - if self.out_max_packet_size < size: - size = self.out_max_packet_size - m = Message() - m.add_byte(chr(MSG_CHANNEL_DATA)) - m.add_int(self.remote_chanid) - m.add_string(s[:size]) - self.transport._send_message(m) - self.out_window_size -= size - finally: - self.lock.release() - return size - - def sendall(self, s): - while s: - if self.closed: - # this doesn't seem useful, but it is the documented behavior of Socket - raise socket.error('Socket is closed') - sent = self.send(s) - s = s[sent:] - return None - - def makefile(self, *params): - 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. - - 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. - """ - try: - self.lock.acquire() - if self.pipe_rfd != None: - 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) - if len(self.in_buffer) > 0: - x = self.in_buffer - self.in_buffer = '' - self.feed_pipe(x) - return self.pipe_rfd - finally: - self.lock.release() - - def shutdown(self, how): - if (how == 0) or (how == 2): - # feign "read" shutdown - self.eof_received = 1 - if (how == 1) or (how == 2): - self.send_eof() - - - # internal use... - - 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 35d8a8d..3eca858 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 5e02bab..f14c786 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 de1a254..3112326 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 0000000..c4c7599 --- /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 74502aa..e0935d5 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 f982d78..1a42fb7 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 } or + L{DSSKey }. + @type key: L{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 '' out = ' 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} + + @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. - - @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, } diff --git a/setup.py b/setup.py index 1d49611..656a298 100644 --- a/setup.py +++ b/setup.py @@ -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)