diff options
Diffstat (limited to 'src/main/java/ch/ethz/ssh2/SFTPv3Client.java')
-rw-r--r-- | src/main/java/ch/ethz/ssh2/SFTPv3Client.java | 1622 |
1 files changed, 1622 insertions, 0 deletions
diff --git a/src/main/java/ch/ethz/ssh2/SFTPv3Client.java b/src/main/java/ch/ethz/ssh2/SFTPv3Client.java new file mode 100644 index 0000000..f3f1843 --- /dev/null +++ b/src/main/java/ch/ethz/ssh2/SFTPv3Client.java @@ -0,0 +1,1622 @@ +/* + * Copyright (c) 2006-2011 Christian Plattner. All rights reserved. + * Please refer to the LICENSE.txt for licensing details. + */ +package ch.ethz.ssh2; + +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.nio.charset.UnsupportedCharsetException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Vector; + +import ch.ethz.ssh2.channel.Channel; +import ch.ethz.ssh2.log.Logger; +import ch.ethz.ssh2.packets.TypesReader; +import ch.ethz.ssh2.packets.TypesWriter; +import ch.ethz.ssh2.sftp.AttribFlags; +import ch.ethz.ssh2.sftp.ErrorCodes; +import ch.ethz.ssh2.sftp.Packet; + +/** + * A <code>SFTPv3Client</code> represents a SFTP (protocol version 3) + * client connection tunnelled over a SSH-2 connection. This is a very simple + * (synchronous) implementation. + * <p/> + * Basically, most methods in this class map directly to one of + * the packet types described in draft-ietf-secsh-filexfer-02.txt. + * <p/> + * Note: this is experimental code. + * <p/> + * Error handling: the methods of this class throw IOExceptions. However, unless + * there is catastrophic failure, exceptions of the type {@link SFTPv3Client} will + * be thrown (a subclass of IOException). Therefore, you can implement more verbose + * behavior by checking if a thrown exception if of this type. If yes, then you + * can cast the exception and access detailed information about the failure. + * <p/> + * Notes about file names, directory names and paths, copy-pasted + * from the specs: + * <ul> + * <li>SFTP v3 represents file names as strings. File names are + * assumed to use the slash ('/') character as a directory separator.</li> + * <li>File names starting with a slash are "absolute", and are relative to + * the root of the file system. Names starting with any other character + * are relative to the user's default directory (home directory).</li> + * <li>Servers SHOULD interpret a path name component ".." as referring to + * the parent directory, and "." as referring to the current directory. + * If the server implementation limits access to certain parts of the + * file system, it must be extra careful in parsing file names when + * enforcing such restrictions. There have been numerous reported + * security bugs where a ".." in a path name has allowed access outside + * the intended area.</li> + * <li>An empty path name is valid, and it refers to the user's default + * directory (usually the user's home directory).</li> + * </ul> + * <p/> + * If you are still not tired then please go on and read the comment for + * {@link #setCharset(String)}. + * + * @author Christian Plattner, plattner@inf.ethz.ch + * @version $Id: SFTPv3Client.java 46 2011-07-06 08:40:29Z dkocher@sudo.ch $ + */ +public class SFTPv3Client +{ + private static final Logger log = Logger.getLogger(SFTPv3Client.class); + + private Session sess; + + private InputStream is; + private OutputStream os; + + private int protocol_version = 0; + + private int next_request_id = 1000; + + private String charsetName = null; + + /** + * + */ + private PacketListener listener; + + /** + * Create a SFTP v3 client. + * + * @param conn The underlying SSH-2 connection to be used. + * @throws IOException + */ + public SFTPv3Client(Connection conn, PacketListener listener) throws IOException + { + if (conn == null) + { + throw new IllegalArgumentException("Cannot accept null argument!"); + } + + this.listener = listener; + + log.debug("Opening session and starting SFTP subsystem."); + sess = conn.openSession(); + sess.startSubSystem("sftp"); + + is = sess.getStdout(); + os = new BufferedOutputStream(sess.getStdin(), 2048); + + if (is == null) + { + throw new IOException("There is a problem with the streams of the underlying channel."); + } + + init(); + } + + /** + * Create a SFTP v3 client. + * + * @param conn The underlying SSH-2 connection to be used. + * @throws IOException + */ + public SFTPv3Client(Connection conn) throws IOException + { + this(conn, new PacketListener() + { + public void read(String packet) + { + log.debug("Read packet " + packet); + } + + public void write(String packet) + { + log.debug("Write packet " + packet); + } + }); + } + + /** + * Set the charset used to convert between Java Unicode Strings and byte encodings + * used by the server for paths and file names. Unfortunately, the SFTP v3 draft + * says NOTHING about such conversions (well, with the exception of error messages + * which have to be in UTF-8). Newer drafts specify to use UTF-8 for file names + * (if I remember correctly). However, a quick test using OpenSSH serving a EXT-3 + * filesystem has shown that UTF-8 seems to be a bad choice for SFTP v3 (tested with + * filenames containing german umlauts). "windows-1252" seems to work better for Europe. + * Luckily, "windows-1252" is the platform default in my case =). + * <p/> + * If you don't set anything, then the platform default will be used (this is the default + * behavior). + * + * @param charset the name of the charset to be used or <code>null</code> to use the platform's + * default encoding. + * @throws IOException + * @see #getCharset() + */ + public void setCharset(String charset) throws IOException + { + if (charset == null) + { + charsetName = charset; + return; + } + + try + { + Charset.forName(charset); + } + catch (UnsupportedCharsetException e) + { + throw (IOException) new IOException("This charset is not supported").initCause(e); + } + charsetName = charset; + } + + /** + * The currently used charset for filename encoding/decoding. + * + * @return The name of the charset (<code>null</code> if the platform's default charset is being used) + * @see #setCharset(String) + */ + public String getCharset() + { + return charsetName; + } + + private void checkHandleValidAndOpen(SFTPv3FileHandle handle) throws IOException + { + if (handle.client != this) + { + throw new IOException("The file handle was created with another SFTPv3FileHandle instance."); + } + + if (handle.isClosed) + { + throw new IOException("The file handle is closed."); + } + } + + private void sendMessage(int type, int requestId, byte[] msg, int off, int len) throws IOException + { + listener.write(Packet.forName(type)); + + int msglen = len + 1; + + if (type != Packet.SSH_FXP_INIT) + { + msglen += 4; + } + + os.write(msglen >> 24); + os.write(msglen >> 16); + os.write(msglen >> 8); + os.write(msglen); + os.write(type); + + if (type != Packet.SSH_FXP_INIT) + { + os.write(requestId >> 24); + os.write(requestId >> 16); + os.write(requestId >> 8); + os.write(requestId); + } + + os.write(msg, off, len); + os.flush(); + } + + private void sendMessage(int type, int requestId, byte[] msg) throws IOException + { + sendMessage(type, requestId, msg, 0, msg.length); + } + + private void readBytes(byte[] buff, int pos, int len) throws IOException + { + while (len > 0) + { + int count = is.read(buff, pos, len); + if (count < 0) + { + throw new IOException("Unexpected end of sftp stream."); + } + if ((count == 0) || (count > len)) + { + throw new IOException("Underlying stream implementation is bogus!"); + } + len -= count; + pos += count; + } + } + + /** + * Read a message and guarantee that the <b>contents</b> is not larger than + * <code>maxlen</code> bytes. + * <p/> + * Note: receiveMessage(34000) actually means that the message may be up to 34004 + * bytes (the length attribute preceeding the contents is 4 bytes). + * + * @param maxlen + * @return the message contents + * @throws IOException + */ + private byte[] receiveMessage(int maxlen) throws IOException + { + byte[] msglen = new byte[4]; + + readBytes(msglen, 0, 4); + + int len = (((msglen[0] & 0xff) << 24) | ((msglen[1] & 0xff) << 16) | ((msglen[2] & 0xff) << 8) | (msglen[3] & 0xff)); + + if ((len > maxlen) || (len <= 0)) + { + throw new IOException("Illegal sftp packet len: " + len); + } + + byte[] msg = new byte[len]; + + readBytes(msg, 0, len); + + return msg; + } + + private int generateNextRequestID() + { + synchronized (this) + { + return next_request_id++; + } + } + + private void closeHandle(byte[] handle) throws IOException + { + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(handle, 0, handle.length); + + sendMessage(Packet.SSH_FXP_CLOSE, req_id, tw.getBytes()); + + expectStatusOKMessage(req_id); + } + + private SFTPv3FileAttributes readAttrs(TypesReader tr) throws IOException + { + /* + * uint32 flags + * uint64 size present only if flag SSH_FILEXFER_ATTR_SIZE + * uint32 uid present only if flag SSH_FILEXFER_ATTR_V3_UIDGID + * uint32 gid present only if flag SSH_FILEXFER_ATTR_V3_UIDGID + * uint32 permissions present only if flag SSH_FILEXFER_ATTR_PERMISSIONS + * uint32 atime present only if flag SSH_FILEXFER_ATTR_V3_ACMODTIME + * uint32 mtime present only if flag SSH_FILEXFER_ATTR_V3_ACMODTIME + * uint32 extended_count present only if flag SSH_FILEXFER_ATTR_EXTENDED + * string extended_type + * string extended_data + * ... more extended data (extended_type - extended_data pairs), + * so that number of pairs equals extended_count + */ + + SFTPv3FileAttributes fa = new SFTPv3FileAttributes(); + + int flags = tr.readUINT32(); + + if ((flags & AttribFlags.SSH_FILEXFER_ATTR_SIZE) != 0) + { + log.debug("SSH_FILEXFER_ATTR_SIZE"); + fa.size = tr.readUINT64(); + } + + if ((flags & AttribFlags.SSH_FILEXFER_ATTR_V3_UIDGID) != 0) + { + log.debug("SSH_FILEXFER_ATTR_V3_UIDGID"); + fa.uid = tr.readUINT32(); + fa.gid = tr.readUINT32(); + } + + if ((flags & AttribFlags.SSH_FILEXFER_ATTR_PERMISSIONS) != 0) + { + log.debug("SSH_FILEXFER_ATTR_PERMISSIONS"); + fa.permissions = tr.readUINT32(); + } + + if ((flags & AttribFlags.SSH_FILEXFER_ATTR_V3_ACMODTIME) != 0) + { + log.debug("SSH_FILEXFER_ATTR_V3_ACMODTIME"); + fa.atime = tr.readUINT32(); + fa.mtime = tr.readUINT32(); + + } + + if ((flags & AttribFlags.SSH_FILEXFER_ATTR_EXTENDED) != 0) + { + int count = tr.readUINT32(); + + log.debug("SSH_FILEXFER_ATTR_EXTENDED (" + count + ")"); + /* Read it anyway to detect corrupt packets */ + + while (count > 0) + { + tr.readByteString(); + tr.readByteString(); + count--; + } + } + + return fa; + } + + /** + * Retrieve the file attributes of an open file. + * + * @param handle a SFTPv3FileHandle handle. + * @return a SFTPv3FileAttributes object. + * @throws IOException + */ + public SFTPv3FileAttributes fstat(SFTPv3FileHandle handle) throws IOException + { + checkHandleValidAndOpen(handle); + + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(handle.fileHandle, 0, handle.fileHandle.length); + + log.debug("Sending SSH_FXP_FSTAT..."); + sendMessage(Packet.SSH_FXP_FSTAT, req_id, tw.getBytes()); + + byte[] resp = receiveMessage(34000); + + TypesReader tr = new TypesReader(resp); + + int t = tr.readByte(); + listener.read(Packet.forName(t)); + + int rep_id = tr.readUINT32(); + if (rep_id != req_id) + { + throw new IOException("The server sent an invalid id field."); + } + + if (t == Packet.SSH_FXP_ATTRS) + { + return readAttrs(tr); + } + + if (t != Packet.SSH_FXP_STATUS) + { + throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")"); + } + + int errorCode = tr.readUINT32(); + String errorMessage = tr.readString(); + listener.read(errorMessage); + throw new SFTPException(errorMessage, errorCode); + } + + private SFTPv3FileAttributes statBoth(String path, int statMethod) throws IOException + { + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(path, charsetName); + + log.debug("Sending SSH_FXP_STAT/SSH_FXP_LSTAT..."); + sendMessage(statMethod, req_id, tw.getBytes()); + + byte[] resp = receiveMessage(34000); + + TypesReader tr = new TypesReader(resp); + + int t = tr.readByte(); + listener.read(Packet.forName(t)); + + int rep_id = tr.readUINT32(); + if (rep_id != req_id) + { + throw new IOException("The server sent an invalid id field."); + } + + if (t == Packet.SSH_FXP_ATTRS) + { + return readAttrs(tr); + } + + if (t != Packet.SSH_FXP_STATUS) + { + throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")"); + } + + int errorCode = tr.readUINT32(); + String errorMessage = tr.readString(); + listener.read(errorMessage); + throw new SFTPException(errorMessage, errorCode); + } + + /** + * Retrieve the file attributes of a file. This method + * follows symbolic links on the server. + * + * @param path See the {@link SFTPv3Client comment} for the class for more details. + * @return a SFTPv3FileAttributes object. + * @throws IOException + * @see #lstat(String) + */ + public SFTPv3FileAttributes stat(String path) throws IOException + { + return statBoth(path, Packet.SSH_FXP_STAT); + } + + /** + * Retrieve the file attributes of a file. This method + * does NOT follow symbolic links on the server. + * + * @param path See the {@link SFTPv3Client comment} for the class for more details. + * @return a SFTPv3FileAttributes object. + * @throws IOException + * @see #stat(String) + */ + public SFTPv3FileAttributes lstat(String path) throws IOException + { + return statBoth(path, Packet.SSH_FXP_LSTAT); + } + + /** + * Read the target of a symbolic link. Note: OpenSSH (as of version 4.4) gets very upset + * (SSH_FX_BAD_MESSAGE error) if you want to read the target of a file that is not a + * symbolic link. Better check first with {@link #lstat(String)}. + * + * @param path See the {@link SFTPv3Client comment} for the class for more details. + * @return The target of the link. + * @throws IOException + */ + public String readLink(String path) throws IOException + { + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(path, charsetName); + + log.debug("Sending SSH_FXP_READLINK..."); + sendMessage(Packet.SSH_FXP_READLINK, req_id, tw.getBytes()); + + byte[] resp = receiveMessage(34000); + + TypesReader tr = new TypesReader(resp); + + int t = tr.readByte(); + listener.read(Packet.forName(t)); + + int rep_id = tr.readUINT32(); + if (rep_id != req_id) + { + throw new IOException("The server sent an invalid id field."); + } + + if (t == Packet.SSH_FXP_NAME) + { + int count = tr.readUINT32(); + + if (count != 1) + { + throw new IOException("The server sent an invalid SSH_FXP_NAME packet."); + } + + return tr.readString(charsetName); + } + + if (t != Packet.SSH_FXP_STATUS) + { + throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")"); + } + + int errorCode = tr.readUINT32(); + String errorMessage = tr.readString(); + listener.read(errorMessage); + throw new SFTPException(errorMessage, errorCode); + } + + private void expectStatusOKMessage(int id) throws IOException + { + byte[] resp = receiveMessage(34000); + + TypesReader tr = new TypesReader(resp); + + int t = tr.readByte(); + listener.read(Packet.forName(t)); + + int rep_id = tr.readUINT32(); + if (rep_id != id) + { + throw new IOException("The server sent an invalid id field."); + } + + if (t != Packet.SSH_FXP_STATUS) + { + throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")"); + } + + int errorCode = tr.readUINT32(); + + if (errorCode == ErrorCodes.SSH_FX_OK) + { + return; + } + String errorMessage = tr.readString(); + listener.read(errorMessage); + throw new SFTPException(errorMessage, errorCode); + } + + /** + * Modify the attributes of a file. Used for operations such as changing + * the ownership, permissions or access times, as well as for truncating a file. + * + * @param path See the {@link SFTPv3Client comment} for the class for more details. + * @param attr A SFTPv3FileAttributes object. Specifies the modifications to be + * made to the attributes of the file. Empty fields will be ignored. + * @throws IOException + */ + public void setstat(String path, SFTPv3FileAttributes attr) throws IOException + { + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(path, charsetName); + tw.writeBytes(createAttrs(attr)); + + log.debug("Sending SSH_FXP_SETSTAT..."); + sendMessage(Packet.SSH_FXP_SETSTAT, req_id, tw.getBytes()); + + expectStatusOKMessage(req_id); + } + + /** + * Modify the attributes of a file. Used for operations such as changing + * the ownership, permissions or access times, as well as for truncating a file. + * + * @param handle a SFTPv3FileHandle handle + * @param attr A SFTPv3FileAttributes object. Specifies the modifications to be + * made to the attributes of the file. Empty fields will be ignored. + * @throws IOException + */ + public void fsetstat(SFTPv3FileHandle handle, SFTPv3FileAttributes attr) throws IOException + { + checkHandleValidAndOpen(handle); + + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(handle.fileHandle, 0, handle.fileHandle.length); + tw.writeBytes(createAttrs(attr)); + + log.debug("Sending SSH_FXP_FSETSTAT..."); + sendMessage(Packet.SSH_FXP_FSETSTAT, req_id, tw.getBytes()); + + expectStatusOKMessage(req_id); + } + + /** + * Create a symbolic link on the server. Creates a link "src" that points + * to "target". + * + * @param src See the {@link SFTPv3Client comment} for the class for more details. + * @param target See the {@link SFTPv3Client comment} for the class for more details. + * @throws IOException + */ + public void createSymlink(String src, String target) throws IOException + { + int req_id = generateNextRequestID(); + + /* Either I am too stupid to understand the SFTP draft + * or the OpenSSH guys changed the semantics of src and target. + */ + + TypesWriter tw = new TypesWriter(); + tw.writeString(target, charsetName); + tw.writeString(src, charsetName); + + log.debug("Sending SSH_FXP_SYMLINK..."); + sendMessage(Packet.SSH_FXP_SYMLINK, req_id, tw.getBytes()); + + expectStatusOKMessage(req_id); + } + + /** + * Have the server canonicalize any given path name to an absolute path. + * This is useful for converting path names containing ".." components or + * relative pathnames without a leading slash into absolute paths. + * + * @param path See the {@link SFTPv3Client comment} for the class for more details. + * @return An absolute path. + * @throws IOException + */ + public String canonicalPath(String path) throws IOException + { + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(path, charsetName); + + log.debug("Sending SSH_FXP_REALPATH..."); + sendMessage(Packet.SSH_FXP_REALPATH, req_id, tw.getBytes()); + + byte[] resp = receiveMessage(34000); + + TypesReader tr = new TypesReader(resp); + + int t = tr.readByte(); + listener.read(Packet.forName(t)); + + int rep_id = tr.readUINT32(); + if (rep_id != req_id) + { + throw new IOException("The server sent an invalid id field."); + } + + if (t == Packet.SSH_FXP_NAME) + { + int count = tr.readUINT32(); + + if (count != 1) + { + throw new IOException("The server sent an invalid SSH_FXP_NAME packet."); + } + + final String name = tr.readString(charsetName); + listener.read(name); + return name; + } + + if (t != Packet.SSH_FXP_STATUS) + { + throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")"); + } + + int errorCode = tr.readUINT32(); + String errorMessage = tr.readString(); + listener.read(errorMessage); + throw new SFTPException(errorMessage, errorCode); + } + + private List<SFTPv3DirectoryEntry> scanDirectory(byte[] handle) throws IOException + { + List<SFTPv3DirectoryEntry> files = new Vector<SFTPv3DirectoryEntry>(); + + while (true) + { + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(handle, 0, handle.length); + + log.debug("Sending SSH_FXP_READDIR..."); + sendMessage(Packet.SSH_FXP_READDIR, req_id, tw.getBytes()); + + byte[] resp = receiveMessage(34000); + + TypesReader tr = new TypesReader(resp); + + int t = tr.readByte(); + listener.read(Packet.forName(t)); + + int rep_id = tr.readUINT32(); + if (rep_id != req_id) + { + throw new IOException("The server sent an invalid id field."); + } + + if (t == Packet.SSH_FXP_NAME) + { + int count = tr.readUINT32(); + + log.debug("Parsing " + count + " name entries..."); + while (count > 0) + { + SFTPv3DirectoryEntry dirEnt = new SFTPv3DirectoryEntry(); + + dirEnt.filename = tr.readString(charsetName); + dirEnt.longEntry = tr.readString(charsetName); + listener.read(dirEnt.longEntry); + + dirEnt.attributes = readAttrs(tr); + files.add(dirEnt); + + log.debug("File: '" + dirEnt.filename + "'"); + count--; + } + continue; + } + + if (t != Packet.SSH_FXP_STATUS) + { + throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")"); + } + + int errorCode = tr.readUINT32(); + + if (errorCode == ErrorCodes.SSH_FX_EOF) + { + return files; + } + String errorMessage = tr.readString(); + listener.read(errorMessage); + throw new SFTPException(errorMessage, errorCode); + } + } + + public final SFTPv3FileHandle openDirectory(String path) throws IOException + { + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(path, charsetName); + + log.debug("Sending SSH_FXP_OPENDIR..."); + sendMessage(Packet.SSH_FXP_OPENDIR, req_id, tw.getBytes()); + + byte[] resp = receiveMessage(34000); + + TypesReader tr = new TypesReader(resp); + + int t = tr.readByte(); + listener.read(Packet.forName(t)); + + int rep_id = tr.readUINT32(); + if (rep_id != req_id) + { + throw new IOException("The server sent an invalid id field."); + } + + if (t == Packet.SSH_FXP_HANDLE) + { + log.debug("Got SSH_FXP_HANDLE."); + return new SFTPv3FileHandle(this, tr.readByteString()); + } + + if (t != Packet.SSH_FXP_STATUS) + { + throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")"); + } + + int errorCode = tr.readUINT32(); + String errorMessage = tr.readString(); + listener.read(errorMessage); + throw new SFTPException(errorMessage, errorCode); + } + + private String expandString(byte[] b, int off, int len) + { + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < len; i++) + { + int c = b[off + i] & 0xff; + + if ((c >= 32) && (c <= 126)) + { + sb.append((char) c); + } + else + { + sb.append("{0x" + Integer.toHexString(c) + "}"); + } + } + + return sb.toString(); + } + + private void init() throws IOException + { + /* Send SSH_FXP_INIT (version 3) */ + + final int client_version = 3; + + log.debug("Sending SSH_FXP_INIT (" + client_version + ")..."); + TypesWriter tw = new TypesWriter(); + tw.writeUINT32(client_version); + sendMessage(Packet.SSH_FXP_INIT, 0, tw.getBytes()); + + /* Receive SSH_FXP_VERSION */ + + log.debug("Waiting for SSH_FXP_VERSION..."); + TypesReader tr = new TypesReader(receiveMessage(34000)); /* Should be enough for any reasonable server */ + + int t = tr.readByte(); + listener.read(Packet.forName(t)); + + if (t != Packet.SSH_FXP_VERSION) + { + throw new IOException("The server did not send a SSH_FXP_VERSION packet (got " + t + ")"); + } + + protocol_version = tr.readUINT32(); + + log.debug("SSH_FXP_VERSION: protocol_version = " + protocol_version); + if (protocol_version != 3) + { + throw new IOException("Server version " + protocol_version + " is currently not supported"); + } + + /* Read and save extensions (if any) for later use */ + + while (tr.remain() != 0) + { + String name = tr.readString(); + listener.read(name); + byte[] value = tr.readByteString(); + log.debug("SSH_FXP_VERSION: extension: " + name + " = '" + expandString(value, 0, value.length) + "'"); + } + } + + /** + * Returns the negotiated SFTP protocol version between the client and the server. + * + * @return SFTP protocol version, i.e., "3". + */ + public int getProtocolVersion() + { + return protocol_version; + } + + /** + * Queries the channel state + * @return True if the underlying session is in open state + */ + public boolean isConnected() { + return sess.getState() == Channel.STATE_OPEN; + } + + /** + * Close this SFTP session. NEVER forget to call this method to free up + * resources - even if you got an exception from one of the other methods. + * Sometimes these other methods may throw an exception, saying that the + * underlying channel is closed (this can happen, e.g., if the other server + * sent a close message.) However, as long as you have not called the + * <code>close()</code> method, you are likely wasting resources. + */ + public void close() + { + sess.close(); + } + + /** + * List the contents of a directory. + * + * @param dirName See the {@link SFTPv3Client comment} for the class for more details. + * @return A Vector containing {@link SFTPv3DirectoryEntry} objects. + * @throws IOException + */ + public List<SFTPv3DirectoryEntry> ls(String dirName) throws IOException + { + SFTPv3FileHandle handle = openDirectory(dirName); + List<SFTPv3DirectoryEntry> result = scanDirectory(handle.fileHandle); + closeFile(handle); + return result; + } + + /** + * Create a new directory. + * + * @param dirName See the {@link SFTPv3Client comment} for the class for more details. + * @param posixPermissions the permissions for this directory, e.g., "0700" (remember that + * this is octal noation). The server will likely apply a umask. + * @throws IOException + */ + public void mkdir(String dirName, int posixPermissions) throws IOException + { + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(dirName, charsetName); + tw.writeUINT32(AttribFlags.SSH_FILEXFER_ATTR_PERMISSIONS); + tw.writeUINT32(posixPermissions); + + sendMessage(Packet.SSH_FXP_MKDIR, req_id, tw.getBytes()); + + expectStatusOKMessage(req_id); + } + + /** + * Remove a file. + * + * @param fileName See the {@link SFTPv3Client comment} for the class for more details. + * @throws IOException + */ + public void rm(String fileName) throws IOException + { + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(fileName, charsetName); + + sendMessage(Packet.SSH_FXP_REMOVE, req_id, tw.getBytes()); + + expectStatusOKMessage(req_id); + } + + /** + * Remove an empty directory. + * + * @param dirName See the {@link SFTPv3Client comment} for the class for more details. + * @throws IOException + */ + public void rmdir(String dirName) throws IOException + { + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(dirName, charsetName); + + sendMessage(Packet.SSH_FXP_RMDIR, req_id, tw.getBytes()); + + expectStatusOKMessage(req_id); + } + + /** + * Move a file or directory. + * + * @param oldPath See the {@link SFTPv3Client comment} for the class for more details. + * @param newPath See the {@link SFTPv3Client comment} for the class for more details. + * @throws IOException + */ + public void mv(String oldPath, String newPath) throws IOException + { + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(oldPath, charsetName); + tw.writeString(newPath, charsetName); + + sendMessage(Packet.SSH_FXP_RENAME, req_id, tw.getBytes()); + + expectStatusOKMessage(req_id); + } + + /** + * Open the file for reading. + */ + public static final int SSH_FXF_READ = 0x00000001; + /** + * Open the file for writing. If both this and SSH_FXF_READ are + * specified, the file is opened for both reading and writing. + */ + public static final int SSH_FXF_WRITE = 0x00000002; + /** + * Force all writes to append data at the end of the file. + */ + public static final int SSH_FXF_APPEND = 0x00000004; + /** + * If this flag is specified, then a new file will be created if one + * does not alread exist (if O_TRUNC is specified, the new file will + * be truncated to zero length if it previously exists). + */ + public static final int SSH_FXF_CREAT = 0x00000008; + /** + * Forces an existing file with the same name to be truncated to zero + * length when creating a file by specifying SSH_FXF_CREAT. + * SSH_FXF_CREAT MUST also be specified if this flag is used. + */ + public static final int SSH_FXF_TRUNC = 0x00000010; + /** + * Causes the request to fail if the named file already exists. + */ + public static final int SSH_FXF_EXCL = 0x00000020; + + /** + * Open a file for reading. + * + * @param fileName See the {@link SFTPv3Client comment} for the class for more details. + * @return a SFTPv3FileHandle handle + * @throws IOException + */ + public SFTPv3FileHandle openFileRO(String fileName) throws IOException + { + return openFile(fileName, SSH_FXF_READ, null); + } + + /** + * Open a file for reading and writing. + * + * @param fileName See the {@link SFTPv3Client comment} for the class for more details. + * @return a SFTPv3FileHandle handle + * @throws IOException + */ + public SFTPv3FileHandle openFileRW(String fileName) throws IOException + { + return openFile(fileName, SSH_FXF_READ | SSH_FXF_WRITE, null); + } + + /** + * Open a file in append mode. The SFTP v3 draft says nothing but assuming normal POSIX + * behavior, all writes will be appendend to the end of the file, no matter which offset + * one specifies. + * <p/> + * A side note for the curious: OpenSSH does an lseek() to the specified writing offset before each write(), + * even for writes to files opened in O_APPEND mode. However, bear in mind that when working + * in the O_APPEND mode, each write() includes an implicit lseek() to the end of the file + * (well, this is what the newsgroups say). + * + * @param fileName See the {@link SFTPv3Client comment} for the class for more details. + * @return a SFTPv3FileHandle handle + * @throws IOException + */ + public SFTPv3FileHandle openFileRWAppend(String fileName) throws IOException + { + return openFile(fileName, SSH_FXF_READ | SSH_FXF_WRITE | SSH_FXF_APPEND, null); + } + + /** + * Open a file in append mode. The SFTP v3 draft says nothing but assuming normal POSIX + * behavior, all writes will be appendend to the end of the file, no matter which offset + * one specifies. + * <p/> + * A side note for the curious: OpenSSH does an lseek() to the specified writing offset before each write(), + * even for writes to files opened in O_APPEND mode. However, bear in mind that when working + * in the O_APPEND mode, each write() includes an implicit lseek() to the end of the file + * (well, this is what the newsgroups say). + * + * @param fileName See the {@link SFTPv3Client comment} for the class for more details. + * @return a SFTPv3FileHandle handle + * @throws IOException + */ + public SFTPv3FileHandle openFileWAppend(String fileName) throws IOException + { + return openFile(fileName, SSH_FXF_WRITE | SSH_FXF_APPEND, null); + } + + /** + * Create a file and open it for reading and writing. + * Same as {@link #createFile(String, SFTPv3FileAttributes) createFile(fileName, null)}. + * + * @param fileName See the {@link SFTPv3Client comment} for the class for more details. + * @return a SFTPv3FileHandle handle + * @throws IOException + */ + public SFTPv3FileHandle createFile(String fileName) throws IOException + { + return createFile(fileName, null); + } + + /** + * Create a file and open it for reading and writing. + * You can specify the default attributes of the file (the server may or may + * not respect your wishes). + * + * @param fileName See the {@link SFTPv3Client comment} for the class for more details. + * @param attr may be <code>null</code> to use server defaults. Probably only + * the <code>uid</code>, <code>gid</code> and <code>permissions</code> + * (remember the server may apply a umask) entries of the {@link SFTPv3FileHandle} + * structure make sense. You need only to set those fields where you want + * to override the server's defaults. + * @return a SFTPv3FileHandle handle + * @throws IOException + */ + public SFTPv3FileHandle createFile(String fileName, SFTPv3FileAttributes attr) throws IOException + { + return openFile(fileName, SSH_FXF_CREAT | SSH_FXF_READ | SSH_FXF_WRITE, attr); + } + + /** + * Create a file (truncate it if it already exists) and open it for writing. + * Same as {@link #createFileTruncate(String, SFTPv3FileAttributes) createFileTruncate(fileName, null)}. + * + * @param fileName See the {@link SFTPv3Client comment} for the class for more details. + * @return a SFTPv3FileHandle handle + * @throws IOException + */ + public SFTPv3FileHandle createFileTruncate(String fileName) throws IOException + { + return createFileTruncate(fileName, null); + } + + /** + * reate a file (truncate it if it already exists) and open it for writing. + * You can specify the default attributes of the file (the server may or may + * not respect your wishes). + * + * @param fileName See the {@link SFTPv3Client comment} for the class for more details. + * @param attr may be <code>null</code> to use server defaults. Probably only + * the <code>uid</code>, <code>gid</code> and <code>permissions</code> + * (remember the server may apply a umask) entries of the {@link SFTPv3FileHandle} + * structure make sense. You need only to set those fields where you want + * to override the server's defaults. + * @return a SFTPv3FileHandle handle + * @throws IOException + */ + public SFTPv3FileHandle createFileTruncate(String fileName, SFTPv3FileAttributes attr) throws IOException + { + return openFile(fileName, SSH_FXF_CREAT | SSH_FXF_TRUNC | SSH_FXF_WRITE, attr); + } + + private byte[] createAttrs(SFTPv3FileAttributes attr) + { + TypesWriter tw = new TypesWriter(); + + int attrFlags = 0; + + if (attr == null) + { + tw.writeUINT32(0); + } + else + { + if (attr.size != null) + { + attrFlags = attrFlags | AttribFlags.SSH_FILEXFER_ATTR_SIZE; + } + + if ((attr.uid != null) && (attr.gid != null)) + { + attrFlags = attrFlags | AttribFlags.SSH_FILEXFER_ATTR_V3_UIDGID; + } + + if (attr.permissions != null) + { + attrFlags = attrFlags | AttribFlags.SSH_FILEXFER_ATTR_PERMISSIONS; + } + + if ((attr.atime != null) && (attr.mtime != null)) + { + attrFlags = attrFlags | AttribFlags.SSH_FILEXFER_ATTR_V3_ACMODTIME; + } + + tw.writeUINT32(attrFlags); + + if (attr.size != null) + { + tw.writeUINT64(attr.size); + } + + if ((attr.uid != null) && (attr.gid != null)) + { + tw.writeUINT32(attr.uid); + tw.writeUINT32(attr.gid); + } + + if (attr.permissions != null) + { + tw.writeUINT32(attr.permissions); + } + + if ((attr.atime != null) && (attr.mtime != null)) + { + tw.writeUINT32(attr.atime); + tw.writeUINT32(attr.mtime); + } + } + + return tw.getBytes(); + } + + public SFTPv3FileHandle openFile(String fileName, int flags, SFTPv3FileAttributes attr) throws IOException + { + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(fileName, charsetName); + tw.writeUINT32(flags); + tw.writeBytes(createAttrs(attr)); + + log.debug("Sending SSH_FXP_OPEN..."); + sendMessage(Packet.SSH_FXP_OPEN, req_id, tw.getBytes()); + + byte[] resp = receiveMessage(34000); + + TypesReader tr = new TypesReader(resp); + + int t = tr.readByte(); + listener.read(Packet.forName(t)); + + int rep_id = tr.readUINT32(); + if (rep_id != req_id) + { + throw new IOException("The server sent an invalid id field."); + } + + if (t == Packet.SSH_FXP_HANDLE) + { + log.debug("Got SSH_FXP_HANDLE."); + return new SFTPv3FileHandle(this, tr.readByteString()); + } + + if (t != Packet.SSH_FXP_STATUS) + { + throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")"); + } + + int errorCode = tr.readUINT32(); + String errorMessage = tr.readString(); + listener.read(errorMessage); + throw new SFTPException(errorMessage, errorCode); + } + + /** + * A read is divided into multiple requests sent sequentially before + * reading any status from the server + */ + private static class OutstandingReadRequest + { + int req_id; + /** + * Read offset to request on server starting at the file offset for the first request. + */ + long serverOffset; + /** + * Length of requested data + */ + int len; + /** + * Offset in destination buffer + */ + int dstOffset; + /** + * Temporary buffer + */ + byte[] buffer; + } + + private void sendReadRequest(int id, SFTPv3FileHandle handle, long offset, int len) throws IOException + { + TypesWriter tw = new TypesWriter(); + tw.writeString(handle.fileHandle, 0, handle.fileHandle.length); + tw.writeUINT64(offset); + tw.writeUINT32(len); + + log.debug("Sending SSH_FXP_READ (" + id + ") " + offset + "/" + len); + sendMessage(Packet.SSH_FXP_READ, id, tw.getBytes()); + } + + /** + * Parallel read requests maximum size. + */ + private static final int DEFAULT_MAX_PARALLELISM = 64; + + /** + * Parallel read requests. + */ + private int parallelism = DEFAULT_MAX_PARALLELISM; + + /** + * @param parallelism + */ + public void setRequestParallelism(int parallelism) + { + this.parallelism = Math.min(parallelism, DEFAULT_MAX_PARALLELISM); + } + + /** + * Mapping request ID to request. + */ + Map<Integer, OutstandingReadRequest> pendingReadQueue + = new HashMap<Integer, OutstandingReadRequest>(); + + /** + * Read bytes from a file in a parallel fashion. As many bytes as you want will be read. + * <p/> + * <ul> + * <li>The server will read as many bytes as it can from the file (up to <code>len</code>), + * and return them.</li> + * <li>If EOF is encountered before reading any data, <code>-1</code> is returned. + * <li>If an error occurs, an exception is thrown</li>. + * <li>For normal disk files, it is guaranteed that the server will return the specified + * number of bytes, or up to end of file. For, e.g., device files this may return + * fewer bytes than requested.</li> + * </ul> + * + * @param handle a SFTPv3FileHandle handle + * @param fileOffset offset (in bytes) in the file + * @param dst the destination byte array + * @param dstoff offset in the destination byte array + * @param len how many bytes to read, 0 < len + * @return the number of bytes that could be read, may be less than requested if + * the end of the file is reached, -1 is returned in case of <code>EOF</code> + * @throws IOException + */ + public int read(SFTPv3FileHandle handle, long fileOffset, byte[] dst, int dstoff, int len) throws IOException + { + boolean errorOccured = false; + + checkHandleValidAndOpen(handle); + + int remaining = len * parallelism; + int clientOffset = dstoff; + + long serverOffset = fileOffset; + for (OutstandingReadRequest r : pendingReadQueue.values()) + { + // Server offset should take pending requests into account. + serverOffset += r.len; + } + + while (true) + { + // Stop if there was an error and no outstanding request + if ((pendingReadQueue.size() == 0) && errorOccured) + { + break; + } + + // Send as many requests as we are allowed to + while (pendingReadQueue.size() < parallelism) + { + if (errorOccured) + { + break; + } + // Send the next read request + OutstandingReadRequest req = new OutstandingReadRequest(); + req.req_id = generateNextRequestID(); + req.serverOffset = serverOffset; + req.len = (remaining > len) ? len : remaining; + req.buffer = dst; + req.dstOffset = dstoff; + + serverOffset += req.len; + clientOffset += req.len; + remaining -= req.len; + + sendReadRequest(req.req_id, handle, req.serverOffset, req.len); + + pendingReadQueue.put(req.req_id, req); + } + if (pendingReadQueue.size() == 0) + { + break; + } + + // Receive a single answer + byte[] resp = receiveMessage(34000); + TypesReader tr = new TypesReader(resp); + + int t = tr.readByte(); + listener.read(Packet.forName(t)); + + // Search the pending queue + OutstandingReadRequest req = pendingReadQueue.remove(tr.readUINT32()); + if (null == req) + { + throw new IOException("The server sent an invalid id field."); + } + // Evaluate the answer + if (t == Packet.SSH_FXP_STATUS) + { + /* In any case, stop sending more packets */ + + int code = tr.readUINT32(); + String msg = tr.readString(); + listener.read(msg); + + if (log.isDebugEnabled()) + { + String[] desc = ErrorCodes.getDescription(code); + log.debug("Got SSH_FXP_STATUS (" + req.req_id + ") (" + ((desc != null) ? desc[0] : "UNKNOWN") + ")"); + } + // Flag to read all pending requests but don't send any more. + errorOccured = true; + if (pendingReadQueue.isEmpty()) + { + if (ErrorCodes.SSH_FX_EOF == code) + { + return -1; + } + throw new SFTPException(msg, code); + } + } + else if (t == Packet.SSH_FXP_DATA) + { + // OK, collect data + int readLen = tr.readUINT32(); + + if ((readLen < 0) || (readLen > req.len)) + { + throw new IOException("The server sent an invalid length field in a SSH_FXP_DATA packet."); + } + + if (log.isDebugEnabled()) + { + log.debug("Got SSH_FXP_DATA (" + req.req_id + ") " + req.serverOffset + "/" + readLen + + " (requested: " + req.len + ")"); + } + + // Read bytes into buffer + tr.readBytes(req.buffer, req.dstOffset, readLen); + + if (readLen < req.len) + { + /* Send this request packet again to request the remaing data in this slot. */ + req.req_id = generateNextRequestID(); + req.serverOffset += readLen; + req.len -= readLen; + + log.debug("Requesting again: " + req.serverOffset + "/" + req.len); + sendReadRequest(req.req_id, handle, req.serverOffset, req.len); + + pendingReadQueue.put(req.req_id, req); + } + return readLen; + } + else + { + throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")"); + } + } + // Should never reach here. + throw new SFTPException("No EOF reached", -1); + } + + /** + * A read is divided into multiple requests sent sequentially before + * reading any status from the server + */ + private static class OutstandingStatusRequest + { + int req_id; + } + + /** + * Mapping request ID to request. + */ + Map<Integer, OutstandingStatusRequest> pendingStatusQueue + = new HashMap<Integer, OutstandingStatusRequest>(); + + /** + * Write bytes to a file. If <code>len</code> > 32768, then the write operation will + * be split into multiple writes. + * + * @param handle a SFTPv3FileHandle handle. + * @param fileOffset offset (in bytes) in the file. + * @param src the source byte array. + * @param srcoff offset in the source byte array. + * @param len how many bytes to write. + * @throws IOException + */ + public void write(SFTPv3FileHandle handle, long fileOffset, byte[] src, int srcoff, int len) throws IOException + { + checkHandleValidAndOpen(handle); + + // Send the next write request + OutstandingStatusRequest req = new OutstandingStatusRequest(); + req.req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(handle.fileHandle, 0, handle.fileHandle.length); + tw.writeUINT64(fileOffset); + tw.writeString(src, srcoff, len); + + log.debug("Sending SSH_FXP_WRITE..."); + sendMessage(Packet.SSH_FXP_WRITE, req.req_id, tw.getBytes()); + + pendingStatusQueue.put(req.req_id, req); + + // Only read next status if parallelism reached + while (pendingStatusQueue.size() >= parallelism) + { + this.readStatus(); + } + } + + private void readStatus() throws IOException + { + byte[] resp = receiveMessage(34000); + + TypesReader tr = new TypesReader(resp); + int t = tr.readByte(); + listener.read(Packet.forName(t)); + + // Search the pending queue + OutstandingStatusRequest status = pendingStatusQueue.remove(tr.readUINT32()); + if (null == status) + { + throw new IOException("The server sent an invalid id field."); + } + + // Evaluate the answer + if (t == Packet.SSH_FXP_STATUS) + { + // In any case, stop sending more packets + int code = tr.readUINT32(); + if (log.isDebugEnabled()) + { + String[] desc = ErrorCodes.getDescription(code); + log.debug("Got SSH_FXP_STATUS (" + status.req_id + ") (" + ((desc != null) ? desc[0] : "UNKNOWN") + ")"); + } + if (code == ErrorCodes.SSH_FX_OK) + { + return; + } + String msg = tr.readString(); + listener.read(msg); + throw new SFTPException(msg, code); + } + throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")"); + } + + private void readPendingReadStatus() throws IOException + { + byte[] resp = receiveMessage(34000); + + TypesReader tr = new TypesReader(resp); + int t = tr.readByte(); + listener.read(Packet.forName(t)); + + // Search the pending queue + OutstandingReadRequest status = pendingReadQueue.remove(tr.readUINT32()); + if (null == status) + { + throw new IOException("The server sent an invalid id field."); + } + + // Evaluate the answer + if (t == Packet.SSH_FXP_STATUS) + { + // In any case, stop sending more packets + int code = tr.readUINT32(); + if (log.isDebugEnabled()) + { + String[] desc = ErrorCodes.getDescription(code); + log.debug("Got SSH_FXP_STATUS (" + status.req_id + ") (" + ((desc != null) ? desc[0] : "UNKNOWN") + ")"); + } + if (code == ErrorCodes.SSH_FX_OK) + { + return; + } + if (code == ErrorCodes.SSH_FX_EOF) + { + return; + } + String msg = tr.readString(); + listener.read(msg); + throw new SFTPException(msg, code); + } + throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")"); + } + + /** + * Close a file. + * + * @param handle a SFTPv3FileHandle handle + * @throws IOException + */ + public void closeFile(SFTPv3FileHandle handle) throws IOException + { + try + { + while (!pendingReadQueue.isEmpty()) + { + this.readPendingReadStatus(); + } + while (!pendingStatusQueue.isEmpty()) + { + this.readStatus(); + } + if (!handle.isClosed) + { + closeHandle(handle.fileHandle); + } + } + finally + { + handle.isClosed = true; + } + } +} |