[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.
This commit is contained in:
Robey Pointer 2003-12-30 07:18:20 +00:00
parent 36d6d95dc6
commit 48c7d888a2
19 changed files with 1093 additions and 564 deletions

42
ChangeLog Normal file
View File

@ -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'.

View File

@ -1,6 +1,6 @@
# releases: # releases:
# aerodactyl (13sep03) # aerodactyl (13sep03)
# bulbasaur # bulbasaur (18sep03)
# charmander (10nov03) # charmander (10nov03)
RELEASE=charmander RELEASE=charmander
@ -8,8 +8,11 @@ RELEASE=charmander
release: release:
python ./setup.py sdist --formats=zip python ./setup.py sdist --formats=zip
docs:
epydoc -o docs/ paramiko
# places where the version number is stored: # places where the version number is stored:
# #
# setup.py # setup.py
# secsh.py # __init__.py
# README # README

13
NOTES
View File

@ -15,19 +15,6 @@ SSHOutputStream --> ssh2 chan --> ssh2 transport --> SOS [no thread]
exported API... 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: from Transport:
* is_authenticated * is_authenticated
auth_key auth_key

43
README
View File

@ -130,49 +130,6 @@ are still running (and you'll have to kill -9 from another shell window).
[fixme: add info about server mode] [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 *** MISSING LINKS
* ctr forms of ciphers are missing (blowfish-ctr, aes128-ctr, aes256-ctr) * ctr forms of ciphers are missing (blowfish-ctr, aes128-ctr, aes256-ctr)

View File

@ -66,7 +66,6 @@ except Exception, e:
try: try:
event = threading.Event() event = threading.Event()
t = paramiko.Transport(sock) t = paramiko.Transport(sock)
t.ultra_debug = 0
t.start_client(event) t.start_client(event)
# print repr(t) # print repr(t)
event.wait(15) event.wait(15)

View File

@ -13,10 +13,11 @@ if len(l.handlers) == 0:
l.addHandler(lh) l.addHandler(lh)
#host_key = paramiko.RSAKey() #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 = paramiko.DSSKey()
host_key.read_private_key_file('demo_dss_key') host_key.read_private_key_file('demo_dss_key')
print 'Read key: ' + paramiko.hexify(host_key.get_fingerprint()) print 'Read key: ' + paramiko.hexify(host_key.get_fingerprint())

136
demo_simple.py Executable file
View File

@ -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)

View File

@ -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!"

View File

@ -11,10 +11,35 @@ __version__ = "0.1-charmander"
__credits__ = "Huzzah!" __credits__ = "Huzzah!"
from auth_transport import Transport import ssh_exception, transport, auth_transport, channel, rsakey, dsskey, util
from channel import Channel
from rsakey import RSAKey
from dsskey import DSSKey
from util import hexify
#__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' ]

View File

@ -1,14 +1,14 @@
#!/usr/bin/python #!/usr/bin/python
from transport import BaseTransport from transport import BaseTransport
from transport import MSG_SERVICE_REQUEST, MSG_SERVICE_ACCEPT, MSG_USERAUTH_REQUEST, MSG_USERAUTH_FAILURE, \ from transport import _MSG_SERVICE_REQUEST, _MSG_SERVICE_ACCEPT, _MSG_USERAUTH_REQUEST, _MSG_USERAUTH_FAILURE, \
MSG_USERAUTH_SUCCESS, MSG_USERAUTH_BANNER _MSG_USERAUTH_SUCCESS, _MSG_USERAUTH_BANNER
from message import Message from message import Message
from ssh_exception import SSHException from ssh_exception import SSHException
from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL
DISCONNECT_SERVICE_NOT_AVAILABLE, DISCONNECT_AUTH_CANCELLED_BY_USER, \ _DISCONNECT_SERVICE_NOT_AVAILABLE, _DISCONNECT_AUTH_CANCELLED_BY_USER, \
DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE = 7, 13, 14 _DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE = 7, 13, 14
@ -55,7 +55,7 @@ class Transport(BaseTransport):
def _request_auth(self): def _request_auth(self):
m = Message() m = Message()
m.add_byte(chr(MSG_SERVICE_REQUEST)) m.add_byte(chr(_MSG_SERVICE_REQUEST))
m.add_string('ssh-userauth') m.add_string('ssh-userauth')
self._send_message(m) self._send_message(m)
@ -90,8 +90,8 @@ class Transport(BaseTransport):
def disconnect_service_not_available(self): def disconnect_service_not_available(self):
m = Message() m = Message()
m.add_byte(chr(MSG_DISCONNECT)) m.add_byte(chr(_MSG_DISCONNECT))
m.add_int(DISCONNECT_SERVICE_NOT_AVAILABLE) m.add_int(_DISCONNECT_SERVICE_NOT_AVAILABLE)
m.add_string('Service not available') m.add_string('Service not available')
m.add_string('en') m.add_string('en')
self._send_message(m) self._send_message(m)
@ -99,8 +99,8 @@ class Transport(BaseTransport):
def disconnect_no_more_auth(self): def disconnect_no_more_auth(self):
m = Message() m = Message()
m.add_byte(chr(MSG_DISCONNECT)) m.add_byte(chr(_MSG_DISCONNECT))
m.add_int(DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE) m.add_int(_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE)
m.add_string('No more auth methods available') m.add_string('No more auth methods available')
m.add_string('en') m.add_string('en')
self._send_message(m) self._send_message(m)
@ -111,7 +111,7 @@ class Transport(BaseTransport):
if self.server_mode and (service == 'ssh-userauth'): if self.server_mode and (service == 'ssh-userauth'):
# accepted # accepted
m = Message() m = Message()
m.add_byte(chr(MSG_SERVICE_ACCEPT)) m.add_byte(chr(_MSG_SERVICE_ACCEPT))
m.add_string(service) m.add_string(service)
self._send_message(m) self._send_message(m)
return return
@ -123,7 +123,7 @@ class Transport(BaseTransport):
if service == 'ssh-userauth': if service == 'ssh-userauth':
self._log(DEBUG, 'userauth is OK') self._log(DEBUG, 'userauth is OK')
m = Message() m = Message()
m.add_byte(chr(MSG_USERAUTH_REQUEST)) m.add_byte(chr(_MSG_USERAUTH_REQUEST))
m.add_string(self.username) m.add_string(self.username)
m.add_string('ssh-connection') m.add_string('ssh-connection')
m.add_string(self.auth_method) m.add_string(self.auth_method)
@ -161,7 +161,7 @@ class Transport(BaseTransport):
if not self.server_mode: if not self.server_mode:
# er, uh... what? # er, uh... what?
m = Message() m = Message()
m.add_byte(chr(MSG_USERAUTH_FAILURE)) m.add_byte(chr(_MSG_USERAUTH_FAILURE))
m.add_string('none') m.add_string('none')
m.add_boolean(0) m.add_boolean(0)
self._send_message(m) self._send_message(m)
@ -202,11 +202,11 @@ class Transport(BaseTransport):
m = Message() m = Message()
if result == self.AUTH_SUCCESSFUL: if result == self.AUTH_SUCCESSFUL:
self._log(DEBUG, 'Auth granted.') self._log(DEBUG, 'Auth granted.')
m.add_byte(chr(MSG_USERAUTH_SUCCESS)) m.add_byte(chr(_MSG_USERAUTH_SUCCESS))
self.auth_complete = 1 self.auth_complete = 1
else: else:
self._log(DEBUG, 'Auth rejected.') 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)) m.add_string(self.get_allowed_auths(username))
if result == self.AUTH_PARTIALLY_SUCCESSFUL: if result == self.AUTH_PARTIALLY_SUCCESSFUL:
m.add_boolean(1) m.add_boolean(1)
@ -245,11 +245,11 @@ class Transport(BaseTransport):
_handler_table = BaseTransport._handler_table.copy() _handler_table = BaseTransport._handler_table.copy()
_handler_table.update({ _handler_table.update({
MSG_SERVICE_REQUEST: parse_service_request, _MSG_SERVICE_REQUEST: parse_service_request,
MSG_SERVICE_ACCEPT: parse_service_accept, _MSG_SERVICE_ACCEPT: parse_service_accept,
MSG_USERAUTH_REQUEST: parse_userauth_request, _MSG_USERAUTH_REQUEST: parse_userauth_request,
MSG_USERAUTH_SUCCESS: parse_userauth_success, _MSG_USERAUTH_SUCCESS: parse_userauth_success,
MSG_USERAUTH_FAILURE: parse_userauth_failure, _MSG_USERAUTH_FAILURE: parse_userauth_failure,
MSG_USERAUTH_BANNER: parse_userauth_banner, _MSG_USERAUTH_BANNER: parse_userauth_banner,
}) })

View File

@ -1,14 +1,14 @@
from message import Message from message import Message
from ssh_exception import SSHException from ssh_exception import SSHException
from transport import MSG_CHANNEL_REQUEST, MSG_CHANNEL_CLOSE, MSG_CHANNEL_WINDOW_ADJUST, MSG_CHANNEL_DATA, \ 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 _MSG_CHANNEL_EOF, _MSG_CHANNEL_SUCCESS, _MSG_CHANNEL_FAILURE
import time, threading, logging, socket, os import time, threading, logging, socket, os
from logging import DEBUG from logging import DEBUG
# this is ugly, and won't work on windows # this is ugly, and won't work on windows
def set_nonblocking(fd): def _set_nonblocking(fd):
import fcntl import fcntl
fcntl.fcntl(fd, fcntl.F_SETFL, os.O_NONBLOCK) fcntl.fcntl(fd, fcntl.F_SETFL, os.O_NONBLOCK)
@ -35,6 +35,11 @@ class Channel(object):
self.pipe_rfd = self.pipe_wfd = None self.pipe_rfd = self.pipe_wfd = None
def __repr__(self): def __repr__(self):
"""
Returns a string representation of this object, for debugging.
@rtype: string
"""
out = '<paramiko.Channel %d' % self.chanid out = '<paramiko.Channel %d' % self.chanid
if self.closed: if self.closed:
out += ' (closed)' out += ' (closed)'
@ -50,56 +55,407 @@ class Channel(object):
out += '>' out += '>'
return out return out
def _set_transport(self, transport): def get_pty(self, term='vt100', width=80, height=24):
self.transport = transport """
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.
def _log(self, level, msg): @param term: the terminal type to emulate (for example, C{'vt100'}).
self.logger.log(level, msg) @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_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 set_window(self, window_size, max_packet_size): def invoke_shell(self):
self.in_window_size = window_size if self.closed or self.eof_received or self.eof_sent or not self.active:
self.in_max_packet_size = max_packet_size raise SSHException('Channel is not open')
# threshold of bytes we receive before we bother to send a window update m = Message()
self.in_window_threshold = window_size // 10 m.add_byte(chr(_MSG_CHANNEL_REQUEST))
self.in_window_sofar = 0 m.add_int(self.remote_chanid)
m.add_string('shell')
m.add_boolean(1)
self.transport._send_message(m)
def set_remote_channel(self, chanid, window_size, max_packet_size): def exec_command(self, command):
self.remote_chanid = chanid if self.closed or self.eof_received or self.eof_sent or not self.active:
self.out_window_size = window_size raise SSHException('Channel is not open')
self.out_max_packet_size = max_packet_size m = Message()
self.active = 1 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 request_success(self, m): def invoke_subsystem(self, subsystem):
self._log(DEBUG, 'Sesch channel %d request ok' % self.chanid) if self.closed or self.eof_received or self.eof_sent or not self.active:
return 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 request_failed(self, m): def resize_pty(self, width=80, height=24):
self.close() """
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.
def feed(self, m): @param width: new width (in characters) of the terminal screen
s = m.get_string() @type width: int
try: @param height: new height (in characters) of the terminal screen
self.lock.acquire() @type height: int
self._log(DEBUG, 'fed %d bytes' % len(s)) """
if self.pipe_wfd != None: if self.closed or self.eof_received or self.eof_sent or not self.active:
self.feed_pipe(s) 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 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):
"""
Get the name of this channel that was previously set by L{set_name}.
@return: the name of this channel
@rtype: string
"""
return self.name
### socket API
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: else:
self.in_buffer += s self.settimeout(0.0)
self.in_buffer_cv.notifyAll()
self._log(DEBUG, '(out from feed)') 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()
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: finally:
self.lock.release() self.lock.release()
def window_adjust(self, m): def recv_ready(self):
nbytes = m.get_int() """
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: try:
self.lock.acquire() self.lock.acquire()
self._log(DEBUG, 'window up %d' % nbytes) if len(self.in_buffer) == 0:
self.out_window_size += nbytes return False
self.out_buffer_cv.notifyAll() return True
finally: finally:
self.lock.release() 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)
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):
"""
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
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):
"""
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
raise socket.error('Socket is closed')
sent = self.send(s)
s = s[sent:]
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 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.
@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()
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):
"""
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): 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" "override me! return True if a pty of the given dimensions (for shell access, usually) can be provided"
return False return False
@ -116,7 +472,58 @@ class Channel(object):
"override me! return True if the pty was resized" "override me! return True if the pty was resized"
return False 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() key = m.get_string()
want_reply = m.get_boolean() want_reply = m.get_boolean()
ok = False ok = False
@ -151,13 +558,13 @@ class Channel(object):
if want_reply: if want_reply:
m = Message() m = Message()
if ok: if ok:
m.add_byte(chr(MSG_CHANNEL_SUCCESS)) m.add_byte(chr(_MSG_CHANNEL_SUCCESS))
else: else:
m.add_byte(chr(MSG_CHANNEL_FAILURE)) m.add_byte(chr(_MSG_CHANNEL_FAILURE))
m.add_int(self.remote_chanid) m.add_int(self.remote_chanid)
self.transport._send_message(m) self.transport._send_message(m)
def handle_eof(self, m): def _handle_eof(self, m):
try: try:
self.lock.acquire() self.lock.acquire()
if not self.eof_received: if not self.eof_received:
@ -170,7 +577,7 @@ class Channel(object):
self.lock.release() self.lock.release()
self._log(DEBUG, 'EOF received') self._log(DEBUG, 'EOF received')
def handle_close(self, m): def _handle_close(self, m):
self.close() self.close()
try: try:
self.lock.acquire() self.lock.acquire()
@ -183,252 +590,24 @@ class Channel(object):
self.lock.release() 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): def _log(self, level, msg):
if self.closed or self.eof_received or self.eof_sent or not self.active: self.logger.log(level, msg)
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 exec_command(self, command): def _send_eof(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('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):
if self.eof_sent: if self.eof_sent:
return return
m = Message() m = Message()
m.add_byte(chr(MSG_CHANNEL_EOF)) m.add_byte(chr(_MSG_CHANNEL_EOF))
m.add_int(self.remote_chanid) m.add_int(self.remote_chanid)
self.transport._send_message(m) self.transport._send_message(m)
self.eof_sent = 1 self.eof_sent = 1
self._log(DEBUG, 'EOF sent') self._log(DEBUG, 'EOF sent')
return return
def _feed_pipe(self, data):
# 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):
"you are already holding the lock" "you are already holding the lock"
if len(self.in_buffer) > 0: if len(self.in_buffer) > 0:
self.in_buffer += data self.in_buffer += data
@ -439,7 +618,7 @@ class Channel(object):
# at least on linux, this will never happen, as the writes are # at least on linux, this will never happen, as the writes are
# considered atomic... but just in case. # considered atomic... but just in case.
self.in_buffer = data[n:] self.in_buffer = data[n:]
self.check_add_window(n) self._check_add_window(n)
self.in_buffer_cv.notifyAll() self.in_buffer_cv.notifyAll()
return return
except OSError, e: except OSError, e:
@ -451,7 +630,7 @@ class Channel(object):
try: try:
os.write(self.pipe_wfd, x) os.write(self.pipe_wfd, x)
self.in_buffer = data self.in_buffer = data
self.check_add_window(1) self._check_add_window(1)
self.in_buffer_cv.notifyAll() self.in_buffer_cv.notifyAll()
return return
except OSError, e: except OSError, e:
@ -460,12 +639,12 @@ class Channel(object):
self.in_buffer = data self.in_buffer = data
self.in_buffer_cv.notifyAll() self.in_buffer_cv.notifyAll()
def read_pipe(self, nbytes): def _read_pipe(self, nbytes):
"you are already holding the lock" "you are already holding the lock"
try: try:
x = os.read(self.pipe_rfd, nbytes) x = os.read(self.pipe_rfd, nbytes)
if len(x) > 0: if len(x) > 0:
self.push_pipe(len(x)) self._push_pipe(len(x))
return x return x
except OSError, e: except OSError, e:
pass pass
@ -487,13 +666,13 @@ class Channel(object):
try: try:
x = os.read(self.pipe_rfd, nbytes) x = os.read(self.pipe_rfd, nbytes)
if len(x) > 0: if len(x) > 0:
self.push_pipe(len(x)) self._push_pipe(len(x))
return x return x
except OSError, e: except OSError, e:
pass pass
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 # 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) # (assumption: the pipe can hold as many bytes as were read out)
if len(self.in_buffer) == 0: if len(self.in_buffer) == 0:
@ -512,7 +691,7 @@ class Channel(object):
self.closed = 1 self.closed = 1
self.transport._unlink_channel(self.chanid) self.transport._unlink_channel(self.chanid)
def check_add_window(self, n): def _check_add_window(self, n):
# already holding the lock! # already holding the lock!
if self.closed or self.eof_received or not self.active: if self.closed or self.eof_received or not self.active:
return return
@ -521,7 +700,7 @@ class Channel(object):
if self.in_window_sofar > self.in_window_threshold: if self.in_window_sofar > self.in_window_threshold:
self._log(DEBUG, 'addwindow send %d' % self.in_window_sofar) self._log(DEBUG, 'addwindow send %d' % self.in_window_sofar)
m = Message() 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.remote_chanid)
m.add_int(self.in_window_sofar) m.add_int(self.in_window_sofar)
self.transport._send_message(m) self.transport._send_message(m)

View File

@ -3,16 +3,16 @@
import base64 import base64
from ssh_exception import SSHException from ssh_exception import SSHException
from message import Message from message import Message
from transport import MSG_USERAUTH_REQUEST from transport import _MSG_USERAUTH_REQUEST
from util import inflate_long, deflate_long from util import inflate_long, deflate_long
from Crypto.PublicKey import DSA from Crypto.PublicKey import DSA
from Crypto.Hash import SHA, MD5 from Crypto.Hash import SHA
from ber import BER from ber import BER
from pkey import PKey
from util import format_binary from util import format_binary
class DSSKey (PKey):
class DSSKey(object):
def __init__(self, msg=None): def __init__(self, msg=None):
self.valid = 0 self.valid = 0
@ -39,9 +39,6 @@ class DSSKey(object):
def get_name(self): def get_name(self):
return 'ssh-dss' return 'ssh-dss'
def get_fingerprint(self):
return MD5.new(str(self)).digest()
def verify_ssh_sig(self, data, msg): def verify_ssh_sig(self, data, msg):
if not self.valid: if not self.valid:
return 0 return 0
@ -78,7 +75,6 @@ class DSSKey(object):
return str(m) return str(m)
def read_private_key_file(self, filename): def read_private_key_file(self, filename):
"throws a file exception, or SSHException (on invalid key, or base64 decoding exception"
# private key file contains: # private key file contains:
# DSAPrivateKey = { version = 0, p, q, g, y, x } # DSAPrivateKey = { version = 0, p, q, g, y, x }
self.valid = 0 self.valid = 0
@ -102,7 +98,7 @@ class DSSKey(object):
def sign_ssh_session(self, randpool, sid, username): def sign_ssh_session(self, randpool, sid, username):
m = Message() m = Message()
m.add_string(sid) 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(username)
m.add_string('ssh-connection') m.add_string('ssh-connection')
m.add_string('publickey') m.add_string('publickey')

View File

@ -7,12 +7,12 @@
from message import Message from message import Message
from util import inflate_long, deflate_long, bit_length from util import inflate_long, deflate_long, bit_length
from ssh_exception import SSHException from ssh_exception import SSHException
from transport import MSG_NEWKEYS from transport import _MSG_NEWKEYS
from Crypto.Hash import SHA from Crypto.Hash import SHA
from Crypto.Util import number from Crypto.Util import number
from logging import DEBUG 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): class KexGex(object):
@ -27,27 +27,27 @@ class KexGex(object):
def start_kex(self): def start_kex(self):
if self.transport.server_mode: if self.transport.server_mode:
self.transport._expect_packet(MSG_KEXDH_GEX_REQUEST) self.transport._expect_packet(_MSG_KEXDH_GEX_REQUEST)
return return
# request a bit range: we accept (min_bits) to (max_bits), but prefer # request a bit range: we accept (min_bits) to (max_bits), but prefer
# (preferred_bits). according to the spec, we shouldn't pull the # (preferred_bits). according to the spec, we shouldn't pull the
# minimum up above 1024. # minimum up above 1024.
m = Message() 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.min_bits)
m.add_int(self.preferred_bits) m.add_int(self.preferred_bits)
m.add_int(self.max_bits) m.add_int(self.max_bits)
self.transport._send_message(m) 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): 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) 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) 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) 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) return self._parse_kexdh_gex_reply(m)
raise SSHException('KexGex asked to handle packet type %d' % ptype) 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') raise SSHException('Can\'t do server-side gex with no modulus pack')
self.g, self.p = pack.get_modulus(min, preferred, max) self.g, self.p = pack.get_modulus(min, preferred, max)
m = Message() 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.p)
m.add_mpint(self.g) m.add_mpint(self.g)
self.transport._send_message(m) 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): def _parse_kexdh_gex_group(self, m):
self.p = m.get_mpint() self.p = m.get_mpint()
@ -114,10 +114,10 @@ class KexGex(object):
# now compute e = g^x mod p # now compute e = g^x mod p
self.e = pow(self.g, self.x, self.p) self.e = pow(self.g, self.x, self.p)
m = Message() m = Message()
m.add_byte(chr(MSG_KEXDH_GEX_INIT)) m.add_byte(chr(_MSG_KEXDH_GEX_INIT))
m.add_mpint(self.e) m.add_mpint(self.e)
self.transport._send_message(m) self.transport._send_message(m)
self.transport._expect_packet(MSG_KEXDH_GEX_REPLY) self.transport._expect_packet(_MSG_KEXDH_GEX_REPLY)
def _parse_kexdh_gex_init(self, m): def _parse_kexdh_gex_init(self, m):
self.e = m.get_mpint() 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) sig = self.transport.get_server_key().sign_ssh_data(self.transport.randpool, H)
# send reply # send reply
m = Message() m = Message()
m.add_byte(chr(MSG_KEXDH_GEX_REPLY)) m.add_byte(chr(_MSG_KEXDH_GEX_REPLY))
m.add_string(key) m.add_string(key)
m.add_mpint(self.f) m.add_mpint(self.f)
m.add_string(sig) m.add_string(sig)

View File

@ -6,11 +6,11 @@
from message import Message, inflate_long from message import Message, inflate_long
from ssh_exception import SSHException from ssh_exception import SSHException
from transport import MSG_NEWKEYS from transport import _MSG_NEWKEYS
from Crypto.Hash import SHA from Crypto.Hash import SHA
from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL 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 # draft-ietf-secsh-transport-09.txt, page 17
P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFFL P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFFL
@ -44,20 +44,20 @@ class KexGroup1(object):
if self.transport.server_mode: if self.transport.server_mode:
# compute f = g^x mod p, but don't send it yet # compute f = g^x mod p, but don't send it yet
self.f = pow(G, self.x, P) self.f = pow(G, self.x, P)
self.transport._expect_packet(MSG_KEXDH_INIT) self.transport._expect_packet(_MSG_KEXDH_INIT)
return return
# compute e = g^x mod p (where g=2), and send it # compute e = g^x mod p (where g=2), and send it
self.e = pow(G, self.x, P) self.e = pow(G, self.x, P)
m = Message() m = Message()
m.add_byte(chr(MSG_KEXDH_INIT)) m.add_byte(chr(_MSG_KEXDH_INIT))
m.add_mpint(self.e) m.add_mpint(self.e)
self.transport._send_message(m) self.transport._send_message(m)
self.transport._expect_packet(MSG_KEXDH_REPLY) self.transport._expect_packet(_MSG_KEXDH_REPLY)
def parse_next(self, ptype, m): 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) 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) return self.parse_kexdh_reply(m)
raise SSHException('KexGroup1 asked to handle packet type %d' % ptype) raise SSHException('KexGroup1 asked to handle packet type %d' % ptype)
@ -94,7 +94,7 @@ class KexGroup1(object):
sig = self.transport.get_server_key().sign_ssh_data(self.transport.randpool, H) sig = self.transport.get_server_key().sign_ssh_data(self.transport.randpool, H)
# send reply # send reply
m = Message() m = Message()
m.add_byte(chr(MSG_KEXDH_REPLY)) m.add_byte(chr(_MSG_KEXDH_REPLY))
m.add_string(key) m.add_string(key)
m.add_mpint(self.f) m.add_mpint(self.f)
m.add_string(sig) m.add_string(sig)

112
paramiko/pkey.py Normal file
View File

@ -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
"""

View File

@ -1,14 +1,15 @@
#!/usr/bin/python #!/usr/bin/python
from message import Message from message import Message
from transport import MSG_USERAUTH_REQUEST from transport import _MSG_USERAUTH_REQUEST
from Crypto.PublicKey import RSA from Crypto.PublicKey import RSA
from Crypto.Hash import SHA from Crypto.Hash import SHA
from ber import BER from ber import BER
from util import format_binary, inflate_long, deflate_long from util import format_binary, inflate_long, deflate_long
from pkey import PKey
import base64 import base64
class RSAKey(object): class RSAKey (PKey):
def __init__(self, msg=None): def __init__(self, msg=None):
self.valid = 0 self.valid = 0
@ -31,10 +32,7 @@ class RSAKey(object):
def get_name(self): def get_name(self):
return 'ssh-rsa' return 'ssh-rsa'
def get_fingerprint(self): def _pkcs1imify(self, data):
return MD5.new(str(self)).digest()
def pkcs1imify(self, data):
""" """
turn a 20-byte SHA1 hash into a blob of data as large as the key's N, turn a 20-byte SHA1 hash into a blob of data as large as the key's N,
using PKCS1's \"emsa-pkcs1-v1_5\" encoding. totally bizarre. using PKCS1's \"emsa-pkcs1-v1_5\" encoding. totally bizarre.
@ -45,26 +43,25 @@ class RSAKey(object):
def verify_ssh_sig(self, data, msg): def verify_ssh_sig(self, data, msg):
if (not self.valid) or (msg.get_string() != 'ssh-rsa'): if (not self.valid) or (msg.get_string() != 'ssh-rsa'):
return 0 return False
sig = inflate_long(msg.get_string(), 1) sig = inflate_long(msg.get_string(), 1)
# verify the signature by SHA'ing the data and encrypting it using the # verify the signature by SHA'ing the data and encrypting it using the
# public key. some wackiness ensues where we "pkcs1imify" the 20-byte # public key. some wackiness ensues where we "pkcs1imify" the 20-byte
# hash into a string as long as the RSA key. # hash into a string as long as the RSA key.
hash = 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))) rsa = RSA.construct((long(self.n), long(self.e)))
return rsa.verify(hash, (sig,)) return rsa.verify(hash, (sig,))
def sign_ssh_data(self, randpool, data): def sign_ssh_data(self, randpool, data):
hash = SHA.new(data).digest() hash = SHA.new(data).digest()
rsa = RSA.construct((long(self.n), long(self.e), long(self.d))) rsa = RSA.construct((long(self.n), long(self.e), long(self.d)))
sig = deflate_long(rsa.sign(self.pkcs1imify(hash), '')[0], 0) sig = deflate_long(rsa.sign(self._pkcs1imify(hash), '')[0], 0)
m = Message() m = Message()
m.add_string('ssh-rsa') m.add_string('ssh-rsa')
m.add_string(sig) m.add_string(sig)
return str(m) return str(m)
def read_private_key_file(self, filename): def read_private_key_file(self, filename):
"throws a file exception, or SSHException (on invalid key), or base64 decoding exception"
# private key file contains: # private key file contains:
# RSAPrivateKey = { version = 0, n, e, d, p, q, d mod p-1, d mod q-1, q**-1 mod p } # RSAPrivateKey = { version = 0, n, e, d, p, q, d mod p-1, d mod q-1, q**-1 mod p }
self.valid = 0 self.valid = 0
@ -72,11 +69,11 @@ class RSAKey(object):
lines = f.readlines() lines = f.readlines()
f.close() f.close()
if lines[0].strip() != '-----BEGIN RSA PRIVATE KEY-----': 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])) data = base64.decodestring(''.join(lines[1:-1]))
keylist = BER(data).decode() keylist = BER(data).decode()
if (type(keylist) != type([])) or (len(keylist) < 4) or (keylist[0] != 0): 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.n = keylist[1]
self.e = keylist[2] self.e = keylist[2]
self.d = keylist[3] self.d = keylist[3]
@ -89,7 +86,7 @@ class RSAKey(object):
def sign_ssh_session(self, randpool, sid, username): def sign_ssh_session(self, randpool, sid, username):
m = Message() m = Message()
m.add_string(sid) 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(username)
m.add_string('ssh-connection') m.add_string('ssh-connection')
m.add_string('publickey') m.add_string('publickey')

View File

@ -1,15 +1,15 @@
#!/usr/bin/python #!/usr/bin/python
MSG_DISCONNECT, MSG_IGNORE, MSG_UNIMPLEMENTED, MSG_DEBUG, MSG_SERVICE_REQUEST, \ _MSG_DISCONNECT, _MSG_IGNORE, _MSG_UNIMPLEMENTED, _MSG_DEBUG, _MSG_SERVICE_REQUEST, \
MSG_SERVICE_ACCEPT = range(1, 7) _MSG_SERVICE_ACCEPT = range(1, 7)
MSG_KEXINIT, MSG_NEWKEYS = range(20, 22) _MSG_KEXINIT, _MSG_NEWKEYS = range(20, 22)
MSG_USERAUTH_REQUEST, MSG_USERAUTH_FAILURE, MSG_USERAUTH_SUCCESS, \ _MSG_USERAUTH_REQUEST, _MSG_USERAUTH_FAILURE, _MSG_USERAUTH_SUCCESS, \
MSG_USERAUTH_BANNER = range(50, 54) _MSG_USERAUTH_BANNER = range(50, 54)
MSG_USERAUTH_PK_OK = 60 _MSG_USERAUTH_PK_OK = 60
MSG_CHANNEL_OPEN, MSG_CHANNEL_OPEN_SUCCESS, MSG_CHANNEL_OPEN_FAILURE, \ _MSG_CHANNEL_OPEN, _MSG_CHANNEL_OPEN_SUCCESS, _MSG_CHANNEL_OPEN_FAILURE, \
MSG_CHANNEL_WINDOW_ADJUST, MSG_CHANNEL_DATA, MSG_CHANNEL_EXTENDED_DATA, \ _MSG_CHANNEL_WINDOW_ADJUST, _MSG_CHANNEL_DATA, _MSG_CHANNEL_EXTENDED_DATA, \
MSG_CHANNEL_EOF, MSG_CHANNEL_CLOSE, MSG_CHANNEL_REQUEST, \ _MSG_CHANNEL_EOF, _MSG_CHANNEL_CLOSE, _MSG_CHANNEL_REQUEST, \
MSG_CHANNEL_SUCCESS, MSG_CHANNEL_FAILURE = range(90, 101) _MSG_CHANNEL_SUCCESS, _MSG_CHANNEL_FAILURE = range(90, 101)
import sys, os, string, threading, socket, logging, struct import sys, os, string, threading, socket, logging, struct
from ssh_exception import SSHException from ssh_exception import SSHException
@ -36,7 +36,7 @@ from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL
# channel request failed reasons: # channel request failed reasons:
CONNECTION_FAILED_CODE = { _CONNECTION_FAILED_CODE = {
1: 'Administratively prohibited', 1: 'Administratively prohibited',
2: 'Connect failed', 2: 'Connect failed',
3: 'Unknown channel type', 3: 'Unknown channel type',
@ -54,29 +54,14 @@ except:
randpool.randomize() randpool.randomize()
class BaseTransport (threading.Thread): class BaseTransport (threading.Thread):
''' """
An SSH Transport attaches to a stream (usually a socket), negotiates an Handles protocol negotiation, key exchange, encryption, and the creation
encrypted session, authenticates, and then creates stream tunnels, called of channels across an SSH session. Basically everything but authentication
"channels", across the session. Multiple channels can be multiplexed is done here.
across a single session (and often are, in the case of port forwardings). """
_PROTO_ID = '2.0'
Transport expects to receive a "socket-like object" to talk to the SSH _CLIENT_ID = 'pyssh_1.1'
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'
preferred_ciphers = [ 'aes128-cbc', 'blowfish-cbc', 'aes256-cbc', '3des-cbc' ] preferred_ciphers = [ 'aes128-cbc', 'blowfish-cbc', 'aes256-cbc', '3des-cbc' ]
preferred_macs = [ 'hmac-sha1', 'hmac-md5', 'hmac-sha1-96', 'hmac-md5-96' ] 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_ADMINISTRATIVELY_PROHIBITED, OPEN_FAILED_CONNECT_FAILED, OPEN_FAILED_UNKNOWN_CHANNEL_TYPE, \
OPEN_FAILED_RESOURCE_SHORTAGE = range(1, 5) OPEN_FAILED_RESOURCE_SHORTAGE = range(1, 5)
_modulus_pack = None
def __init__(self, sock): 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) threading.Thread.__init__(self, target=self._run)
self.randpool = randpool self.randpool = randpool
self.sock = sock self.sock = sock
self.sock.settimeout(0.1) self.sock.settimeout(0.1)
# negotiated crypto parameters # 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.remote_version = ''
self.block_size_out = self.block_size_in = 8 self.block_size_out = self.block_size_in = 8
self.local_mac_len = self.remote_mac_len = 0 self.local_mac_len = self.remote_mac_len = 0
@ -136,7 +142,7 @@ class BaseTransport(threading.Thread):
self.window_size = 65536 self.window_size = 65536
self.max_packet_size = 2048 self.max_packet_size = 2048
self.ultra_debug = 0 self.ultra_debug = 0
self.modulus_pack = None self.saved_exception = None
# used for noticing when to re-key: # used for noticing when to re-key:
self.received_bytes = 0 self.received_bytes = 0
self.received_packets = 0 self.received_packets = 0
@ -159,6 +165,17 @@ class BaseTransport(threading.Thread):
self.start() self.start()
def add_server_key(self, key): 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 self.server_key_dict[key.get_name()] = key
def get_server_key(self): def get_server_key(self):
@ -167,7 +184,7 @@ class BaseTransport(threading.Thread):
except KeyError: except KeyError:
return None return None
def load_server_moduli(self, filename=None): def load_server_moduli(filename=None):
""" """
I{(optional)} I{(optional)}
Load a file of prime moduli for use in doing group-exchange key 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. @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 # places to look for the openssh "moduli" file
file_list = [ '/etc/ssh/moduli', '/usr/local/etc/moduli' ] file_list = [ '/etc/ssh/moduli', '/usr/local/etc/moduli' ]
if filename is not None: if filename is not None:
file_list.insert(0, filename) file_list.insert(0, filename)
for fn in file_list: for fn in file_list:
try: try:
self.modulus_pack.read_file(fn) BaseTransport._modulus_pack.read_file(fn)
return True return True
except IOError: except IOError:
pass pass
# none succeeded # none succeeded
self.modulus_pack = None BaseTransport._modulus_pack = None
return False return False
load_server_moduli = staticmethod(load_server_moduli)
def _get_modulus_pack(self): def _get_modulus_pack(self):
"used by KexGex to find primes for group exchange" "used by KexGex to find primes for group exchange"
return self.modulus_pack return self._modulus_pack
def __repr__(self): def __repr__(self):
"""
Returns a string representation of this object, for debugging.
@rtype: string
"""
if not self.active: if not self.active:
return '<paramiko.BaseTransport (unconnected)>' return '<paramiko.BaseTransport (unconnected)>'
out = '<paramiko.BaseTransport' out = '<paramiko.BaseTransport'
@ -285,7 +308,7 @@ class BaseTransport(threading.Thread):
chanid = self.channel_counter chanid = self.channel_counter
self.channel_counter += 1 self.channel_counter += 1
m = Message() m = Message()
m.add_byte(chr(MSG_CHANNEL_OPEN)) m.add_byte(chr(_MSG_CHANNEL_OPEN))
m.add_string(kind) m.add_string(kind)
m.add_int(chanid) m.add_int(chanid)
m.add_int(self.window_size) m.add_int(self.window_size)
@ -293,7 +316,7 @@ class BaseTransport(threading.Thread):
self.channels[chanid] = chan = Channel(chanid) self.channels[chanid] = chan = Channel(chanid)
self.channel_events[chanid] = event = threading.Event() self.channel_events[chanid] = event = threading.Event()
chan._set_transport(self) 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) self._send_message(m)
finally: finally:
self.lock.release() self.lock.release()
@ -311,6 +334,140 @@ class BaseTransport(threading.Thread):
self.lock.release() self.lock.release()
return chan 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): def _unlink_channel(self, chanid):
"used by a Channel to remove itself from the active channel list" "used by a Channel to remove itself from the active channel list"
try: try:
@ -405,7 +562,8 @@ class BaseTransport(threading.Thread):
padding = ord(packet[0]) padding = ord(packet[0])
payload = packet[1:packet_size - padding + 1] payload = packet[1:packet_size - padding + 1]
randpool.add_event(packet[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 = Message(payload[1:])
msg.seqno = self.sequence_number_in msg.seqno = self.sequence_number_in
self.sequence_number_in = (self.sequence_number_in + 1) & 0xffffffffL 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._write_all(self.local_version + '\r\n')
self._check_banner() self._check_banner()
self._send_kex_init() self._send_kex_init()
self.expected_packet = MSG_KEXINIT self.expected_packet = _MSG_KEXINIT
while self.active: while self.active:
ptype, m = self._read_message() ptype, m = self._read_message()
if ptype == MSG_IGNORE: if ptype == _MSG_IGNORE:
continue continue
elif ptype == MSG_DISCONNECT: elif ptype == _MSG_DISCONNECT:
self._parse_disconnect(m) self._parse_disconnect(m)
self.active = False self.active = False
break break
elif ptype == MSG_DEBUG: elif ptype == _MSG_DEBUG:
self._parse_debug(m) self._parse_debug(m)
continue continue
if self.expected_packet != 0: if self.expected_packet != 0:
@ -512,53 +670,34 @@ class BaseTransport(threading.Thread):
else: else:
self._log(WARNING, 'Oops, unhandled type %d' % ptype) self._log(WARNING, 'Oops, unhandled type %d' % ptype)
msg = Message() msg = Message()
msg.add_byte(chr(MSG_UNIMPLEMENTED)) msg.add_byte(chr(_MSG_UNIMPLEMENTED))
msg.add_int(m.seqno) msg.add_int(m.seqno)
self._send_message(msg) self._send_message(msg)
except SSHException, e: except SSHException, e:
self._log(DEBUG, 'Exception: ' + str(e)) self._log(DEBUG, 'Exception: ' + str(e))
self._log(DEBUG, tb_strings()) self._log(DEBUG, tb_strings())
self.saved_exception = e
except EOFError, e: except EOFError, e:
self._log(DEBUG, 'EOF') self._log(DEBUG, 'EOF')
self._log(DEBUG, tb_strings()) self._log(DEBUG, tb_strings())
self.saved_exception = e
except Exception, e: except Exception, e:
self._log(DEBUG, 'Unknown exception: ' + str(e)) self._log(DEBUG, 'Unknown exception: ' + str(e))
self._log(DEBUG, tb_strings()) self._log(DEBUG, tb_strings())
self.saved_exception = e
if self.active: if self.active:
self.active = False self.active = False
if self.completion_event != None: if self.completion_event != None:
self.completion_event.set() self.completion_event.set()
if self.auth_event != None: if self.auth_event != None:
self.auth_event.set() self.auth_event.set()
for e in self.channel_events.values(): for event in self.channel_events.values():
e.set() event.set()
self.sock.close() self.sock.close()
### protocol stages ### 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): def _negotiate_keys(self, m):
# throws SSHException on anything unusual # throws SSHException on anything unusual
@ -615,7 +754,7 @@ class BaseTransport(threading.Thread):
available_server_keys = self.preferred_keys available_server_keys = self.preferred_keys
m = Message() m = Message()
m.add_byte(chr(MSG_KEXINIT)) m.add_byte(chr(_MSG_KEXINIT))
m.add_bytes(randpool.get_bytes(16)) m.add_bytes(randpool.get_bytes(16))
m.add(','.join(self.preferred_kex)) m.add(','.join(self.preferred_kex))
m.add(','.join(available_server_keys)) 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 # 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 # 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. # 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): def _activate_inbound(self):
"switch on newly negotiated encryption parameters for inbound traffic" "switch on newly negotiated encryption parameters for inbound traffic"
@ -750,7 +889,7 @@ class BaseTransport(threading.Thread):
def _activate_outbound(self): def _activate_outbound(self):
"switch on newly negotiated encryption parameters for outbound traffic" "switch on newly negotiated encryption parameters for outbound traffic"
m = Message() m = Message()
m.add_byte(chr(MSG_NEWKEYS)) m.add_byte(chr(_MSG_NEWKEYS))
self._send_message(m) self._send_message(m)
self.block_size_out = self._cipher_info[self.local_cipher]['block-size'] self.block_size_out = self._cipher_info[self.local_cipher]['block-size']
if self.server_mode: if self.server_mode:
@ -769,7 +908,7 @@ class BaseTransport(threading.Thread):
else: else:
self.mac_key_out = self._compute_key('E', self.local_mac_engine.digest_size) self.mac_key_out = self._compute_key('E', self.local_mac_engine.digest_size)
# we always expect to receive NEWKEYS now # we always expect to receive NEWKEYS now
self.expected_packet = MSG_NEWKEYS self.expected_packet = _MSG_NEWKEYS
def _parse_newkeys(self, m): def _parse_newkeys(self, m):
self._log(DEBUG, 'Switch to new keys ...') self._log(DEBUG, 'Switch to new keys ...')
@ -801,7 +940,7 @@ class BaseTransport(threading.Thread):
try: try:
self.lock.acquire() self.lock.acquire()
chan = self.channels[chanid] 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) self._log(INFO, 'Secsh channel %d opened.' % chanid)
if self.channel_events.has_key(chanid): if self.channel_events.has_key(chanid):
self.channel_events[chanid].set() self.channel_events[chanid].set()
@ -815,8 +954,8 @@ class BaseTransport(threading.Thread):
reason = m.get_int() reason = m.get_int()
reason_str = m.get_string() reason_str = m.get_string()
lang = m.get_string() lang = m.get_string()
if CONNECTION_FAILED_CODE.has_key(reason): if _CONNECTION_FAILED_CODE.has_key(reason):
reason_text = CONNECTION_FAILED_CODE[reason] reason_text = _CONNECTION_FAILED_CODE[reason]
else: else:
reason_text = '(unknown code)' reason_text = '(unknown code)'
self._log(INFO, 'Secsh channel %d open FAILED: %s: %s' % (chanid, reason_str, reason_text)) 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() self.lock.release()
return 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): def _parse_channel_open(self, m):
kind = m.get_string() kind = m.get_string()
chanid = m.get_int() chanid = m.get_int()
@ -862,7 +997,7 @@ class BaseTransport(threading.Thread):
reason = self.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED reason = self.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
if reject: if reject:
msg = Message() 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(chanid)
msg.add_int(reason) msg.add_int(reason)
msg.add_string('') msg.add_string('')
@ -873,12 +1008,12 @@ class BaseTransport(threading.Thread):
self.lock.acquire() self.lock.acquire()
self.channels[my_chanid] = chan self.channels[my_chanid] = chan
chan._set_transport(self) chan._set_transport(self)
chan.set_window(self.window_size, self.max_packet_size) chan._set_window(self.window_size, self.max_packet_size)
chan.set_remote_channel(chanid, initial_window_size, max_packet_size) chan._set_remote_channel(chanid, initial_window_size, max_packet_size)
finally: finally:
self.lock.release() self.lock.release()
m = Message() 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(chanid)
m.add_int(my_chanid) m.add_int(my_chanid)
m.add_int(self.window_size) m.add_int(self.window_size)
@ -892,22 +1027,6 @@ class BaseTransport(threading.Thread):
finally: finally:
self.lock.release() 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): def _parse_debug(self, m):
always_display = m.get_boolean() always_display = m.get_boolean()
msg = m.get_string() msg = m.get_string()
@ -915,19 +1034,19 @@ class BaseTransport(threading.Thread):
self._log(DEBUG, 'Debug msg: ' + safe_string(msg)) self._log(DEBUG, 'Debug msg: ' + safe_string(msg))
_handler_table = { _handler_table = {
MSG_NEWKEYS: _parse_newkeys, _MSG_NEWKEYS: _parse_newkeys,
MSG_CHANNEL_OPEN_SUCCESS: _parse_channel_open_success, _MSG_CHANNEL_OPEN_SUCCESS: _parse_channel_open_success,
MSG_CHANNEL_OPEN_FAILURE: _parse_channel_open_failure, _MSG_CHANNEL_OPEN_FAILURE: _parse_channel_open_failure,
MSG_CHANNEL_OPEN: _parse_channel_open, _MSG_CHANNEL_OPEN: _parse_channel_open,
MSG_KEXINIT: _negotiate_keys, _MSG_KEXINIT: _negotiate_keys,
} }
_channel_handler_table = { _channel_handler_table = {
MSG_CHANNEL_SUCCESS: Channel.request_success, _MSG_CHANNEL_SUCCESS: Channel._request_success,
MSG_CHANNEL_FAILURE: Channel.request_failed, _MSG_CHANNEL_FAILURE: Channel._request_failed,
MSG_CHANNEL_DATA: Channel.feed, _MSG_CHANNEL_DATA: Channel._feed,
MSG_CHANNEL_WINDOW_ADJUST: Channel.window_adjust, _MSG_CHANNEL_WINDOW_ADJUST: Channel._window_adjust,
MSG_CHANNEL_REQUEST: Channel.handle_request, _MSG_CHANNEL_REQUEST: Channel._handle_request,
MSG_CHANNEL_EOF: Channel.handle_eof, _MSG_CHANNEL_EOF: Channel._handle_eof,
MSG_CHANNEL_CLOSE: Channel.handle_close, _MSG_CHANNEL_CLOSE: Channel._handle_close,
} }

View File

@ -3,7 +3,7 @@ from distutils.core import setup
longdesc = ''' longdesc = '''
This is a library for making SSH2 connections (client or server). This is a library for making SSH2 connections (client or server).
Emphasis is on using SSH2 as an alternative to SSL for making secure 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. are supported.
(Previous name: secsh) (Previous name: secsh)