summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rwxr-xr-xdemo_sftp.py157
-rw-r--r--paramiko/__init__.py8
-rw-r--r--paramiko/channel.py161
-rw-r--r--paramiko/file.py428
-rw-r--r--paramiko/message.py10
-rw-r--r--paramiko/sftp.py358
-rw-r--r--paramiko/util.py2
7 files changed, 972 insertions, 152 deletions
diff --git a/demo_sftp.py b/demo_sftp.py
new file mode 100755
index 00000000..1f6cd3a6
--- /dev/null
+++ b/demo_sftp.py
@@ -0,0 +1,157 @@
+#!/usr/bin/python
+
+import sys, os, socket, threading, getpass, logging, time, base64, select, termios, tty, traceback
+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
+
+
+##### main demo
+
+# 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)
+
+
+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)
+
+# 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)
+
+try:
+ event = threading.Event()
+ t = paramiko.Transport(sock)
+ t.start_client(event)
+ # print repr(t)
+ event.wait(15)
+ if not t.is_active():
+ print '*** SSH negotiation failed.'
+ sys.exit(1)
+ # print repr(t)
+
+ keys = load_host_keys()
+ keytype, hostkey = t.get_remote_server_key()
+ if not keys.has_key(hostname):
+ print '*** WARNING: Unknown host key!'
+ elif not keys[hostname].has_key(keytype):
+ print '*** WARNING: Unknown host key!'
+ elif keys[hostname][keytype] != hostkey:
+ print '*** WARNING: Host key has changed!!!'
+ sys.exit(1)
+ else:
+ print '*** Host key OK.'
+
+ event.clear()
+
+ # get username
+ if username == '':
+ default_username = getpass.getuser()
+ username = raw_input('Username [%s]: ' % default_username)
+ if len(username) == 0:
+ username = default_username
+
+ # ask for what kind of authentication to try
+ default_auth = 'p'
+ auth = raw_input('Auth by (p)assword, (r)sa key, or (d)ss key? [%s] ' % default_auth)
+ if len(auth) == 0:
+ auth = default_auth
+
+ if auth == 'r':
+ key = paramiko.RSAKey()
+ default_path = os.environ['HOME'] + '/.ssh/id_rsa'
+ path = raw_input('RSA key [%s]: ' % default_path)
+ if len(path) == 0:
+ path = default_path
+ try:
+ key.read_private_key_file(path)
+ except paramiko.PasswordRequiredException:
+ password = getpass.getpass('RSA key password: ')
+ key.read_private_key_file(path, password)
+ t.auth_publickey(username, key, event)
+ elif auth == 'd':
+ key = paramiko.DSSKey()
+ default_path = os.environ['HOME'] + '/.ssh/id_dsa'
+ path = raw_input('DSS key [%s]: ' % default_path)
+ if len(path) == 0:
+ path = default_path
+ try:
+ key.read_private_key_file(path)
+ except paramiko.PasswordRequiredException:
+ password = getpass.getpass('DSS key password: ')
+ key.read_private_key_file(path, password)
+ t.auth_key(username, key, event)
+ else:
+ pw = getpass.getpass('Password for %s@%s: ' % (username, hostname))
+ t.auth_password(username, pw, event)
+
+ event.wait(10)
+ # print repr(t)
+ if not t.is_authenticated():
+ print '*** Authentication failed. :('
+ t.close()
+ sys.exit(1)
+
+ chan = t.open_session()
+ chan.invoke_subsystem('sftp')
+ print '*** SFTP...'
+ sftp = paramiko.SFTP(chan)
+ print repr(sftp.listdir('/tmp'))
+
+ 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/__init__.py b/paramiko/__init__.py
index 9c76b7a5..f8d5cbd2 100644
--- a/paramiko/__init__.py
+++ b/paramiko/__init__.py
@@ -67,27 +67,33 @@ __version__ = "0.9-doduo"
__license__ = "GNU Lesser General Public License (LGPL)"
-import transport, auth_transport, channel, rsakey, dsskey, ssh_exception
+import transport, auth_transport, channel, rsakey, dsskey, message, ssh_exception, sftp
Transport = auth_transport.Transport
Channel = channel.Channel
RSAKey = rsakey.RSAKey
DSSKey = dsskey.DSSKey
SSHException = ssh_exception.SSHException
+Message = message.Message
PasswordRequiredException = ssh_exception.PasswordRequiredException
+SFTP = sftp.SFTP
__all__ = [ 'Transport',
'Channel',
'RSAKey',
'DSSKey',
+ 'Message',
'SSHException',
'PasswordRequiredException',
+ 'SFTP',
'transport',
'auth_transport',
'channel',
'rsakey',
'dsskey',
'pkey',
+ 'message',
'ssh_exception',
+ 'sftp',
'util' ]
diff --git a/paramiko/channel.py b/paramiko/channel.py
index fc521525..c634f93a 100644
--- a/paramiko/channel.py
+++ b/paramiko/channel.py
@@ -26,6 +26,7 @@ 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 file import BufferedFile
import time, threading, logging, socket, os
from logging import DEBUG
@@ -334,7 +335,7 @@ class Channel (object):
@param nbytes: maximum number of bytes to read.
@type nbytes: int
- @return: data
+ @return: data.
@rtype: string
@raise socket.timeout: if no data is ready before the timeout set by
@@ -834,7 +835,7 @@ class Channel (object):
self.in_window_sofar = 0
-class ChannelFile (object):
+class ChannelFile (BufferedFile):
"""
A file-like wrapper around L{Channel}. A ChannelFile is created by calling
L{Channel.makefile} and doesn't have the non-portable side effect of
@@ -846,28 +847,10 @@ class ChannelFile (object):
C{ChannelFile} does nothing but flush the buffer.
"""
- def __init__(self, channel, mode = "r", buf_size = -1):
+ def __init__(self, channel, mode = 'r', bufsize = -1):
self.channel = channel
- self.mode = mode
- if buf_size <= 0:
- self.buf_size = 1024
- self.line_buffered = 0
- elif buf_size == 1:
- self.buf_size = 1
- self.line_buffered = 1
- else:
- self.buf_size = buf_size
- self.line_buffered = 0
- self.wbuffer = ""
- self.rbuffer = ""
- self.readable = ("r" in mode)
- self.writable = ("w" in mode) or ("+" in mode) or ("a" in mode)
- self.universal_newlines = ('U' in mode)
- self.binary = ("b" in mode)
- self.at_trailing_cr = False
- self.name = '<file from ' + repr(self.channel) + '>'
- self.newlines = None
- self.softspace = False
+ BufferedFile.__init__(self)
+ self._set_mode(mode, bufsize)
def __repr__(self):
"""
@@ -877,134 +860,12 @@ class ChannelFile (object):
"""
return '<paramiko.ChannelFile from ' + repr(self.channel) + '>'
- def __iter__(self):
- return self
-
- def next(self):
- line = self.readline()
- if not line:
- raise StopIteration
- return line
-
- def write(self, str):
- if not self.writable:
- raise IOError("file not open for writing")
- if self.buf_size == 0 and not self.line_buffered:
- self.channel.sendall(str)
- return
- self.wbuffer += str
- if self.line_buffered:
- last_newline_pos = self.wbuffer.rfind("\n")
- if last_newline_pos >= 0:
- self.channel.sendall(self.wbuffer[:last_newline_pos+1])
- self.wbuffer = self.wbuffer[last_newline_pos+1:]
- else:
- if len(self.wbuffer) >= self.buf_size:
- self.channel.sendall(self.wbuffer)
- self.wbuffer = ""
- return
-
- def writelines(self, sequence):
- for s in sequence:
- self.write(s)
- return
+ def _read(self, size):
+ return self.channel.recv(size)
- def flush(self):
- self.channel.sendall(self.wbuffer)
- self.wbuffer = ""
- return
+ def _write(self, data):
+ self.channel.sendall(data)
+ return len(data)
- def read(self, size = None):
- if not self.readable:
- raise IOError("file not open for reading")
- if size is None or size < 0:
- result = self.rbuffer
- self.rbuffer = ""
- while not self.channel.eof_received:
- new_data = self.channel.recv(65536)
- if not new_data:
- break
- result += new_data
- return result
- if size <= len(self.rbuffer):
- result = self.rbuffer[:size]
- self.rbuffer = self.rbuffer[size:]
- return result
- while len(self.rbuffer) < size and not self.channel.eof_received:
- new_data = self.channel.recv(max(self.buf_size, size-len(self.rbuffer)))
- if not new_data:
- break
- self.rbuffer += new_data
- result = self.rbuffer[:size]
- self.rbuffer[size:]
- return result
-
- def readline(self, size=None):
- line = self.rbuffer
- while 1:
- if self.at_trailing_cr and (len(line) > 0):
- if line[0] == '\n':
- line = line[1:]
- self.at_trailing_cr = False
- if self.universal_newlines:
- if ('\n' in line) or ('\r' in line):
- break
- else:
- if '\n' in line:
- break
- if size >= 0:
- if len(line) >= size:
- # truncate line and return
- self.rbuffer = line[size:]
- line = line[:size]
- return line
- n = size - len(line)
- else:
- n = 64
- new_data = self.channel.recv(n)
- if not new_data:
- self.rbuffer = ''
- return line
- line += new_data
- # find the newline
- pos = line.find('\n')
- if self.universal_newlines:
- rpos = line.find('\r')
- if (rpos >= 0) and ((rpos < pos) or (pos < 0)):
- pos = rpos
- xpos = pos + 1
- if (line[pos] == '\r') and (xpos < len(line)) and (line[xpos] == '\n'):
- xpos += 1
- self.rbuffer = line[xpos:]
- lf = line[pos:xpos]
- line = line[:xpos]
- if (len(self.rbuffer) == 0) and (lf == '\r'):
- # we could read the line up to a '\r' and there could still be a
- # '\n' following that we read next time. note that and eat it.
- self.at_trailing_cr = True
- # silliness about tracking what kinds of newlines we've seen
- if self.newlines is None:
- self.newlines = lf
- elif (type(self.newlines) is str) and (self.newlines != lf):
- self.newlines = (self.newlines, lf)
- elif lf not in self.newlines:
- self.newlines += (lf,)
- return line
-
- def readlines(self, sizehint = None):
- lines = []
- while 1:
- line = self.readline()
- if not line:
- break
- lines.append(line)
- return lines
-
- def xreadlines(self):
- return self
-
- def close(self):
- self.flush()
- return
# vim: set shiftwidth=4 expandtab :
diff --git a/paramiko/file.py b/paramiko/file.py
new file mode 100644
index 00000000..d19ec30e
--- /dev/null
+++ b/paramiko/file.py
@@ -0,0 +1,428 @@
+#!/usr/bin/python
+
+# Copyright (C) 2003-2004 Robey Pointer <robey@lag.net>
+#
+# This file is part of paramiko.
+#
+# Paramiko is free software; you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation; either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Foobar; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+
+"""
+BufferedFile.
+"""
+
+_FLAG_READ = 0x1
+_FLAG_WRITE = 0x2
+_FLAG_APPEND = 0x4
+_FLAG_BINARY = 0x10
+_FLAG_BUFFERED = 0x20
+_FLAG_LINE_BUFFERED = 0x40
+_FLAG_UNIVERSAL_NEWLINE = 0x80
+
+class BufferedFile (object):
+ """
+ Reusable base class to implement python-style file buffering around a
+ simpler stream
+ """
+
+ _DEFAULT_BUFSIZE = 1024
+
+ SEEK_SET = 0
+ SEEK_CUR = 1
+ SEEK_END = 2
+
+ def __init__(self):
+ self._flags = 0
+ self._bufsize = self._DEFAULT_BUFSIZE
+ self._wbuffer = self._rbuffer = ''
+ self._at_trailing_cr = False
+ self._closed = False
+ # pos - position within the file, according to the user
+ # realpos - position according the OS
+ # (these may be different because we buffer for line reading)
+ self._pos = self._realpos = 0
+
+ def __iter__(self):
+ """
+ Returns an iterator that can be used to iterate over the lines in this
+ file. This iterator happens to return the file itself, since a file is
+ its own iterator.
+
+ @raise: ValueError if the file is closed.
+
+ @return: an interator.
+ @rtype: iterator
+ """
+ if self._closed:
+ raise ValueError('I/O operation on closed file')
+ return self
+
+ def close(self):
+ """
+ Close the file. Future read and write operations will fail.
+ """
+ self.flush()
+ self._closed = True
+
+ def flush(self):
+ """
+ Write out any data in the write buffer. This may do nothing if write
+ buffering is not turned on.
+ """
+ self._write_all(self._wbuffer)
+ self._wbuffer = ''
+ return
+
+ def next(self):
+ """
+ Returns the next line from the input, or raises L{StopIteration} when
+ EOF is hit. Unlike python file objects, it's okay to mix calls to
+ C{next} and L{readline}.
+
+ @raise: StopIteration when the end of the file is reached.
+
+ @return: a line read from the file.
+ @rtype: string
+ """
+ line = self.readline()
+ if not line:
+ raise StopIteration
+ return line
+
+ def read(self, size=None):
+ """
+ Read at most C{size} bytes from the file (less if we hit the end of the
+ file first). If the C{size} argument is negative or omitted, read all
+ the remaining data in the file.
+
+ @param size: maximum number of bytes to read.
+ @type size: int
+ @return: data read from the file, or an empty string if EOF was
+ encountered immediately.
+ @rtype: string
+ """
+ if self._closed:
+ raise IOError('File is closed')
+ if not (self._flags & _FLAG_READ):
+ raise IOError('File not open for reading')
+ if (size is None) or (size < 0):
+ # go for broke
+ result = self.rbuffer
+ self.rbuffer = ''
+ self._pos += len(result)
+ while 1:
+ try:
+ new_data = self._read(self._DEFAULT_BUFSIZE)
+ except EOFError:
+ new_data = None
+ if (new_data is None) or (len(new_data) == 0):
+ break
+ result += new_data
+ self._realpos += len(new_data)
+ self._pos += len(new_data)
+ return result
+ if size <= len(self._rbuffer):
+ result = self._rbuffer[:size]
+ self._rbuffer = self._rbuffer[size:]
+ self._pos += len(result)
+ return result
+ while len(self._rbuffer) < size:
+ try:
+ new_data = self._read(max(self._bufsize, size - len(self._rbuffer)))
+ except EOFError:
+ new_data = None
+ if (new_data is None) or (len(new_data) == 0):
+ break
+ self._rbuffer += new_data
+ self._realpos += len(new_data)
+ result = self._rbuffer[:size]
+ self._rbuffer = self._rbuffer[size:]
+ self._pos += len(result)
+ return result
+
+ def readline(self, size=None):
+ """
+ Read one entire line from the file. A trailing newline character is
+ kept in the string (but may be absent when a file ends with an
+ incomplete line). If the size argument is present and non-negative, it
+ is a maximum byte count (including the trailing newline) and an
+ incomplete line may be returned. An empty string is returned only when
+ EOF is encountered immediately.
+
+ @note: Unlike stdio's C{fgets()}, the returned string contains null
+ characters (C{'\0'}) if they occurred in the input.
+
+ @param size: maximum length of returned string.
+ @type size: int
+ @return: next line of the file, or an empty string if the end of the
+ file has been reached.
+ @rtype: string
+ """
+ # it's almost silly how complex this function is.
+ if self._closed:
+ raise IOError('File is closed')
+ line = self._rbuffer
+ while 1:
+ if self._at_trailing_cr and (self._flags & _FLAG_UNIVERSAL_NEWLINE) and (len(line) > 0):
+ # edge case: the newline may be '\r\n' and we may have read
+ # only the first '\r' last time.
+ if line[0] == '\n':
+ line = line[1:]
+ self._record_newline('\r\n')
+ else:
+ self._record_newline('\r')
+ self._at_trailing_cr = False
+ # check size before looking for a linefeed, in case we already have
+ # enough.
+ if (size is not None) and (size >= 0):
+ if len(line) >= size:
+ # truncate line and return
+ self.rbuffer = line[size:]
+ line = line[:size]
+ self._pos += len(line)
+ return line
+ n = size - len(line)
+ else:
+ n = self._DEFAULT_BUFSIZE
+ if ('\n' in line) or ((self._flags & _FLAG_UNIVERSAL_NEWLINE) and ('\r' in line)):
+ break
+ try:
+ new_data = self._read(n)
+ except EOFError:
+ new_data = None
+ if (new_data is None) or (len(new_data) == 0):
+ self._rbuffer = ''
+ self._pos += len(line)
+ return line
+ line += new_data
+ self._realpos += len(new_data)
+ # find the newline
+ pos = line.find('\n')
+ if self._flags & _FLAG_UNIVERSAL_NEWLINE:
+ rpos = line.find('\r')
+ if (rpos >= 0) and ((rpos < pos) or (pos < 0)):
+ pos = rpos
+ xpos = pos + 1
+ if (line[pos] == '\r') and (xpos < len(line)) and (line[xpos] == '\n'):
+ xpos += 1
+ self._rbuffer = line[xpos:]
+ lf = line[pos:xpos]
+ line = line[:xpos]
+ if (len(self._rbuffer) == 0) and (lf == '\r'):
+ # we could read the line up to a '\r' and there could still be a
+ # '\n' following that we read next time. note that and eat it.
+ self._at_trailing_cr = True
+ else:
+ self._record_newline(lf)
+ self._pos += len(line)
+ return line
+
+ def readlines(self, sizehint=None):
+ """
+ Read all remaining lines using L{readline} and return them as a list.
+ If the optional C{sizehint} argument is present, instead of reading up
+ to EOF, whole lines totalling approximately sizehint bytes (possibly
+ after rounding up to an internal buffer size) are read.
+
+ @param sizehint: desired maximum number of bytes to read.
+ @type sizehint: int
+ @return: list of lines read from the file.
+ @rtype: list
+ """
+ lines = []
+ bytes = 0
+ while 1:
+ line = self.readline()
+ if len(line) == 0:
+ break
+ lines.append(line)
+ bytes += len(line)
+ if (sizehint is not None) and (bytes >= sizehint):
+ break
+ return lines
+
+ def seek(self, offset, whence=0):
+ """
+ Set the file's current position, like stdio's C{fseek}. Not all file
+ objects support seeking.
+
+ @note: If a file is opened in append mode (C{'a'} or C{'a+'}), any seek
+ operations will be undone at the next write (as the file position will
+ move back to the end of the file).
+
+ @param offset: position to move to within the file, relative to
+ C{whence}.
+ @type offset: int
+ @param whence: type of movement: 0 = absolute; 1 = relative to the
+ current position; 2 = relative to the end of the file.
+ @type whence: int
+
+ @raise IOError: if the file doesn't support random access.
+ """
+ raise IOError('File does not support seeking.')
+
+ def tell(self):
+ """
+ Return the file's current position. This may not be accurate or
+ useful if the underlying file doesn't support random access, or was
+ opened in append mode.
+
+ @return: file position (in bytes).
+ @rtype: int
+ """
+ return self._pos
+
+ def write(self, data):
+ """
+ Write data to the file. If write buffering is on (C{bufsize} was
+ specified and non-zero, some or all of the data may not actually be
+ written yet. (Use L{flush} or L{close} to force buffered data to be
+ written out.)
+
+ @param data: data to write.
+ @type data: string
+ """
+ if self._closed:
+ raise IOError('File is closed')
+ if not (self._flags & _FLAG_WRITE):
+ raise IOError('File not open for writing')
+ if not (self._flags & _FLAG_BUFFERED):
+ self._write_all(data)
+ return
+ self._wbuffer += data
+ if self._flags & _FLAG_LINE_BUFFERED:
+ last_newline_pos = self._wbuffer.rfind('\n')
+ if last_newline_pos >= 0:
+ self._write_all(self._wbuffer[:last_newline_pos + 1])
+ self._wbuffer = self._wbuffer[last_newline_pos+1:]
+ else:
+ if len(self._wbuffer) >= self._bufsize:
+ self._write_all(self._wbuffer)
+ self._wbuffer = ''
+ return
+
+ def writelines(self, sequence):
+ """
+ Write a sequence of strings to the file. The sequence can be any
+ iterable object producing strings, typically a list of strings. (The
+ name is intended to match L{readlines}; C{writelines} does not add line
+ separators.)
+
+ @param sequence: an iterable sequence of strings.
+ @type sequence: sequence
+ """
+ for line in sequence:
+ self.write(line)
+ return
+
+ def xreadlines(self):
+ """
+ Identical to C{iter(f)}. This is a deprecated file interface that
+ predates python iterator support.
+
+ @return: an iterator.
+ @rtype: iterator
+ """
+ return self
+
+
+ ### overrides...
+
+
+ def _read(self, size):
+ """
+ I{(subclass override)}
+ Read data from the stream. Return C{None} or raise C{EOFError} to
+ indicate EOF.
+ """
+ raise EOFError()
+
+ def _write(self, data):
+ """
+ I{(subclass override)}
+ Write data into the stream.
+ """
+ raise IOError('write not implemented')
+
+ def _get_size(self):
+ """
+ I{(subclass override)}
+ Return the size of the file. This is called from within L{_set_mode}
+ if the file is opened in append mode, so the file position can be
+ tracked and L{seek} and L{tell} will work correctly. If the file is
+ a stream that can't be randomly accessed, you don't need to override
+ this method,
+ """
+ return 0
+
+
+ ### internals...
+
+
+ def _set_mode(self, mode='r', bufsize=-1):
+ """
+ Subclasses call this method to initialize the BufferedFile.
+ """
+ if bufsize == 1:
+ # apparently, line buffering only affects writes. reads are only
+ # buffered if you call readline (directly or indirectly: iterating
+ # over a file will indirectly call readline).
+ self._flags |= _FLAG_BUFFERED | _FLAG_LINE_BUFFERED
+ elif bufsize > 1:
+ self._bufsize = bufsize
+ self._flags |= _FLAG_BUFFERED
+ if ('r' in mode) or ('+' in mode):
+ self._flags |= _FLAG_READ
+ if ('w' in mode) or ('+' in mode):
+ self._flags |= _FLAG_WRITE
+ if ('a' in mode):
+ self._flags |= _FLAG_WRITE | _FLAG_APPEND
+ self._size = self._get_size()
+ self._pos = self._realpos = self._size
+ if ('b' in mode):
+ self._flags |= _FLAG_BINARY
+ if ('U' in mode):
+ self._flags |= _FLAG_UNIVERSAL_NEWLINE
+ # built-in file objects have this attribute to store which kinds of
+ # line terminations they've seen:
+ # <http://www.python.org/doc/current/lib/built-in-funcs.html>
+ self.newlines = None
+
+ def _write_all(self, data):
+ # the underlying stream may be something that does partial writes (like
+ # a socket).
+ total = len(data)
+ while data:
+ count = self._write(data)
+ data = data[count:]
+ if self._flags & _FLAG_APPEND:
+ self._size += total
+ self._pos = self._realpos = self._size
+ else:
+ self._pos += total
+ self._realpos += total
+ return None
+
+ def _record_newline(self, newline):
+ # silliness about tracking what kinds of newlines we've seen.
+ # i don't understand why it can be None, a string, or a tuple, instead
+ # of just always being a tuple, but we'll emulate that behavior anyway.
+ if not (self._flags & _FLAG_UNIVERSAL_NEWLINE):
+ return
+ if self.newlines is None:
+ self.newlines = newline
+ elif (type(self.newlines) is str) and (self.newlines != newline):
+ self.newlines = (self.newlines, newline)
+ elif newline not in self.newlines:
+ self.newlines += (newline,)
diff --git a/paramiko/message.py b/paramiko/message.py
index c4856623..78fd4d78 100644
--- a/paramiko/message.py
+++ b/paramiko/message.py
@@ -207,6 +207,16 @@ class Message (object):
self.packet = self.packet + struct.pack('>I', n)
return self
+ def add_int64(self, n):
+ """
+ Add a 64-bit int to the stream.
+
+ @param n: long int to add.
+ @type n: long
+ """
+ self.packet = self.packet + struct.pack('>Q', n)
+ return self
+
def add_mpint(self, z):
"this only works on positive numbers"
self.add_string(deflate_long(z))
diff --git a/paramiko/sftp.py b/paramiko/sftp.py
new file mode 100644
index 00000000..38876f5d
--- /dev/null
+++ b/paramiko/sftp.py
@@ -0,0 +1,358 @@
+#!/usr/bin/python
+
+# Copyright (C) 2003-2004 Robey Pointer <robey@lag.net>
+#
+# This file is part of paramiko.
+#
+# Paramiko is free software; you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation; either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Foobar; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+
+import struct, logging, socket
+from util import format_binary, tb_strings
+from channel import Channel
+from message import Message
+from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL
+from file import BufferedFile
+
+CMD_INIT, CMD_VERSION, CMD_OPEN, CMD_CLOSE, CMD_READ, CMD_WRITE, CMD_LSTAT, CMD_FSTAT, CMD_SETSTAT, \
+ CMD_FSETSTAT, CMD_OPENDIR, CMD_READDIR, CMD_REMOVE, CMD_MKDIR, CMD_RMDIR, CMD_REALPATH, \
+ CMD_STAT, CMD_RENAME, CMD_READLINK, CMD_SYMLINK = range(1, 21)
+CMD_STATUS, CMD_HANDLE, CMD_DATA, CMD_NAME, CMD_ATTRS = range(101, 106)
+CMD_EXTENDED, CMD_EXTENDED_REPLY = range(200, 202)
+
+FX_OK = 0
+FX_EOF, FX_NO_SUCH_FILE, FX_PERMISSION_DENIED, FX_FAILURE, FX_BAD_MESSAGE, FX_NO_CONNECTION, \
+ FX_CONNECTION_LOST, FX_OP_UNSUPPORTED = range(1, 9)
+
+VERSION = 3
+
+
+class SFTPAttributes (object):
+
+ FLAG_SIZE = 1
+ FLAG_UIDGID = 2
+ FLAG_PERMISSIONS = 4
+ FLAG_AMTIME = 8
+ FLAG_EXTENDED = 0x80000000L
+
+ def __init__(self, msg=None):
+ self.flags = 0
+ self.attr = {}
+ if msg is not None:
+ self.unpack(msg)
+
+ def unpack(self, msg):
+ self.flags = msg.get_int()
+ if self.flags & self.FLAG_SIZE:
+ self.size = msg.get_int64()
+ if self.flags & self.FLAG_UIDGID:
+ self.uid = msg.get_int()
+ self.gid = msg.get_int()
+ if self.flags & self.FLAG_PERMISSIONS:
+ self.permissions = msg.get_int()
+ if self.flags & self.FLAG_AMTIME:
+ self.atime = msg.get_int()
+ self.mtime = msg.get_int()
+ if self.flags & self.FLAG_EXTENDED:
+ count = msg.get_int()
+ for i in range(count):
+ self.attr[msg.get_string()] = msg.get_string()
+ return msg.get_remainder()
+
+ def pack(self, msg):
+ self.flags = 0
+ if hasattr(self, 'size'):
+ self.flags |= self.FLAG_SIZE
+ if hasattr(self, 'uid') or hasattr(self, 'gid'):
+ self.flags |= self.FLAG_UIDGID
+ if hasattr(self, 'permissions'):
+ self.flags |= self.FLAG_PERMISSIONS
+ if hasattr(self, 'atime') or hasattr(self, 'mtime'):
+ self.flags |= self.FLAG_AMTIME
+ if len(self.attr) > 0:
+ self.flags |= self.FLAG_EXTENDED
+ msg.add_int(self.flags)
+ if self.flags & self.FLAG_SIZE:
+ msg.add_int64(self.size)
+ if self.flags & self.FLAG_UIDGID:
+ msg.add_int(getattr(self, 'uid', 0))
+ msg.add_int(getattr(self, 'gid', 0))
+ if self.flags & self.FLAG_PERMISSIONS:
+ msg.add_int(self.permissions)
+ if self.flags & self.FLAG_AMTIME:
+ msg.add_int(getattr(self, 'atime', 0))
+ msg.add_int(getattr(self, 'mtime', 0))
+ if self.flags & self.FLAG_EXTENDED:
+ msg.add_int(len(self.attr))
+ for key, val in self.attr:
+ msg.add_string(key)
+ msg.add_string(val)
+ return
+
+
+class SFTPError (Exception):
+ pass
+
+
+class SFTPFile (BufferedFile):
+ def __init__(self, sftp, handle, mode='r', bufsize=-1):
+ BufferedFile.__init__(self)
+ self.sftp = sftp
+ self.handle = handle
+ BufferedFile._set_mode(self, mode, bufsize)
+
+ def _get_size(self):
+ t, msg = self.sftp._request(CMD_FSTAT, self.handle)
+ if t != CMD_ATTRS:
+ raise SFTPError('Expected attrs')
+ attr = SFTPAttributes()
+ attr.unpack(msg)
+ try:
+ return attr.size
+ except:
+ return 0
+
+ def close(self):
+ BufferedFile.close(self)
+ self.sftp._request(CMD_CLOSE, self.handle)
+
+ def _read(self, size):
+ t, msg = self.sftp._request(CMD_READ, self.handle, long(self._realpos), int(size))
+ if t != CMD_DATA:
+ raise SFTPError('Expected data')
+ return msg.get_string()
+
+ def _write(self, data):
+ t, msg = self.sftp._request(CMD_WRITE, self.handle, long(self._realpos), str(data))
+ return len(data)
+
+ def seek(self, offset, whence=0):
+ if whence == self.SEEK_SET:
+ self._realpos = self._pos = offset
+ elif whence == self.SEEK_CUR:
+ self._realpos += offset
+ self._pos += offset
+ else:
+ self._realpos = self._pos = self._get_size() + offset
+ self._rbuffer = self._wbuffer = ''
+
+
+class SFTP (object):
+ def __init__(self, sock):
+ self.sock = sock
+ self.ultra_debug = 1
+ self.request_number = 1
+ if type(sock) is Channel:
+ self.logger = logging.getLogger('paramiko.chan.' + sock.get_name() + '.sftp')
+ else:
+ self.logger = logging.getLogger('paramiko.sftp')
+ # protocol: (maybe should move to a different method)
+ self._send_packet(CMD_INIT, struct.pack('>I', VERSION))
+ t, data = self._read_packet()
+ if t != CMD_VERSION:
+ raise SFTPError('Incompatible sftp protocol')
+ version = struct.unpack('>I', data[:4])[0]
+ if version != VERSION:
+ raise SFTPError('Incompatible sftp protocol')
+
+ def from_transport(selfclass, t):
+ chan = t.open_session()
+ if chan is None:
+ return None
+ chan.invoke_subsystem('sftp')
+ return selfclass(chan)
+ from_transport = classmethod(from_transport)
+
+ def listdir(self, path):
+ t, msg = self._request(CMD_OPENDIR, path)
+ if t != CMD_HANDLE:
+ raise SFTPError('Expected handle')
+ handle = msg.get_string()
+ filelist = []
+ while 1:
+ try:
+ t, msg = self._request(CMD_READDIR, handle)
+ except EOFError, e:
+ # done with handle
+ break
+ if t != CMD_NAME:
+ raise SFTPError('Expected name response')
+ count = msg.get_int()
+ for i in range(count):
+ filename = msg.get_string()
+ longname = msg.get_string()
+ attr = SFTPAttributes(msg)
+ if (filename != '.') and (filename != '..'):
+ filelist.append(filename)
+ # currently we ignore the rest
+ self._request(CMD_CLOSE, handle)
+ return filelist
+
+ def open(self, filename, mode='r', bufsize=-1):
+ imode = 0
+ if ('r' in mode) or ('+' in mode):
+ imode |= self._FXF_READ
+ if ('w' in mode) or ('+' in mode):
+ imode |= self._FXF_WRITE
+ if ('w' in mode):
+ imode |= self._FXF_CREATE | self._FXF_TRUNC
+ if ('a' in mode):
+ imode |= self._FXF_APPEND
+ attrblock = SFTPAttributes()
+ t, msg = self._request(CMD_OPEN, filename, imode, attrblock)
+ if t != CMD_HANDLE:
+ raise SFTPError('Expected handle')
+ handle = msg.get_string()
+ return SFTPFile(self, handle, mode, bufsize)
+
+ def remove(self, path):
+ """
+ Remove the file at the given path.
+
+ @param path: path (absolute or relative) of the file to remove.
+ @type path: string
+
+ @raise IOError: if the path refers to a folder (directory). Use
+ L{rmdir} to remove a folder.
+ """
+ self._request(CMD_REMOVE, path)
+
+ unlink = remove
+
+ def rename(self, oldpath, newpath):
+ """
+ Rename a file or folder from C{oldpath} to C{newpath}.
+
+ @param oldpath: existing name of the file or folder.
+ @type oldpath: string
+ @param newpath: new name for the file or folder.
+ @type newpath: string
+
+ @raise IOError: if C{newpath} is a folder, or something else goes
+ wrong.
+ """
+ self._request(CMD_RENAME, oldpath, newpath)
+
+ def mkdir(self, path, mode=0777):
+ """
+ Create a folder (directory) named C{path} with numeric mode C{mode}.
+ The default mode is 0777 (octal). On some systems, mode is ignored.
+ Where it is used, the current umask value is first masked out.
+
+ @param path: name of the folder to create.
+ @type path: string
+ @param mode: permissions (posix-style) for the newly-created folder.
+ @type mode: int
+ """
+ attr = SFTPAttributes()
+ attr.permissions = mode
+ self._request(CMD_MKDIR, path, attr)
+
+
+ ### internals...
+
+
+ _FXF_READ = 0x1
+ _FXF_WRITE = 0x2
+ _FXF_APPEND = 0x4
+ _FXF_CREATE = 0x8
+ _FXF_TRUNC = 0x10
+ _FXF_EXCL = 0x20
+
+
+ def _log(self, level, msg):
+ if type(msg) == type([]):
+ for m in msg:
+ self.logger.log(level, m)
+ else:
+ self.logger.log(level, msg)
+
+ def _write_all(self, out):
+ while len(out) > 0:
+ n = self.sock.send(out)
+ if n <= 0:
+ raise EOFError()
+ if n == len(out):
+ return
+ out = out[n:]
+ return
+
+ def _read_all(self, n):
+ out = ''
+ while n > 0:
+ try:
+ x = self.sock.recv(n)
+ if len(x) == 0:
+ raise EOFError()
+ out += x
+ n -= len(x)
+ except socket.timeout:
+ if not self.active:
+ raise EOFError()
+ return out
+
+ def _send_packet(self, t, packet):
+ out = struct.pack('>I', len(packet) + 1) + chr(t) + packet
+ if self.ultra_debug:
+ self._log(DEBUG, format_binary(out, 'OUT: '))
+ self._write_all(out)
+
+ def _read_packet(self):
+ size = struct.unpack('>I', self._read_all(4))[0]
+ data = self._read_all(size)
+ if self.ultra_debug:
+ self._log(DEBUG, format_binary(data, 'IN: '));
+ if size > 0:
+ return ord(data[0]), data[1:]
+ return 0, ''
+
+ def _request(self, t, *arg):
+ msg = Message()
+ msg.add_int(self.request_number)
+ for item in arg:
+ if type(item) is int:
+ msg.add_int(item)
+ elif type(item) is long:
+ msg.add_int64(item)
+ elif type(item) is str:
+ msg.add_string(item)
+ elif type(item) is SFTPAttributes:
+ item.pack(msg)
+ else:
+ raise Exception('unknown type for ' + repr(item) + ' type ' + repr(type(item)))
+ self._send_packet(t, str(msg))
+ t, data = self._read_packet()
+ msg = Message(data)
+ num = msg.get_int()
+ if num != self.request_number:
+ raise SFTPError('Expected response #%d, got response #%d' % (self.request_number, num))
+ self.request_number += 1
+ if t == CMD_STATUS:
+ self._convert_status(msg)
+ return t, msg
+
+ def _convert_status(self, msg):
+ """
+ Raises EOFError or IOError on error status; otherwise does nothing.
+ """
+ code = msg.get_int()
+ text = msg.get_string()
+ if code == FX_OK:
+ return
+ elif code == FX_EOF:
+ raise EOFError(text)
+ else:
+ raise IOError(text)
+
+
diff --git a/paramiko/util.py b/paramiko/util.py
index e36b3640..cc71c6c8 100644
--- a/paramiko/util.py
+++ b/paramiko/util.py
@@ -92,7 +92,7 @@ def format_binary(data, prefix=''):
def format_binary_line(data):
left = ' '.join(['%02X' % ord(c) for c in data])
- right = ''.join([('.%c..' % c)[(ord(c)+61)//94] for c in data])
+ right = ''.join([('.%c..' % c)[(ord(c)+63)//95] for c in data])
return '%-50s %s' % (left, right)
def hexify(s):