Provide an SFTP client Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/21c1cee9 Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/21c1cee9 Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/21c1cee9
Branch: refs/heads/master Commit: 21c1cee99ba981dd262f7911617160de0c8c6654 Parents: 16fc6a8 Author: Guillaume Nodet <[email protected]> Authored: Mon Jul 22 16:07:29 2013 +0200 Committer: Guillaume Nodet <[email protected]> Committed: Mon Jul 22 16:07:29 2013 +0200 ---------------------------------------------------------------------- .../java/org/apache/sshd/ClientSession.java | 6 + .../java/org/apache/sshd/client/ScpClient.java | 18 + .../java/org/apache/sshd/client/SftpClient.java | 177 +++++ .../client/channel/AbstractClientChannel.java | 2 +- .../sshd/client/scp/DefaultScpClient.java | 19 + .../sshd/client/session/ClientSessionImpl.java | 6 + .../sshd/client/sftp/DefaultSftpClient.java | 703 +++++++++++++++++++ .../org/apache/sshd/common/file/SshFile.java | 26 + .../common/file/nativefs/NativeSshFile.java | 120 ++++ .../apache/sshd/server/sftp/SftpSubsystem.java | 623 +++++----------- .../src/test/java/org/apache/sshd/SftpTest.java | 61 ++ .../sshd/sftp/reply/SshFxpStatusReply.java | 35 +- .../java/org/apache/sshd/sftp/SftpTest.java | 11 + 13 files changed, 1328 insertions(+), 479 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/21c1cee9/sshd-core/src/main/java/org/apache/sshd/ClientSession.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/ClientSession.java b/sshd-core/src/main/java/org/apache/sshd/ClientSession.java index 6b7b1af..26fd46b 100644 --- a/sshd-core/src/main/java/org/apache/sshd/ClientSession.java +++ b/sshd-core/src/main/java/org/apache/sshd/ClientSession.java @@ -24,6 +24,7 @@ import java.util.Map; import org.apache.sshd.client.ClientFactoryManager; import org.apache.sshd.client.ScpClient; +import org.apache.sshd.client.SftpClient; import org.apache.sshd.client.channel.ChannelDirectTcpip; import org.apache.sshd.client.channel.ChannelExec; import org.apache.sshd.client.channel.ChannelShell; @@ -118,6 +119,11 @@ public interface ClientSession extends Session { ScpClient createScpClient(); /** + * Create an SFTP client from this session. + */ + SftpClient createSftpClient() throws IOException; + + /** * Start forwarding the given local address on the client to the given address on the server. */ SshdSocketAddress startLocalPortForwarding(SshdSocketAddress local, SshdSocketAddress remote) throws IOException; http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/21c1cee9/sshd-core/src/main/java/org/apache/sshd/client/ScpClient.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/client/ScpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/ScpClient.java index fc6bfb8..d2bdaa8 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/ScpClient.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/ScpClient.java @@ -1,3 +1,21 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ package org.apache.sshd.client; import java.io.IOException; http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/21c1cee9/sshd-core/src/main/java/org/apache/sshd/client/SftpClient.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/client/SftpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/SftpClient.java new file mode 100644 index 0000000..5150a81 --- /dev/null +++ b/sshd-core/src/main/java/org/apache/sshd/client/SftpClient.java @@ -0,0 +1,177 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sshd.client; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.EnumSet; + +/** + * @author <a href="http://mina.apache.org">Apache MINA Project</a> + */ +public interface SftpClient { + + int S_IFMT = 0170000; // bitmask for the file type bitfields + int S_IFSOCK = 0140000; // socket + int S_IFLNK = 0120000; // symbolic link + int S_IFREG = 0100000; // regular file + int S_IFBLK = 0060000; // block device + int S_IFDIR = 0040000; // directory + int S_IFCHR = 0020000; // character device + int S_IFIFO = 0010000; // fifo + int S_ISUID = 0004000; // set UID bit + int S_ISGID = 0002000; // set GID bit + int S_ISVTX = 0001000; // sticky bit + int S_IRUSR = 0000400; + int S_IWUSR = 0000200; + int S_IXUSR = 0000100; + int S_IRGRP = 0000040; + int S_IWGRP = 0000020; + int S_IXGRP = 0000010; + int S_IROTH = 0000004; + int S_IWOTH = 0000002; + int S_IXOTH = 0000001; + + enum OpenMode { + Read, + Write, + Append, + Create, + Truncate, + Exclusive + } + + enum Attribute { + Size, + UidGid, + Perms, + AcModTime + } + + public static class Handle { + public final String id; + public Handle(String id) { + this.id = id; + } + } + + public static class Attributes { + public EnumSet<Attribute> flags = EnumSet.noneOf(Attribute.class); + public long size; + public int uid; + public int gid; + public int perms; + public int atime; + public int mtime; + public Attributes size(long size) { + flags.add(Attribute.Size); + this.size = size; + return this; + } + public Attributes owner(int uid, int gid) { + flags.add(Attribute.UidGid); + this.uid = uid; + this.gid = gid; + return this; + } + public Attributes perms(int perms) { + flags.add(Attribute.Perms); + this.perms = perms; + return this; + } + public Attributes time(int atime, int mtime) { + flags.add(Attribute.AcModTime); + this.atime = atime; + this.mtime = mtime; + return this; + } + public boolean isDirectory() { + return (perms & S_IFMT) == S_IFDIR; + } + public boolean isRegularFile() { + return (perms & S_IFMT) == S_IFREG; + } + public boolean isSymlink() { + return (perms & S_IFMT) == S_IFLNK; + } + } + + public static class DirEntry { + public final String filename; + public final String longFilename; + public final Attributes attributes; + public DirEntry(String filename, String longFilename, Attributes attributes) { + this.filename = filename; + this.longFilename = longFilename; + this.attributes = attributes; + } + } + + // + // Low level API + // + + Handle open(String path, EnumSet<OpenMode> options) throws IOException; + + void close(Handle handle) throws IOException; + + void remove(String path) throws IOException; + + void rename(String oldPath, String newPath) throws IOException; + + int read(Handle handle, long fileOffset, byte[] dst, int dstoff, int len) throws IOException; + + void write(Handle handle, long fileOffset, byte[] src, int srcoff, int len) throws IOException; + + void mkdir(String path) throws IOException; + + void rmdir(String path) throws IOException; + + Handle openDir(String path) throws IOException; + + DirEntry[] readDir(Handle handle) throws IOException; + + String canonicalPath(String canonical) throws IOException; + + Attributes stat(String path) throws IOException; + + Attributes lstat(String path) throws IOException; + + Attributes stat(Handle handle) throws IOException; + + void setStat(String path, Attributes attributes) throws IOException; + + void setStat(Handle handle, Attributes attributes) throws IOException; + + String readLink(String path) throws IOException; + + void symLink(String linkPath, String targetPath) throws IOException; + + // + // High level API + // + + Iterable<DirEntry> readDir(String path) throws IOException; + + InputStream read(String path) throws IOException; + + OutputStream write(String path) throws IOException; + +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/21c1cee9/sshd-core/src/main/java/org/apache/sshd/client/channel/AbstractClientChannel.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/client/channel/AbstractClientChannel.java b/sshd-core/src/main/java/org/apache/sshd/client/channel/AbstractClientChannel.java index 40015c3..eb2ea17 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/channel/AbstractClientChannel.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/channel/AbstractClientChannel.java @@ -127,7 +127,7 @@ public abstract class AbstractClientChannel extends AbstractChannel implements C @Override protected void doClose() { super.doClose(); - IoUtils.closeQuietly(invertedIn, in, out, err); + IoUtils.closeQuietly(invertedIn, invertedOut, invertedErr, in, out, err); } public int waitFor(int mask, long timeout) { http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/21c1cee9/sshd-core/src/main/java/org/apache/sshd/client/scp/DefaultScpClient.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/client/scp/DefaultScpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/scp/DefaultScpClient.java index 2bd6985..1b590c2 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/scp/DefaultScpClient.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/scp/DefaultScpClient.java @@ -1,3 +1,21 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ package org.apache.sshd.client.scp; import java.io.IOException; @@ -14,6 +32,7 @@ import org.apache.sshd.common.file.SshFile; import org.apache.sshd.common.scp.ScpHelper; /** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> */ public class DefaultScpClient implements ScpClient { http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/21c1cee9/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java index 10825b7..d40173f 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java @@ -31,6 +31,7 @@ import org.apache.sshd.ClientSession; import org.apache.sshd.client.ClientFactoryManager; import org.apache.sshd.client.ScpClient; import org.apache.sshd.client.ServerKeyVerifier; +import org.apache.sshd.client.SftpClient; import org.apache.sshd.client.UserAuth; import org.apache.sshd.client.UserInteraction; import org.apache.sshd.client.auth.UserAuthAgent; @@ -45,6 +46,7 @@ import org.apache.sshd.client.future.AuthFuture; import org.apache.sshd.client.future.DefaultAuthFuture; import org.apache.sshd.client.future.OpenFuture; import org.apache.sshd.client.scp.DefaultScpClient; +import org.apache.sshd.client.sftp.DefaultSftpClient; import org.apache.sshd.common.Channel; import org.apache.sshd.common.KeyExchange; import org.apache.sshd.common.KeyPairProvider; @@ -256,6 +258,10 @@ public class ClientSessionImpl extends AbstractSession implements ClientSession return new DefaultScpClient(this); } + public SftpClient createSftpClient() throws IOException { + return new DefaultSftpClient(this); + } + public SshdSocketAddress startLocalPortForwarding(SshdSocketAddress local, SshdSocketAddress remote) throws IOException { return getTcpipForwarder().startLocalPortForwarding(local, remote); } http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/21c1cee9/sshd-core/src/main/java/org/apache/sshd/client/sftp/DefaultSftpClient.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/client/sftp/DefaultSftpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/sftp/DefaultSftpClient.java new file mode 100644 index 0000000..1dd6351 --- /dev/null +++ b/sshd-core/src/main/java/org/apache/sshd/client/sftp/DefaultSftpClient.java @@ -0,0 +1,703 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sshd.client.sftp; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.io.OutputStream; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.sshd.ClientSession; +import org.apache.sshd.client.SftpClient; +import org.apache.sshd.client.channel.ChannelSubsystem; +import org.apache.sshd.common.SshException; +import org.apache.sshd.common.util.Buffer; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class DefaultSftpClient implements SftpClient { + + public static final int SSH_FXP_INIT = 1; + public static final int SSH_FXP_VERSION = 2; + public static final int SSH_FXP_OPEN = 3; + public static final int SSH_FXP_CLOSE = 4; + public static final int SSH_FXP_READ = 5; + public static final int SSH_FXP_WRITE = 6; + public static final int SSH_FXP_LSTAT = 7; + public static final int SSH_FXP_FSTAT = 8; + public static final int SSH_FXP_SETSTAT = 9; + public static final int SSH_FXP_FSETSTAT = 10; + public static final int SSH_FXP_OPENDIR = 11; + public static final int SSH_FXP_READDIR = 12; + public static final int SSH_FXP_REMOVE = 13; + public static final int SSH_FXP_MKDIR = 14; + public static final int SSH_FXP_RMDIR = 15; + public static final int SSH_FXP_REALPATH = 16; + public static final int SSH_FXP_STAT = 17; + public static final int SSH_FXP_RENAME = 18; + public static final int SSH_FXP_READLINK = 19; + public static final int SSH_FXP_SYMLINK = 20; + public static final int SSH_FXP_STATUS = 101; + public static final int SSH_FXP_HANDLE = 102; + public static final int SSH_FXP_DATA = 103; + public static final int SSH_FXP_NAME = 104; + public static final int SSH_FXP_ATTRS = 105; + public static final int SSH_FXP_EXTENDED = 200; + public static final int SSH_FXP_EXTENDED_REPLY = 201; + + public static final int SSH_FX_OK = 0; + public static final int SSH_FX_EOF = 1; + public static final int SSH_FX_NO_SUCH_FILE = 2; + public static final int SSH_FX_PERMISSION_DENIED = 3; + public static final int SSH_FX_FAILURE = 4; + public static final int SSH_FX_BAD_MESSAGE = 5; + public static final int SSH_FX_NO_CONNECTION = 6; + public static final int SSH_FX_CONNECTION_LOST = 7; + public static final int SSH_FX_OP_UNSUPPORTED = 8; + + public static final int SSH_FILEXFER_ATTR_SIZE = 0x00000001; + public static final int SSH_FILEXFER_ATTR_UIDGID = 0x00000002; + public static final int SSH_FILEXFER_ATTR_PERMISSIONS = 0x00000004; + public static final int SSH_FILEXFER_ATTR_ACMODTIME = 0x00000008; //v3 naming convention + public static final int SSH_FILEXFER_ATTR_EXTENDED = 0x80000000; + + public static final int SSH_FXF_READ = 0x00000001; + public static final int SSH_FXF_WRITE = 0x00000002; + public static final int SSH_FXF_APPEND = 0x00000004; + public static final int SSH_FXF_CREAT = 0x00000008; + public static final int SSH_FXF_TRUNC = 0x00000010; + public static final int SSH_FXF_EXCL = 0x00000020; + + private final ClientSession clientSession; + private final ChannelSubsystem channel; + private final Map<Integer, Buffer> messages; + private final AtomicInteger cmdId = new AtomicInteger(100); + private final Buffer receiveBuffer = new Buffer(); + + public DefaultSftpClient(ClientSession clientSession) throws IOException { + this.clientSession = clientSession; + this.channel = clientSession.createSubsystemChannel("sftp"); + this.messages = new HashMap<Integer, Buffer>(); + try { + this.channel.setOut(new OutputStream() { + @Override + public void write(int b) throws IOException { + write(new byte[] { (byte) b }, 0, 1); + } + @Override + public void write(byte[] b, int off, int len) throws IOException { + data(b, off, len); + } + }); + this.channel.setErr(new ByteArrayOutputStream()); + this.channel.open().await(); + } catch (InterruptedException e) { + throw (IOException) new InterruptedIOException().initCause(e); + } + init(); + } + + /** + * Receive binary data + */ + protected int data(byte[] buf, int start, int len) throws IOException { + Buffer incoming = new Buffer(buf, start, len); + // If we already have partial data, we need to append it to the buffer and use it + if (receiveBuffer.available() > 0) { + receiveBuffer.putBuffer(incoming); + incoming = receiveBuffer; + } + // Process commands + int rpos = incoming.rpos(); + while (receive(incoming)); + int read = incoming.rpos() - rpos; + // Compact and add remaining data + receiveBuffer.compact(); + if (receiveBuffer != incoming && incoming.available() > 0) { + receiveBuffer.putBuffer(incoming); + } + return read; + } + + /** + * Read SFTP packets from buffer + */ + protected boolean receive(Buffer incoming) throws IOException { + int rpos = incoming.rpos(); + int wpos = incoming.wpos(); + if (wpos - rpos > 4) { + int length = incoming.getInt(); + if (length < 5) { + throw new IOException("Illegal sftp packet length: " + length); + } + if (wpos - rpos >= length + 4) { + incoming.rpos(rpos); + incoming.wpos(rpos + 4 + length); + process(incoming); + incoming.rpos(rpos + 4 + length); + incoming.wpos(wpos); + return true; + } + } + incoming.rpos(rpos); + return false; + } + + /** + * Process an SFTP packet + */ + protected void process(Buffer incoming) throws IOException { + Buffer buffer = new Buffer(); + buffer.putBuffer(incoming); + buffer.rpos(5); + int id = buffer.getInt(); + buffer.rpos(0); + synchronized (messages) { + messages.put(id, buffer); + messages.notifyAll(); + } + } + + + protected int send(int cmd, Buffer buffer) throws IOException { + int id = cmdId.incrementAndGet(); + DataOutputStream dos = new DataOutputStream(channel.getInvertedIn()); + dos.writeInt(5 + buffer.available()); + dos.writeByte(cmd); + dos.writeInt(id); + dos.write(buffer.array(), buffer.rpos(), buffer.available()); + dos.flush(); + return id; + } + + protected Buffer receive(int id) throws IOException { + synchronized (messages) { + while (true) { + Buffer buffer = messages.get(id); + if (buffer != null) { + return buffer; + } + try { + messages.wait(); + } catch (InterruptedException e) { + throw (IOException) new InterruptedIOException().initCause(e); + } + } + } + } + + protected Buffer read() throws IOException { + DataInputStream dis = new DataInputStream(channel.getInvertedOut()); + int length = dis.readInt(); + if (length < 5) { + throw new IllegalArgumentException(); + } + Buffer buffer = new Buffer(length + 4); + buffer.putInt(length); + int nb = length; + while (nb > 0) { + int l = dis.read(buffer.array(), buffer.wpos(), nb); + if (l < 0) { + throw new IllegalArgumentException(); + } + buffer.wpos(buffer.wpos() + l); + nb -= l; + } + return buffer; + } + + protected void init() throws IOException { + // Init packet + DataOutputStream dos = new DataOutputStream(channel.getInvertedIn()); + dos.writeInt(5); + dos.writeByte(SSH_FXP_INIT); + dos.writeInt(3); + dos.flush(); + Buffer buffer = null; + synchronized (messages) { + while (messages.isEmpty()) { + try { + messages.wait(); + } catch (InterruptedException e) { + throw (IOException) new InterruptedIOException().initCause(e); + } + } + buffer = messages.remove(messages.keySet().iterator().next()); + + } + int length = buffer.getInt(); + int type = buffer.getByte(); + int id = buffer.getInt(); + if (type == SSH_FXP_VERSION) { + if (id != 3) { + throw new SshException("Unable to use SFTP v3, server replied with version " + id); + } + } else if (type == SSH_FXP_STATUS) { + int substatus = buffer.getInt(); + String msg = buffer.getString(); + String lang = buffer.getString(); + throw new SshException("SFTP error (" + substatus + "): " + msg); + } else { + throw new SshException("Unexpected SFTP packet received: " + type); + } + } + + protected void checkStatus(Buffer buffer) throws IOException { + int length = buffer.getInt(); + int type = buffer.getByte(); + int id = buffer.getInt(); + if (type == SSH_FXP_STATUS) { + int substatus = buffer.getInt(); + String msg = buffer.getString(); + String lang = buffer.getString(); + if (substatus != SSH_FX_OK) { + throw new SshException("SFTP error (" + substatus + "): " + msg); + } + } else { + throw new SshException("Unexpected SFTP packet received: " + type); + } + } + + protected Handle checkHandle(Buffer buffer) throws IOException { + int length = buffer.getInt(); + int type = buffer.getByte(); + int id = buffer.getInt(); + if (type == SSH_FXP_STATUS) { + int substatus = buffer.getInt(); + String msg = buffer.getString(); + String lang = buffer.getString(); + throw new SshException("SFTP error (" + substatus + "): " + msg); + } else if (type == SSH_FXP_HANDLE) { + String handle = buffer.getString(); + return new Handle(handle); + } else { + throw new SshException("Unexpected SFTP packet received: " + type); + } + } + + protected Attributes checkAttributes(Buffer buffer) throws IOException { + int length = buffer.getInt(); + int type = buffer.getByte(); + int id = buffer.getInt(); + if (type == SSH_FXP_STATUS) { + int substatus = buffer.getInt(); + String msg = buffer.getString(); + String lang = buffer.getString(); + throw new SshException("SFTP error (" + substatus + "): " + msg); + } else if (type == SSH_FXP_ATTRS) { + return readAttributes(buffer); + } else { + throw new SshException("Unexpected SFTP packet received: " + type); + } + } + + protected String checkOneName(Buffer buffer) throws IOException { + int length = buffer.getInt(); + int type = buffer.getByte(); + int id = buffer.getInt(); + if (type == SSH_FXP_STATUS) { + int substatus = buffer.getInt(); + String msg = buffer.getString(); + String lang = buffer.getString(); + throw new SshException("SFTP error (" + substatus + "): " + msg); + } else if (type == SSH_FXP_NAME) { + int len = buffer.getInt(); + if (len != 1) { + throw new SshException("SFTP error: received " + len + " names instead of 1"); + } + String name = buffer.getString(); + String longName = buffer.getString(); + Attributes attrs = readAttributes(buffer); + return name; + } else { + throw new SshException("Unexpected SFTP packet received: " + type); + } + } + + protected Attributes readAttributes(Buffer buffer) throws IOException { + Attributes attrs = new Attributes(); + int flags = buffer.getInt(); + if ((flags & SSH_FILEXFER_ATTR_SIZE) != 0) { + attrs.flags.add(Attribute.Size); + attrs.size = buffer.getLong(); + } + if ((flags & SSH_FILEXFER_ATTR_UIDGID) != 0) { + attrs.flags.add(Attribute.UidGid); + attrs.uid = buffer.getInt(); + attrs.gid = buffer.getInt(); + } + if ((flags & SSH_FILEXFER_ATTR_PERMISSIONS) != 0) { + attrs.flags.add(Attribute.Perms); + attrs.perms = buffer.getInt(); + } + if ((flags & SSH_FILEXFER_ATTR_ACMODTIME) != 0) { + attrs.flags.add(Attribute.AcModTime); + attrs.atime = buffer.getInt(); + attrs.mtime = buffer.getInt(); + } + return attrs; + } + + protected void writeAttributes(Buffer buffer, Attributes attributes) throws IOException { + int flags = 0; + for (Attribute a : attributes.flags) { + switch (a) { + case Size: flags |= SSH_FILEXFER_ATTR_SIZE; break; + case UidGid: flags |= SSH_FILEXFER_ATTR_UIDGID; break; + case Perms: flags |= SSH_FILEXFER_ATTR_PERMISSIONS; break; + case AcModTime: flags |= SSH_FILEXFER_ATTR_ACMODTIME; break; + } + } + buffer.putInt(flags); + if ((flags & SSH_FILEXFER_ATTR_SIZE) != 0) { + buffer.putLong(attributes.size); + } + if ((flags & SSH_FILEXFER_ATTR_UIDGID) != 0) { + buffer.putInt(attributes.uid); + buffer.putInt(attributes.gid); + } + if ((flags & SSH_FILEXFER_ATTR_PERMISSIONS) != 0) { + buffer.putInt(attributes.perms); + } + if ((flags & SSH_FILEXFER_ATTR_ACMODTIME) != 0) { + buffer.putInt(attributes.atime); + buffer.putInt(attributes.mtime); + } + } + + public Handle open(String path, EnumSet<OpenMode> options) throws IOException { + Buffer buffer = new Buffer(); + buffer.putString(path); + int mode = 0; + for (OpenMode m : options) { + switch (m) { + case Read: mode |= SSH_FXF_READ; break; + case Write: mode |= SSH_FXF_WRITE; break; + case Append: mode |= SSH_FXF_APPEND; break; + case Create: mode |= SSH_FXF_CREAT; break; + case Truncate: mode |= SSH_FXF_TRUNC; break; + case Exclusive: mode |= SSH_FXF_EXCL; break; + } + } + buffer.putInt(mode); + return checkHandle(receive(send(SSH_FXP_OPEN, buffer))); + } + + public void close(Handle handle) throws IOException { + Buffer buffer = new Buffer(); + buffer.putString(handle.id); + checkStatus(receive(send(SSH_FXP_CLOSE, buffer))); + } + + public void remove(String path) throws IOException { + Buffer buffer = new Buffer(); + buffer.putString(path); + checkStatus(receive(send(SSH_FXP_REMOVE, buffer))); + } + + public void rename(String oldPath, String newPath) throws IOException { + Buffer buffer = new Buffer(); + buffer.putString(oldPath); + buffer.putString(newPath); + checkStatus(receive(send(SSH_FXP_RENAME, buffer))); + } + + public int read(Handle handle, long fileOffset, byte[] dst, int dstoff, int len) throws IOException { + Buffer buffer = new Buffer(); + buffer.putString(handle.id); + buffer.putLong(fileOffset); + buffer.putInt(len); + buffer = receive(send(SSH_FXP_READ, buffer)); + int length = buffer.getInt(); + int type = buffer.getByte(); + int id = buffer.getInt(); + if (type == SSH_FXP_STATUS) { + int substatus = buffer.getInt(); + String msg = buffer.getString(); + String lang = buffer.getString(); + if (substatus == SSH_FX_EOF) { + return -1; + } + throw new SshException("SFTP error (" + substatus + "): " + msg); + } else if (type == SSH_FXP_DATA) { + len = buffer.getInt(); + buffer.getRawBytes(dst, dstoff, len); + return len; + } else { + throw new SshException("Unexpected SFTP packet received: " + type); + } + } + + public void write(Handle handle, long fileOffset, byte[] src, int srcoff, int len) throws IOException { + Buffer buffer = new Buffer(); + buffer.putString(handle.id); + buffer.putLong(fileOffset); + buffer.putBytes(src, srcoff, len); + checkStatus(receive(send(SSH_FXP_WRITE, buffer))); + } + + public void mkdir(String path) throws IOException { + Buffer buffer = new Buffer(); + buffer.putString(path); + checkStatus(receive(send(SSH_FXP_MKDIR, buffer))); + } + + public void rmdir(String path) throws IOException { + Buffer buffer = new Buffer(); + buffer.putString(path); + checkStatus(receive(send(SSH_FXP_RMDIR, buffer))); + } + + public Handle openDir(String path) throws IOException { + Buffer buffer = new Buffer(); + buffer.putString(path); + return checkHandle(receive(send(SSH_FXP_OPENDIR, buffer))); + } + + public DirEntry[] readDir(Handle handle) throws IOException { + Buffer buffer = new Buffer(); + buffer.putString(handle.id); + buffer = receive(send(SSH_FXP_READDIR, buffer)); + int length = buffer.getInt(); + int type = buffer.getByte(); + int id = buffer.getInt(); + if (type == SSH_FXP_STATUS) { + int substatus = buffer.getInt(); + String msg = buffer.getString(); + String lang = buffer.getString(); + if (substatus == SSH_FX_EOF) { + return null; + } + throw new SshException("SFTP error (" + substatus + "): " + msg); + } else if (type == SSH_FXP_NAME) { + int len = buffer.getInt(); + DirEntry[] entries = new DirEntry[len]; + for (int i = 0; i < len; i++) { + String name = buffer.getString(); + String longName = buffer.getString(); + Attributes attrs = readAttributes(buffer); + entries[i] = new DirEntry(name, longName, attrs); + } + return entries; + } else { + throw new SshException("Unexpected SFTP packet received: " + type); + } + } + + public String canonicalPath(String path) throws IOException { + Buffer buffer = new Buffer(); + buffer.putString(path); + return checkOneName(receive(send(SSH_FXP_REALPATH, buffer))); + } + + public Attributes stat(String path) throws IOException { + Buffer buffer = new Buffer(); + buffer.putString(path); + return checkAttributes(receive(send(SSH_FXP_STAT, buffer))); + } + + public Attributes lstat(String path) throws IOException { + Buffer buffer = new Buffer(); + buffer.putString(path); + return checkAttributes(receive(send(SSH_FXP_LSTAT, buffer))); + } + + public Attributes stat(Handle handle) throws IOException { + Buffer buffer = new Buffer(); + buffer.putString(handle.id); + return checkAttributes(receive(send(SSH_FXP_FSTAT, buffer))); + } + + public void setStat(String path, Attributes attributes) throws IOException { + Buffer buffer = new Buffer(); + buffer.putString(path); + writeAttributes(buffer, attributes); + checkStatus(receive(send(SSH_FXP_SETSTAT, buffer))); + } + + public void setStat(Handle handle, Attributes attributes) throws IOException { + Buffer buffer = new Buffer(); + buffer.putString(handle.id); + writeAttributes(buffer, attributes); + checkStatus(receive(send(SSH_FXP_FSETSTAT, buffer))); + } + + public String readLink(String path) throws IOException { + Buffer buffer = new Buffer(); + buffer.putString(path); + return checkOneName(receive(send(SSH_FXP_READLINK, buffer))); + } + + public void symLink(String linkPath, String targetPath) throws IOException { + Buffer buffer = new Buffer(); + buffer.putString(linkPath); + buffer.putString(targetPath); + checkStatus(receive(send(SSH_FXP_RENAME, buffer))); + } + + public Iterable<DirEntry> readDir(final String path) throws IOException { + return new Iterable<DirEntry>() { + public Iterator<DirEntry> iterator() { + return new Iterator<DirEntry>() { + Handle handle; + DirEntry[] entries; + int index; + { + open(); + load(); + } + public boolean hasNext() { + return entries != null && index < entries.length; + } + public DirEntry next() { + DirEntry entry = entries[index++]; + if (index >= entries.length) { + load(); + } + return entry; + } + private void open() { + try { + handle = openDir(path); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + private void load() { + try { + entries = readDir(handle); + index = 0; + if (entries == null) { + close(handle); + } + } catch (IOException e) { + entries = null; + try { + close(handle); + } catch (IOException t) { + // Ignore + } + throw new RuntimeException(e); + } + } + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }; + } + + public InputStream read(final String path) throws IOException { + return new InputStream() { + byte[] buffer = new byte[32 * 1024]; + int index = 0; + int available = 0; + Handle handle = DefaultSftpClient.this.open(path, EnumSet.of(OpenMode.Read)); + long offset; + @Override + public int read() throws IOException { + byte[] buffer = new byte[1]; + int read = read(buffer, 0, 1); + if (read > 0) { + return buffer[0]; + } + return read; + } + @Override + public int read(byte[] b, int off, int len) throws IOException { + int idx = off; + while (len > 0) { + if (index >= available) { + available = DefaultSftpClient.this.read(handle, offset, buffer, 0, buffer.length); + if (available < 0) { + if (idx == off) { + return -1; + } else { + break; + } + } + offset += available; + index = 0; + } + if (index >= available) { + break; + } + int nb = Math.min(len, available - index); + System.arraycopy(buffer, index, b, idx, nb); + index += nb; + idx += nb; + len -= nb; + } + return idx - off; + } + @Override + public void close() throws IOException { + DefaultSftpClient.this.close(handle); + } + }; + } + + public OutputStream write(final String path) throws IOException { + return new OutputStream() { + byte[] buffer = new byte[32 * 1024]; + int index = 0; + Handle handle = DefaultSftpClient.this.open(path, EnumSet.of(OpenMode.Write)); + long offset; + @Override + public void write(int b) throws IOException { + byte[] buffer = new byte[1]; + buffer[0] = (byte) b; + write(buffer, 0, 1); + } + @Override + public void write(byte[] b, int off, int len) throws IOException { + do { + int nb = Math.min(len, buffer.length - index); + System.arraycopy(b, off, buffer, index, nb); + index += nb; + if (index == buffer.length) { + flush(); + } + len -= nb; + } while (len > 0); + } + @Override + public void flush() throws IOException { + DefaultSftpClient.this.write(handle, offset, buffer, 0, index); + offset += index; + index = 0; + } + @Override + public void close() throws IOException { + DefaultSftpClient.this.close(handle); + } + }; + } + +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/21c1cee9/sshd-core/src/main/java/org/apache/sshd/common/file/SshFile.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/file/SshFile.java b/sshd-core/src/main/java/org/apache/sshd/common/file/SshFile.java index 511fe4c..1da1748 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/file/SshFile.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/file/SshFile.java @@ -23,6 +23,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.List; +import java.util.Map; +import java.util.Set; /** * This is the file abstraction used by the server. @@ -31,6 +33,21 @@ import java.util.List; */ public interface SshFile { + enum Attribute { + Size, // long + Uid, // int + Owner, // String + Gid, // int + Group, // String + IsDirectory, // boolean + IsRegularFile, // boolean + IsSymbolicLink, // boolean + Permissions, // int + CreationTime, // long + LastModifiedTime, // long + LastAccessTime // long + } + /** * Get the full path from the base directory of the FileSystemView. * @return a path where the path separator is '/' (even if the operating system @@ -44,6 +61,15 @@ public interface SshFile { */ String getName(); + Map<Attribute,Object> getAttributes() throws IOException; + + void setAttributes(Map<Attribute, Object> attributes) throws IOException; + + Object getAttribute(Attribute attribute) throws IOException; + + void setAttribute(Attribute attribute, Object value) throws IOException; + + /** * Get the owner name of the file * @return the name of the owner. http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/21c1cee9/sshd-core/src/main/java/org/apache/sshd/common/file/nativefs/NativeSshFile.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/file/nativefs/NativeSshFile.java b/sshd-core/src/main/java/org/apache/sshd/common/file/nativefs/NativeSshFile.java index d2796fd..ee7cd6e 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/file/nativefs/NativeSshFile.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/file/nativefs/NativeSshFile.java @@ -27,10 +27,22 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.RandomAccessFile; import java.lang.reflect.Method; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.attribute.FileTime; +import java.nio.file.attribute.GroupPrincipal; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.UserPrincipal; +import java.nio.file.attribute.UserPrincipalLookupService; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.StringTokenizer; import org.apache.sshd.common.file.SshFile; @@ -591,4 +603,112 @@ public class NativeSshFile implements SshFile { public String toString() { return fileName; } + + public Map<Attribute, Object> getAttributes() throws IOException { + Map<String, Object> a = Files.readAttributes(file.toPath(), "unix:size,uid,owner,gid,group,isDirectory,isRegularFile,isSymbolicLink,permissions,creationTime,lastModifiedTime,lastAccessTime", LinkOption.NOFOLLOW_LINKS); + Map<Attribute, Object> map = new HashMap<Attribute, Object>(); + map.put(Attribute.Size, a.get("size")); + map.put(Attribute.Uid, a.get("uid")); + map.put(Attribute.Owner, ((UserPrincipal) a.get("owner")).getName()); + map.put(Attribute.Gid, a.get("gid")); + map.put(Attribute.Group, ((GroupPrincipal) a.get("group")).getName()); + map.put(Attribute.IsDirectory, a.get("isDirectory")); + map.put(Attribute.IsRegularFile, a.get("isRegularFile")); + map.put(Attribute.IsSymbolicLink, a.get("isSymbolicLink")); + map.put(Attribute.CreationTime, ((FileTime) a.get("creationTime")).toMillis()); + map.put(Attribute.LastModifiedTime, ((FileTime) a.get("lastModifiedTime")).toMillis()); + map.put(Attribute.LastAccessTime, ((FileTime) a.get("lastAccessTime")).toMillis()); + map.put(Attribute.Permissions, fromPerms((Set<PosixFilePermission>) a.get("permissions"))); + return map; + } + + private int fromPerms(Set<PosixFilePermission> perms) { + int p = 0; + for (PosixFilePermission perm : perms) { + switch (perm) { + case OWNER_READ: p |= 0000400; break; + case OWNER_WRITE: p |= 0000200; break; + case OWNER_EXECUTE: p |= 0000100; break; + case GROUP_READ: p |= 0000040; break; + case GROUP_WRITE: p |= 0000020; break; + case GROUP_EXECUTE: p |= 0000010; break; + case OTHERS_READ: p |= 0000004; break; + case OTHERS_WRITE: p |= 0000002; break; + case OTHERS_EXECUTE: p |= 0000001; break; + } + } + return p; + } + + public void setAttributes(Map<Attribute, Object> attributes) throws IOException { + for (Attribute attribute : attributes.keySet()) { + String name = null; + Object value = attributes.get(attribute); + switch (attribute) { + case Uid: name = "unix:uid"; break; + case Owner: name = "unix:owner"; value = toUser((String) value); break; + case Gid: name = "unix:gid"; break; + case Group: name = "unix:group"; value = toGroup((String) value); break; + case CreationTime: name = "unix:creationTime"; value = FileTime.fromMillis((Long) value); break; + case LastModifiedTime: name = "unix:lastModifiedTime"; value = FileTime.fromMillis((Long) value); break; + case LastAccessTime: name = "unix:lastAccessTime"; value = FileTime.fromMillis((Long) value); break; + case Permissions: name = "unix:permissions"; value = toPerms((Integer) value); break; + } + if (name != null && value != null) { + Files.setAttribute(file.toPath(), name, value, LinkOption.NOFOLLOW_LINKS); + } + } + } + + private GroupPrincipal toGroup(String name) throws IOException { + UserPrincipalLookupService lookupService = file.toPath().getFileSystem().getUserPrincipalLookupService(); + return lookupService.lookupPrincipalByGroupName(name); + } + + private UserPrincipal toUser(String name) throws IOException { + UserPrincipalLookupService lookupService = file.toPath().getFileSystem().getUserPrincipalLookupService(); + return lookupService.lookupPrincipalByName(name); + } + + private Set<PosixFilePermission> toPerms(int perms) { + Set<PosixFilePermission> p = new HashSet<PosixFilePermission>(); + if ((perms & 0000400) != 0) { + p.add(PosixFilePermission.OWNER_READ); + } + if ((perms & 0000200) != 0) { + p.add(PosixFilePermission.OWNER_WRITE); + } + if ((perms & 0000100) != 0) { + p.add(PosixFilePermission.OWNER_EXECUTE); + } + if ((perms & 0000040) != 0) { + p.add(PosixFilePermission.GROUP_READ); + } + if ((perms & 0000020) != 0) { + p.add(PosixFilePermission.GROUP_WRITE); + } + if ((perms & 0000010) != 0) { + p.add(PosixFilePermission.GROUP_EXECUTE); + } + if ((perms & 0000004) != 0) { + p.add(PosixFilePermission.OTHERS_READ); + } + if ((perms & 0000002) != 0) { + p.add(PosixFilePermission.OTHERS_WRITE); + } + if ((perms & 0000001) != 0) { + p.add(PosixFilePermission.OTHERS_EXECUTE); + } + return p; + } + + public Object getAttribute(Attribute attribute) throws IOException { + return getAttributes().get(attribute); + } + + public void setAttribute(Attribute attribute, Object value) throws IOException { + Map<Attribute, Object> map = new HashMap<Attribute, Object>(); + map.put(attribute, value); + setAttributes(map); + } }
